From 4e7d91d2377142a065bfb22aa5c0577f782d106c Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 1 Aug 2018 14:26:48 +0200 Subject: [PATCH 001/122] future warnings fixes --- activitysim/abm/models/util/cdap.py | 5 +- activitysim/core/assign.py | 91 +++++++++--------------- activitysim/core/interaction_simulate.py | 24 ++++--- activitysim/core/simulate.py | 38 ++++++++-- 4 files changed, 84 insertions(+), 74 deletions(-) diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 3bc967b3b..ff66885c5 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -10,6 +10,7 @@ from activitysim.core.simulate import eval_variables from activitysim.core.simulate import compute_utilities +from activitysim.core.simulate import uniquify_spec_index from activitysim.core import chunk from activitysim.core import logit @@ -249,7 +250,7 @@ def build_cdap_spec(interaction_coefficients, hhsize, for households of specified size. We generate this spec automatically from a table of rules and coefficients because the - interaction rulkes are fairly simple and can be expressed compactly whereas + interaction rules are fairly simple and can be expressed compactly whereas there is a lot of redundancy between the spec files for different household sizes, as well as in the vectorized expression of the interaction alternatives within the spec file itself @@ -388,6 +389,8 @@ def build_cdap_spec(interaction_coefficients, hhsize, # eval expression goes in the index spec.set_index(expression_name, inplace=True) + uniquify_spec_index(spec) + if trace_spec: tracing.trace_df(spec, '%s.hhsize%d_spec' % (trace_label, hhsize), transpose=False, slicer='NONE') diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 552b56e3c..237706f0c 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -3,6 +3,7 @@ import logging import os +from collections import OrderedDict import numpy as np import pandas as pd @@ -13,6 +14,21 @@ logger = logging.getLogger(__name__) +def uniquify_key(dict, key, template="{} ({})"): + """ + rename key so there are no duplicates with keys in dice + + e.g. if there is already a key named "dog", the second key will be reformatted to "dog (2)" + """ + n = 1 + new_key = key + while new_key in dict: + n += 1 + new_key = template.format(key, n) + + return new_key + + def evaluate_constants(expressions, constants): """ Evaluate a list of constant expressions - each one can depend on the one before @@ -42,38 +58,6 @@ def evaluate_constants(expressions, constants): return d -def undupe_column_names(df, template="{} ({})"): - """ - rename df column names so there are no duplicates (in place) - - e.g. if there are two columns named "dog", the second column will be reformatted to "dog (2)" - - Parameters - ---------- - df : pandas.DataFrame - dataframe whose column names should be de-duplicated - template : template taking two arguments (old_name, int) to use to rename columns - - Returns - ------- - df : pandas.DataFrame - dataframe that was renamed in place, for convenience in chaining - """ - - new_names = [] - seen = set() - for name in df.columns: - n = 1 - new_name = name - while new_name in seen: - n += 1 - new_name = template.format(name, n) - new_names.append(new_name) - seen.add(new_name) - df.columns = new_names - return df - - def read_assignment_spec(fname, description_name="Description", target_name="Target", @@ -217,8 +201,8 @@ def to_series(x, target=None): # convert to numpy array so we can slice ndarrays as well as series trace_rows = np.asanyarray(trace_rows) if trace_rows.any(): - trace_results = [] - trace_assigned_locals = {} + trace_results = OrderedDict() + trace_assigned_locals = OrderedDict() # avoid touching caller's passed-in locals_d parameter (they may be looping) _locals_dict = local_utilities() @@ -230,7 +214,10 @@ def to_series(x, target=None): _locals_dict['df'] = df local_keys = _locals_dict.keys() - target_history = [] + # build a dataframe of eval results for non-temp targets + # since we allow targets to be recycled, we want to only keep the last usage + variables = OrderedDict() + # need to be able to identify which variables causes an error, which keeps # this from being expressed more parsimoniously for e in zip(assignment_expressions.target, assignment_expressions.expression): @@ -246,7 +233,7 @@ def to_series(x, target=None): x = eval(expression, globals(), _locals_dict) _locals_dict[target] = x if trace_assigned_locals is not None: - trace_assigned_locals[target] = x + trace_assigned_locals[uniquify_key(trace_assigned_locals, target)] = x continue try: @@ -273,41 +260,27 @@ def to_series(x, target=None): # values = to_series(None, target=target) raise err - target_history.append((target, values)) + if not is_temp(target): + variables[target] = values if trace_results is not None: - trace_results.append((target, values[trace_rows])) + trace_results[uniquify_key(trace_results, target)] = values[trace_rows] # update locals to allows us to ref previously assigned targets _locals_dict[target] = values - # build a dataframe of eval results for non-temp targets - # since we allow targets to be recycled, we want to only keep the last usage - # we scan through targets in reverse order and add them to the front of the list - # the first time we see them so they end up in execution order - variables = [] - seen = set() - for statement in reversed(target_history): - # statement is a tuple (, ) - target_name = statement[0] - if not is_temp(target_name) and target_name not in seen: - variables.insert(0, statement) - seen.add(target_name) - - # DataFrame from list of tuples [, ), ...] - variables = pd.DataFrame.from_items(variables) - # in case items were numpy arrays not pandas series, fix index - variables.index = df.index - if trace_results is not None: - trace_results = pd.DataFrame.from_items(trace_results) + trace_results = pd.DataFrame.from_dict(trace_results) trace_results.index = df[trace_rows].index - trace_results = undupe_column_names(trace_results) - # add df columns to trace_results trace_results = pd.concat([df[trace_rows], trace_results], axis=1) + # we stored result in dict - convert to df + variables = pd.DataFrame.from_dict(variables) + # in case items were numpy arrays not pandas series, fix index + variables.index = df.index + return variables, trace_results, trace_assigned_locals diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 64357b33f..ead59dd36 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -5,12 +5,15 @@ import numpy as np import pandas as pd +from collections import OrderedDict from . import logit from . import tracing from .simulate import set_skim_wrapper_targets from . import chunk +from . import assign + from activitysim.core.util import force_garbage_collect @@ -76,7 +79,7 @@ def to_series(x): # # convert to numpy array so we can slice ndarrays as well as series # trace_rows = np.asanyarray(trace_rows) assert type(trace_rows) == np.ndarray - trace_eval_results = [] + trace_eval_results = OrderedDict() else: trace_eval_results = None @@ -108,10 +111,15 @@ def to_series(x): utilities.utility += (v * coefficient).astype('float') if trace_eval_results is not None: - trace_eval_results.append((expr, - v[trace_rows])) - trace_eval_results.append(('partial utility (coefficient = %s)' % coefficient, - v[trace_rows]*coefficient)) + + # expressions should have been uniquified when spec was read + # (though we could do it here if need be...) + # expr = assign.uniquify_key(trace_eval_results, expr, template="{} # ({})") + assert expr not in trace_eval_results + + trace_eval_results[expr] = v[trace_rows] + k = 'partial utility (coefficient = %s) for %s' % (coefficient, expr) + trace_eval_results[k] = v[trace_rows] * coefficient except Exception as err: logger.exception("Variable evaluation failed for: %s" % str(expr)) @@ -124,11 +132,9 @@ def to_series(x): logger.warn("%s: %s columns have missing values" % (trace_label, has_missing_vals)) if trace_eval_results is not None: + trace_eval_results['total utility'] = utilities.utility[trace_rows] - trace_eval_results.append(('total utility', - utilities.utility[trace_rows])) - - trace_eval_results = pd.DataFrame.from_items(trace_eval_results) + trace_eval_results = pd.DataFrame.from_dict(trace_eval_results) trace_eval_results.index = df[trace_rows].index # add df columns to trace_results diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index a85a564dc..cdee661d5 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -3,9 +3,9 @@ from __future__ import print_function -from math import ceil import os import logging +from collections import OrderedDict import numpy as np import pandas as pd @@ -15,7 +15,6 @@ from . import tracing from . import pipeline -from . import util from . import assign from . import chunk @@ -34,6 +33,23 @@ def random_rows(df, n): return df +def uniquify_spec_index(spec): + + # uniquify spec index inplace + # ensure uniqueness of spec index by appending comment with dupe count + # this allows us to use pandas dot to compute_utilities + dict = OrderedDict() + for expr in spec.index: + dict[assign.uniquify_key(dict, expr, template="{} # ({})")] = expr + + # bug + prev_index_name = spec.index.name + spec.index = dict.keys() + spec.index.name = prev_index_name + + assert spec.index.is_unique + + def read_model_spec(fpath, fname, description_name="Description", expression_name="Expression"): @@ -76,6 +92,10 @@ def read_model_spec(fpath, fname, spec = spec.set_index(expression_name).fillna(0) + # ensure uniqueness of spec index by appending comment with dupe count + # this allows us to use pandas dot to compute_utilities + uniquify_spec_index(spec) + # drop any rows with all zeros since they won't have any effect (0 marginal utility) zero_rows = (spec == 0).all(axis=1) if zero_rows.any(): @@ -130,7 +150,7 @@ def to_series(x): return pd.Series([x] * len(df), index=df.index) return x - value_list = [] + values = OrderedDict() print('eval_variables', end='') # print ... for each expression for expr in exprs: print('.', end='') # print ... @@ -141,7 +161,9 @@ def to_series(x): expr_values = to_series(eval(expr[1:], globals_dict, locals_dict)) else: expr_values = df.eval(expr) - value_list.append((expr, expr_values)) + # read model spec should ensure uniqueness, otherwise we should uniquify + assert expr not in values + values[expr] = expr_values except Exception as err: print() # print ... logger.exception("Variable evaluation failed for: %s" % str(expr)) @@ -149,7 +171,7 @@ def to_series(x): raise err print() # print ... - values = pd.DataFrame.from_items(value_list) + values = pd.DataFrame.from_dict(values) # FIXME - for performance, it is essential that spec and expression_values # FIXME - not contain booleans when dotted with spec values @@ -173,6 +195,12 @@ def compute_utilities(expression_values, spec): spec = spec.astype(np.float64) + # pandas.dot depends on column names of expression_values matching spec index values + # expressions should have been uniquified when spec was read + # we could do it here if need be, and then set spec.index and expression_values.columns equal + assert spec.index.is_unique + assert (spec.index.values == expression_values.columns.values).all() + utilities = expression_values.dot(spec) return utilities From 2a5ea23702d4c3374724a2beb89c24f53bd73da8 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 2 Aug 2018 12:53:10 +0200 Subject: [PATCH 002/122] replace deprecated pandas as_matrix calls) --- activitysim/core/interaction_sample.py | 6 +++--- activitysim/core/interaction_simulate.py | 2 +- activitysim/core/logit.py | 4 ++-- activitysim/core/pipeline.py | 10 ++++------ activitysim/core/simulate.py | 2 +- activitysim/core/skim.py | 2 +- activitysim/core/test/test_logit.py | 2 +- activitysim/core/test/test_simulate.py | 2 +- activitysim/core/timetable.py | 12 ++++++------ docs/howitworks.rst | 2 +- 10 files changed, 21 insertions(+), 23 deletions(-) diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index b078ac955..995670d6e 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -64,11 +64,11 @@ def make_sample_choices( probs = probs[~zero_probs] choosers = choosers[~zero_probs] - cum_probs_arr = probs.as_matrix().cumsum(axis=1) + cum_probs_arr = probs.values.cumsum(axis=1) # alt probs in convenient layout to return prob of chose alternative # (same layout as cum_probs_arr) - alt_probs_array = probs.as_matrix().flatten() + alt_probs_array = probs.values.flatten() # get sample_size rands for each chooser # transform as we iterate over alternatives @@ -252,7 +252,7 @@ def _interaction_sample( # reshape utilities (one utility column and one row per row in interaction_utilities) # to a dataframe with one row per chooser and one column per alternative utilities = pd.DataFrame( - interaction_utilities.as_matrix().reshape(len(choosers), alternative_count), + interaction_utilities.values.reshape(len(choosers), alternative_count), index=choosers.index) cum_size = chunk.log_df_size(trace_label, 'utilities', utilities, cum_size) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index ead59dd36..8ff76bd62 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -257,7 +257,7 @@ def _interaction_simulate( # reshape utilities (one utility column and one row per row in model_design) # to a dataframe with one row per chooser and one column per alternative utilities = pd.DataFrame( - interaction_utilities.as_matrix().reshape(len(choosers), sample_size), + interaction_utilities.values.reshape(len(choosers), sample_size), index=choosers.index) if have_trace_targets: diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 752dc41c3..5284fd5ac 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -98,7 +98,7 @@ def utils_to_probs(utils, trace_label=None, exponentiated=False, allow_zero_prob """ trace_label = tracing.extend_trace_label(trace_label, 'utils_to_probs') - utils_arr = utils.as_matrix().astype('float') + utils_arr = utils.values.astype('float') if not exponentiated: utils_arr = np.exp(utils_arr) @@ -187,7 +187,7 @@ def make_choices(probs, trace_label=None, trace_choosers=None): rands = pipeline.get_rn_generator().random_for_df(probs) - probs_arr = probs.as_matrix().cumsum(axis=1) - rands + probs_arr = probs.values.cumsum(axis=1) - rands # rows, cols = np.where(probs_arr > 0) # choices = [s.iat[0] for _, s in pd.Series(cols).groupby(rows)] diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index aeddcd198..72d73f040 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -699,15 +699,13 @@ def extend_table(table_name, df): if orca.is_table(table_name): - extend_df = orca.get_table(table_name).to_frame() + table_df = orca.get_table(table_name).to_frame() # don't expect indexes to overlap - assert len(extend_df.index.intersection(df.index)) == 0 + assert len(table_df.index.intersection(df.index)) == 0 - # preserve existing column order (concat reorders columns) - columns = list(extend_df.columns) + [c for c in df.columns if c not in extend_df.columns] - - df = pd.concat([extend_df, df])[columns] + # preserve existing column order + df = pd.concat([table_df, df], sort=False) replace_table(table_name, df) diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index cdee661d5..b1775934d 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -770,7 +770,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): # t0 = tracing.print_elapsed_time("compute_utilities", t0, debug=True) # logsum is log of exponentiated utilities summed across columns of each chooser row - utils_arr = utilities.as_matrix().astype('float') + utils_arr = utilities.values.astype('float') logsums = np.log(np.exp(utils_arr).sum(axis=1)) logsums = pd.Series(logsums, index=choosers.index) diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 942d3d10d..4ea4e25f1 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -509,7 +509,7 @@ def __init__(self, df): """ self.df = df - self.data = df.as_matrix() + self.data = df.values self.offset_mapper = OffsetMapper() self.offset_mapper.set_offset_list(list(df.index)) diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index 9798b51e2..cca1a53da 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -65,7 +65,7 @@ def utilities(choosers, spec, test_data): vars = eval_variables(spec.index, choosers) utils = vars.dot(spec).astype('float') return pd.DataFrame( - utils.as_matrix().reshape(test_data['probabilities'].shape), + utils.values.reshape(test_data['probabilities'].shape), columns=test_data['probabilities'].columns) diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index eee7d7880..9cf91a03f 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -46,7 +46,7 @@ def test_read_model_spec(data_dir, spec_name): assert spec.index.name == 'expression' assert list(spec.columns) == ['alt0', 'alt1'] npt.assert_array_equal( - spec.as_matrix(), + spec.values, [[1.1, 11], [2.2, 22], [3.3, 33], [4.4, 44]]) diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index 4bea0fe0a..f69c6134f 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -97,7 +97,7 @@ def tour_map(persons, tours, tdd_alts, persons_id_col='person_id'): tour_sigil = sigil[tour_type] # numpy array with one time window row for each row in nth_tours - tour_windows = window_periods_df.loc[nth_tours.tdd].as_matrix() + tour_windows = window_periods_df.loc[nth_tours.tdd].values # row idxs of tour_df group rows in windows row_ixs = nth_tours[persons_id_col].map(row_ix_map).values @@ -186,7 +186,7 @@ def __init__(self, windows_df, tdd_alts_df, table_name=None): self.windows_table_name = table_name self.windows_df = windows_df - self.windows = self.windows_df.as_matrix() + self.windows = self.windows_df.values # series to map window row index value to window row's ordinal index self.window_row_ix = pd.Series(range(len(windows_df.index)), index=windows_df.index) @@ -278,10 +278,10 @@ def tour_available(self, window_row_ids, tdds): # t0 = tracing.print_elapsed_time() # numpy array with one tdd_footprints_df row for tdds - tour_footprints = util.quick_loc_df(tdds, self.tdd_footprints_df).as_matrix() + tour_footprints = util.quick_loc_df(tdds, self.tdd_footprints_df).values # t0 = tracing.print_elapsed_time("tour_footprints", t0, debug=True) - # assert (tour_footprints == self.tdd_footprints_df.loc[tdds].as_matrix()).all + # assert (tour_footprints == self.tdd_footprints_df.loc[tdds].values).all # numpy array with one windows row for each person windows = self.slice_windows_by_row_id(window_row_ids) @@ -321,7 +321,7 @@ def assign(self, window_row_ids, tdds): tour_footprints = self.tdd_footprints_df.loc[tdds] # numpy array with one time window row for each row in df - tour_footprints = tour_footprints.as_matrix() + tour_footprints = tour_footprints.values # row idxs of windows to assign to row_ixs = window_row_ids.map(self.window_row_ix).values @@ -361,7 +361,7 @@ def assign_subtour_mask(self, window_row_ids, tdds): tour_footprints = self.tdd_footprints_df.loc[tdds] # numpy array with one time window row for each row in df - tour_footprints = tour_footprints.as_matrix() + tour_footprints = tour_footprints.values # row idxs of windows to assign to row_ixs = window_row_ids.map(self.window_row_ix).values diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 156c26a27..00288459e 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -279,7 +279,7 @@ and selecting numpy array items with vector indexes returns a vector. Trace dat # reshape utilities (one utility column and one row per row in model_design) # to a dataframe with one row per chooser and one column per alternative utilities = pd.DataFrame( - interaction_utilities.as_matrix().reshape(len(choosers), alternative_count), + interaction_utilities.values.reshape(len(choosers), alternative_count), index=choosers.index) # convert to probabilities (utilities exponentiated and normalized to probs) From d75ba4f1fcbf21475dbf5b72b06a6012b4b40395 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 14 Aug 2018 12:10:43 +0200 Subject: [PATCH 003/122] fix rows_per_chunk to avoid rounding above chunk_size --- activitysim/core/chunk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 7d50dc93b..e87ffc5a2 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -51,12 +51,15 @@ def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): # closest number of chooser rows to achieve chunk_size rpc = int(round(chunk_size / float(row_size))) + + rpc = int(chunk_size / float(row_size)) + rpc = max(rpc, 1) rpc = min(rpc, num_choosers) # chunks = int(ceil(num_choosers / float(rpc))) # effective_chunk_size = row_size * rpc - + # # logger.debug("%s #chunk_calc chunk_size %s" % (trace_label, chunk_size)) # logger.debug("%s #chunk_calc num_choosers %s" % (trace_label, num_choosers)) # logger.debug("%s #chunk_calc total row_size %s" % (trace_label, row_size)) From 22d7beaf2677230884320af9258067191635af49 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 4 Sep 2018 14:31:41 -0400 Subject: [PATCH 004/122] tracing builds rather than imports traceable_table_refs random uses step_name hash instead of step_num update version number --- .gitignore | 2 +- activitysim/abm/misc.py | 21 - activitysim/abm/models/accessibility.py | 60 +-- .../abm/models/atwork_subtour_destination.py | 26 +- activitysim/abm/models/initialize.py | 43 +- .../abm/models/joint_tour_participation.py | 6 +- activitysim/abm/models/school_location.py | 44 ++- activitysim/abm/models/trip_destination.py | 7 +- activitysim/abm/models/util/cdap.py | 6 +- activitysim/abm/models/util/tour_frequency.py | 4 +- activitysim/abm/models/workplace_location.py | 36 +- activitysim/abm/tables/__init__.py | 4 +- activitysim/abm/tables/households.py | 38 +- activitysim/abm/tables/input_store.py | 41 ++ activitysim/abm/tables/landuse.py | 7 +- activitysim/abm/tables/persons.py | 25 +- activitysim/abm/tables/random_channels.py | 41 -- activitysim/abm/tables/skims.py | 110 ++++-- activitysim/abm/tables/table_dict.py | 33 ++ activitysim/abm/test/configs/initialize.yaml | 43 -- .../test/configs/initialize_households.yaml | 18 + .../abm/test/configs/initialize_landuse.yaml | 9 + activitysim/abm/test/configs/settings.yaml | 2 +- activitysim/abm/test/test_misc.py | 10 - activitysim/abm/test/test_pipeline.py | 213 +++++----- activitysim/core/config.py | 42 +- activitysim/core/inject.py | 18 - activitysim/core/inject_defaults.py | 17 +- activitysim/core/pipeline.py | 111 +++--- activitysim/core/random.py | 229 +++++------ activitysim/core/skim.py | 101 ++--- activitysim/core/steps/output.py | 36 +- .../core/test/configs/custom_logging.yaml | 2 +- activitysim/core/test/configs/logging.yaml | 2 +- activitysim/core/test/extensions/steps.py | 2 +- activitysim/core/test/test_logit.py | 2 +- activitysim/core/test/test_random.py | 42 +- activitysim/core/test/test_skim.py | 18 +- activitysim/core/test/test_tracing.py | 66 ++-- activitysim/core/timetable.py | 7 +- activitysim/core/tracing.py | 368 +++++------------- docs/abmexample.rst | 5 +- docs/howitworks.rst | 12 +- example/configs/initialize.yaml | 43 -- example/configs/initialize_households.yaml | 18 + example/configs/initialize_landuse.yaml | 9 + example/configs/logging.yaml | 2 +- example/configs/settings.yaml | 8 +- example/simulation.py | 58 +-- example_multi/configs/logging.yaml | 2 +- example_multi/extensions/skims.py | 5 + scripts/make_pipeline_output.py | 4 +- setup.py | 2 +- 53 files changed, 983 insertions(+), 1097 deletions(-) create mode 100644 activitysim/abm/tables/input_store.py delete mode 100644 activitysim/abm/tables/random_channels.py create mode 100644 activitysim/abm/tables/table_dict.py delete mode 100644 activitysim/abm/test/configs/initialize.yaml create mode 100644 activitysim/abm/test/configs/initialize_households.yaml create mode 100644 activitysim/abm/test/configs/initialize_landuse.yaml delete mode 100644 example/configs/initialize.yaml create mode 100644 example/configs/initialize_households.yaml create mode 100644 example/configs/initialize_landuse.yaml diff --git a/.gitignore b/.gitignore index 9a2ba377c..fbafd8c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ sandbox/ -sandbox.py +example_mp/ example/data/* .idea .ipynb_checkpoints diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index 174d8ac34..5b177fd5f 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -19,27 +19,6 @@ logger = logging.getLogger(__name__) -@inject.injectable(cache=True) -def store(data_dir, settings): - if 'store' not in settings: - logger.error("store file name not specified in settings") - raise RuntimeError("store file name not specified in settings") - fname = os.path.join(data_dir, settings["store"]) - if not os.path.exists(fname): - logger.error("store file not found: %s" % fname) - raise RuntimeError("store file not found: %s" % fname) - - file = pd.HDFStore(fname, mode='r') - pipeline.close_on_exit(file, fname) - - return file - - -@inject.injectable(cache=True) -def cache_skim_key_values(settings): - return settings['skim_time_periods']['labels'] - - @inject.injectable(cache=True) def households_sample_size(settings, override_hh_ids): diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 60c1fb4ac..9fb9837f1 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -34,11 +34,22 @@ class AccessibilitySkims(object): whether to transpose the matrix before flattening. (i.e. act as a D-O instead of O-D skim) """ - def __init__(self, skim_dict, omx, length, transpose=False): + def __init__(self, skim_dict, dest_zones, orig_zones, transpose=False): + + assert skim_dict.skim_data.shape[0] == len(dest_zones) + assert len(orig_zones) <= len(dest_zones) + assert np.isin(orig_zones, dest_zones).all() + assert len(np.unique(orig_zones)) == len(orig_zones) + assert len(np.unique(dest_zones)) == len(dest_zones) + self.skim_dict = skim_dict - self.omx = omx - self.length = length self.transpose = transpose + self.orig_map = None + + if len(orig_zones) < len(dest_zones): + self.orig_map = np.isin(dest_zones, orig_zones) + else: + self.orig_map = None def __getitem__(self, key): """ @@ -48,19 +59,16 @@ def __getitem__(self, key): this allows the skim array to be accessed from expressions as skim['DISTANCE'] or skim[('SOVTOLL_TIME', 'MD')] """ - try: - data = self.skim_dict.get(key).data - except KeyError: - omx_key = '__'.join(key) - logger.info("AccessibilitySkims loading %s from omx as %s" % (key, omx_key,)) - data = self.omx[omx_key] - data = data[:self.length, :self.length] + data = self.skim_dict.get(key).data if self.transpose: - return data.transpose().flatten() - else: - return data.flatten() + data = data.transpose() + + if self.orig_map is not None: + data = data[self.orig_map, :] + + return data.flatten() @inject.injectable() @@ -70,8 +78,8 @@ def accessibility_spec(configs_dir): @inject.step() -def compute_accessibility(accessibility_spec, - skim_dict, omx_file, land_use, trace_od): +def compute_accessibility(accessibility, accessibility_spec, + skim_dict, land_use, trace_od): """ Compute accessibility for each zone in land use file using expressions from accessibility_spec @@ -92,18 +100,24 @@ def compute_accessibility(accessibility_spec, logger.info("Running compute_accessibility") model_settings = config.read_model_settings('accessibility.yaml') + accessibility_df = accessibility.to_frame() + constants = config.get_model_constants(model_settings) land_use_columns = model_settings.get('land_use_columns', []) land_use_df = land_use.to_frame() - zone_count = len(land_use_df.index) + orig_zones = accessibility_df.index.values + dest_zones = land_use_df.index.values + + orig_zone_count = len(orig_zones) + dest_zone_count = len(dest_zones) # create OD dataframe od_df = pd.DataFrame( data={ - 'orig': np.repeat(np.asanyarray(land_use_df.index), zone_count), - 'dest': np.tile(np.asanyarray(land_use_df.index), zone_count) + 'orig': np.repeat(np.asanyarray(accessibility_df.index), dest_zone_count), + 'dest': np.tile(np.asanyarray(land_use_df.index), orig_zone_count) } ) @@ -120,18 +134,18 @@ def compute_accessibility(accessibility_spec, locals_d = { 'log': np.log, 'exp': np.exp, - 'skim_od': AccessibilitySkims(skim_dict, omx_file, zone_count), - 'skim_do': AccessibilitySkims(skim_dict, omx_file, zone_count, transpose=True) + 'skim_od': AccessibilitySkims(skim_dict, dest_zones, orig_zones), + 'skim_do': AccessibilitySkims(skim_dict, dest_zones, orig_zones, transpose=True) } if constants is not None: locals_d.update(constants) results, trace_results, trace_assigned_locals \ = assign.assign_variables(accessibility_spec, od_df, locals_d, trace_rows=trace_od_rows) - accessibility_df = pd.DataFrame(index=land_use.index) + for column in results.columns: data = np.asanyarray(results[column]) - data.shape = (zone_count, zone_count) + data.shape = (orig_zone_count, dest_zone_count) accessibility_df[column] = np.log(np.sum(data, axis=1) + 1) # - write table to pipeline @@ -147,8 +161,6 @@ def compute_accessibility(accessibility_spec, df = pd.concat([od_df[trace_od_rows], trace_results], axis=1) # dump the trace results table (with _temp variables) to aid debugging - # note that this is not the same as the orca-injected accessibility table - # FIXME - should we name this differently and also dump the updated accessibility table? tracing.trace_df(df, label='accessibility', index_label='skim_offset', diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index bf29f32ce..6928a1dcf 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -108,19 +108,19 @@ def atwork_subtour_destination_logsums(persons_merged, logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair in workplace_location_sample, and computing the logsum of all the utilities - +-------+--------------+----------------+------------+----------------+ - | PERID | dest_TAZ | rand | pick_count | logsum (added) | - +=======+==============+================+============+================+ - | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | - +-------+--------------+----------------+------------+----------------+ - + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | - +-------+--------------+----------------+------------+----------------+ - + ... | | | | | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | - +-------+--------------+----------------+------------+----------------+ + +-----------+--------------+----------------+------------+----------------+ + | person_id | dest_TAZ | rand | pick_count | logsum (added) | + +===========+==============+================+============+================+ + | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | + +-----------+--------------+----------------+------------+----------------+ + + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | + +-----------+--------------+----------------+------------+----------------+ + + ... | | | | | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | + +-----------+--------------+----------------+------------+----------------+ """ diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 3c9c7e6bb..f6a901dc7 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -22,21 +22,15 @@ logger = logging.getLogger(__name__) -@inject.step() -def initialize(configs_dir): - """ - Because random seed is set differently for each step, the sampling of households depends - on which step they are initially loaded in so we force them to load here and they get - stored to the pipeline, - """ +def annotate_tables(model_settings, trace_label): - trace_label = 'initialize' + annotate_tables = model_settings.get('annotate_tables', []) - model_settings = config.read_model_settings('initialize.yaml') + if not annotate_tables: + logger.warn("annotate_tables setting is empty - nothing to do!") t0 = tracing.print_elapsed_time() - annotate_tables = model_settings.get('annotate_tables') for table_info in annotate_tables: tablename = table_info['tablename'] @@ -60,8 +54,35 @@ def initialize(configs_dir): # - write table to pipeline pipeline.replace_table(tablename, df) - t0 = tracing.print_elapsed_time("annotate %s" % tablename, t0, debug=True) +@inject.step() +def initialize_landuse(configs_dir): + + trace_label = 'initialize_landuse' + + model_settings = config.read_model_settings('initialize_landuse.yaml', mandatory=True) + + annotate_tables(model_settings, trace_label) + + # create accessibility + land_use = pipeline.get_table('land_use') + + accessibility_df = pd.DataFrame(index=land_use.index) + + # - write table to pipeline + pipeline.replace_table("accessibility", accessibility_df) + + +@inject.step() +def initialize_households(configs_dir): + + trace_label = 'initialize_households' + + model_settings = config.read_model_settings('initialize_households.yaml', mandatory=True) + + annotate_tables(model_settings, trace_label) + + t0 = tracing.print_elapsed_time() inject.get_table('person_windows').to_frame() t0 = tracing.print_elapsed_time("preload person_windows", t0, debug=True) diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index faecc6aca..696ed7cc0 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -236,7 +236,7 @@ def joint_tour_participation( # - create joint_tour_participation_candidates table candidates = joint_tour_participation_candidates(joint_tours, persons_merged) - tracing.register_traceable_table('participants', candidates) + tracing.register_traceable_table('joint_tour_participants', candidates) pipeline.get_rn_generator().add_channel(candidates, 'joint_tour_participants') logger.info("Running joint_tours_participation with %d potential participants (candidates)" % @@ -298,8 +298,8 @@ def joint_tour_participation( pipeline.replace_table("joint_tour_participants", participants) - # FIXME drop channel if we aren't using any more? - # pipeline.get_rn_generator().drop_channel('joint_tours_participants') + # drop channel as we aren't using any more (and it has candidates that weren't chosen) + pipeline.get_rn_generator().drop_channel('joint_tours_participants') # - assign joint tour 'point person' (participant_num == 1) point_persons = participants[participants.participant_num == 1] diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 2c10511c0..76352af80 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -57,12 +57,18 @@ def school_location_sample( """ build a table of persons * all zones to select a sample of alternative school locations. - PERID, dest_TAZ, rand, pick_count - 23750, 14, 0.565502716034, 4 - 23750, 16, 0.711135838871, 6 - ... - 23751, 12, 0.408038878552, 1 - 23751, 14, 0.972732479292, 2 + | PERID | dest_TAZ | rand | pick_count | + +===========+==============+================+============+ + | 23750 | 14 | 0.565502716034 | 4 | + +-----------+--------------+----------------+------------+ + + 23750 | 16 | 0.711135838871 | 6 | + +-----------+--------------+----------------+------------+ + + ... | | | | + +-----------+--------------+----------------+------------+ + | 23751 | 12 | 0.408038878552 | 1 | + +-----------+--------------+----------------+------------+ + | 23751 | 14 | 0.972732479292 | 2 | + +-----------+--------------+----------------+------------+ """ trace_label = 'school_location_sample' @@ -151,19 +157,19 @@ def school_location_logsums( logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair in school_location_sample, and computing the logsum of all the utilities - +-------+--------------+----------------+------------+----------------+ - | PERID | dest_TAZ | rand | pick_count | logsum (added) | - +=======+==============+================+============+================+ - | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | - +-------+--------------+----------------+------------+----------------+ - + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | - +-------+--------------+----------------+------------+----------------+ - + ... | | | | | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | - +-------+--------------+----------------+------------+----------------+ + +-----------+--------------+----------------+------------+----------------+ + | person_id | dest_TAZ | rand | pick_count | logsum (added) | + +===========+==============+================+============+================+ + | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | + +-----------+--------------+----------------+------------+----------------+ + + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | + +-----------+--------------+----------------+------------+----------------+ + + ... | | | | | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | + +-----------+--------------+----------------+------------+----------------+ """ trace_label = 'school_location_logsums' diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 2896f858a..c554ed5ad 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -537,6 +537,8 @@ def trip_destination( trips_df = trips.to_frame() tours_merged_df = tours_merged.to_frame() + logger.info("Running %s with %d trips" % (trace_label, trips_df.shape[0])) + trips_df = run_trip_destination( trips_df, tours_merged_df, @@ -556,8 +558,11 @@ def trip_destination( pipeline.replace_table("trips", trips_df) + print "trips_df\n", trips_df.shape + if trace_hh_id: tracing.trace_df(trips_df, label=trace_label, slicer='trip_id', - index_label='trip_id') + index_label='trip_id', + warn_if_empty=True) diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index ff66885c5..a6df0cbc1 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -21,8 +21,8 @@ # FIXME - this allows us to turn some dev debug table dump code on and off - eventually remove? # DUMP = False -_persons_index_ = 'PERID' -_hh_index_ = 'HHID' +_persons_index_ = 'person_id' +_hh_index_ = 'household_id' _hh_size_ = 'hhsize' _hh_id_ = 'household_id' @@ -765,7 +765,7 @@ def _run_cdap( assign_cdap_rank(persons, trace_hh_id, trace_label) # Calculate CDAP utilities for each individual, ignoring interactions - # ind_utils has index of `PERID` and a column for each alternative + # ind_utils has index of 'person_id' and a column for each alternative # i.e. three columns 'M' (Mandatory), 'N' (NonMandatory), 'H' (Home) indiv_utils = individual_utilities(persons[persons.cdap_rank <= MAX_HHSIZE], cdap_indiv_spec, locals_d, diff --git a/activitysim/abm/models/util/tour_frequency.py b/activitysim/abm/models/util/tour_frequency.py index ec5813ecb..bce49f078 100644 --- a/activitysim/abm/models/util/tour_frequency.py +++ b/activitysim/abm/models/util/tour_frequency.py @@ -174,7 +174,7 @@ def process_tours(tour_frequency, tour_frequency_alts, tour_category, parent_col """ alt1 alt2 alt3 - PERID + person_id 2588676 2 0 0 2588677 1 1 0 """ @@ -488,7 +488,7 @@ def process_joint_tours(joint_tour_frequency, joint_tour_frequency_alts, point_p """ household_id tour_type tour_type_count tour_type_num tour_num tour_count - joint_tour_id + tour_id 3209530 320953 disc 1 1 1 2 3209531 320953 disc 2 2 2 2 23267026 2326702 shop 1 1 1 1 diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index b0a9ac806..cd86cef34 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -53,12 +53,12 @@ def workplace_location_sample(persons_merged, """ build a table of workers * all zones in order to select a sample of alternative work locations. - PERID, dest_TAZ, rand, pick_count - 23750, 14, 0.565502716034, 4 - 23750, 16, 0.711135838871, 6 + person_id, dest_TAZ, rand, pick_count + 23750, 14, 0.565502716034, 4 + 23750, 16, 0.711135838871, 6 ... - 23751, 12, 0.408038878552, 1 - 23751, 14, 0.972732479292, 2 + 23751, 12, 0.408038878552, 1 + 23751, 14, 0.972732479292, 2 """ trace_label = 'workplace_location_sample' @@ -121,19 +121,19 @@ def workplace_location_logsums(persons_merged, logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair in workplace_location_sample, and computing the logsum of all the utilities - +-------+--------------+----------------+------------+----------------+ - | PERID | dest_TAZ | rand | pick_count | logsum (added) | - +=======+==============+================+============+================+ - | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | - +-------+--------------+----------------+------------+----------------+ - + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | - +-------+--------------+----------------+------------+----------------+ - + ... | | | | | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | - +-------+--------------+----------------+------------+----------------+ - | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | - +-------+--------------+----------------+------------+----------------+ + +-----------+--------------+----------------+------------+----------------+ + | PERID | dest_TAZ | rand | pick_count | logsum (added) | + +===========+==============+================+============+================+ + | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | + +-----------+--------------+----------------+------------+----------------+ + + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | + +-----------+--------------+----------------+------------+----------------+ + + ... | | | | | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | + +-----------+--------------+----------------+------------+----------------+ """ trace_label = 'workplace_location_logsums' diff --git a/activitysim/abm/tables/__init__.py b/activitysim/abm/tables/__init__.py index 177e781b4..cf8bd5bb8 100644 --- a/activitysim/abm/tables/__init__.py +++ b/activitysim/abm/tables/__init__.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +import input_store + import households import persons import landuse @@ -11,4 +13,4 @@ import time_windows import constants -import random_channels +import table_dict diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index 245a1d87f..32b2dfbcd 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -12,16 +12,18 @@ from activitysim.core import inject +from input_store import read_input_table + logger = logging.getLogger(__name__) @inject.table() -def households(store, households_sample_size, trace_hh_id, override_hh_ids): +def households(households_sample_size, override_hh_ids, trace_hh_id): - df_full = store["households"] + df_full = read_input_table("households") # only using households listed in override_hh_ids - if override_hh_ids is not None: + if override_hh_ids: # trace_hh_id will not used if it is not in list of override_hh_ids logger.info("override household list containing %s households" % len(override_hh_ids)) @@ -41,27 +43,41 @@ def households(store, households_sample_size, trace_hh_id, override_hh_ids): # df contains only trace_hh (or empty if not in full store) df = tracing.slice_ids(df_full, trace_hh_id) + df = df[df.index.isin(ids)] + # if we need a subset of full store elif households_sample_size > 0 and df_full.shape[0] > households_sample_size: logger.info("sampling %s of %s households" % (households_sample_size, df_full.shape[0])) - # take the requested random sample - df = asim.random_rows(df_full, households_sample_size) + """ + Because random seed is set differently for each step, sampling of households using + Random.global_rng would sample differently depending upon which step it was called from. + We use a one-off rng seeded with the pseudo step name 'sample_households' to provide + repeatable sampling no matter when the table is loaded. + + Note that the external_rng is also seeded with base_seed so the sample will (rightly) change + if the pipeline rng's base_seed is changed + """ + + prng = pipeline.get_rn_generator().get_external_rng('sample_households') + df = df_full.take(prng.choice(len(df_full), size=households_sample_size, replace=False)) # if tracing and we missed trace_hh in sample, but it is in full store if trace_hh_id and trace_hh_id not in df.index and trace_hh_id in df_full.index: - # replace first hh in sample with trace_hh - logger.debug("replacing household %s with %s in household sample" % - (df.index[0], trace_hh_id)) - df_hh = tracing.slice_ids(df_full, trace_hh_id) - df = pd.concat([df_hh, df[1:]]) + # replace first hh in sample with trace_hh + logger.debug("replacing household %s with %s in household sample" % + (df.index[0], trace_hh_id)) + df_hh = df_full.loc[[trace_hh_id]] + df = pd.concat([df_hh, df[1:]]) else: df = df_full logger.info("loaded households %s" % (df.shape,)) + df.index.name = 'household_id' + # FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id assert 'chunk_id' not in df.columns df['chunk_id'] = pd.Series(range(len(df)), df.index) @@ -73,7 +89,7 @@ def households(store, households_sample_size, trace_hh_id, override_hh_ids): if trace_hh_id: tracing.register_traceable_table('households', df) - tracing.trace_df(df, "households", warn_if_empty=True) + tracing.trace_df(df, "raw.households", warn_if_empty=True) return df diff --git a/activitysim/abm/tables/input_store.py b/activitysim/abm/tables/input_store.py new file mode 100644 index 000000000..4a59aa125 --- /dev/null +++ b/activitysim/abm/tables/input_store.py @@ -0,0 +1,41 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import os +import warnings +import logging + +import pandas as pd + +from activitysim.core import inject +from activitysim.core.config import setting + +# FIXME +warnings.filterwarnings('ignore', category=pd.io.pytables.PerformanceWarning) +pd.options.mode.chained_assignment = None + +logger = logging.getLogger(__name__) + + +def read_input_table(table_name): + + input_store_path = inject.get_injectable("input_store_path", None) + + if not input_store_path: + + filename = setting('input_store', None) + + if not filename: + logger.error("input store file name not specified in settings") + raise RuntimeError("store file name not specified in settings") + + data_dir = inject.get_injectable("data_dir") + input_store_path = os.path.join(data_dir, filename) + + if not os.path.exists(input_store_path): + logger.error("store file not found: %s" % input_store_path) + raise RuntimeError("store file not found: %s" % input_store_path) + + df = pd.read_hdf(input_store_path, table_name) + + return df diff --git a/activitysim/abm/tables/landuse.py b/activitysim/abm/tables/landuse.py index 36001c999..d12e915db 100644 --- a/activitysim/abm/tables/landuse.py +++ b/activitysim/abm/tables/landuse.py @@ -4,17 +4,20 @@ import logging from activitysim.core import inject +from input_store import read_input_table logger = logging.getLogger(__name__) @inject.table() -def land_use(store): +def land_use(): - df = store["land_use/taz_data"] + df = read_input_table("land_use/taz_data") logger.info("loaded land_use %s" % (df.shape,)) + df.index.name = 'TAZ' + # replace table function with dataframe inject.add_table('land_use', df) diff --git a/activitysim/abm/tables/persons.py b/activitysim/abm/tables/persons.py index 8f3fffc4e..abec59f65 100644 --- a/activitysim/abm/tables/persons.py +++ b/activitysim/abm/tables/persons.py @@ -10,22 +10,35 @@ from activitysim.core import tracing from activitysim.core.util import other_than, reindex -from constants import * +from input_store import read_input_table logger = logging.getLogger(__name__) -@inject.table() -def persons(store, households_sample_size, households, trace_hh_id): +def read_raw_persons(households): + + # we only use these to know whether we need to slice persons + households_sample_size = inject.get_injectable('households_sample_size') + override_hh_ids = inject.get_injectable('override_hh_ids') - df = store["persons"] + df = read_input_table("persons") - if households_sample_size > 0: + if (households_sample_size > 0) or override_hh_ids: # keep all persons in the sampled households df = df[df.household_id.isin(households.index)] + return df + + +@inject.table() +def persons(households, trace_hh_id): + + df = read_raw_persons(households) + logger.info("loaded persons %s" % (df.shape,)) + df.index.name = 'person_id' + # replace table function with dataframe inject.add_table('persons', df) @@ -33,7 +46,7 @@ def persons(store, households_sample_size, households, trace_hh_id): if trace_hh_id: tracing.register_traceable_table('persons', df) - tracing.trace_df(df, "persons", warn_if_empty=True) + tracing.trace_df(df, "raw.persons", warn_if_empty=True) return df diff --git a/activitysim/abm/tables/random_channels.py b/activitysim/abm/tables/random_channels.py deleted file mode 100644 index b5c98b118..000000000 --- a/activitysim/abm/tables/random_channels.py +++ /dev/null @@ -1,41 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -import logging - -from activitysim.core import inject - - -logger = logging.getLogger(__name__) - -""" -We expect that the random number channel can be determined by the name of the index of the -dataframe accompanying the request. - -channel_info is a dict with keys and value of the form: - -:: - - : - - -channel_name: str - The channel name is just the table name used by the pipeline and inject. -table_index_name: str - name of the table index (so we can deduce the channel for a dataframe by index name) - -""" - -CHANNEL_INFO = { - 'households': 'HHID', - 'persons': 'PERID', - 'tours': 'tour_id', - 'joint_tour_participants': 'participant_id', - 'trips': 'trip_id', -} - - -@inject.injectable() -def channel_info(): - - return CHANNEL_INFO diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index d2e4f28d6..4aefa18b9 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -4,12 +4,14 @@ import os import logging -import openmatrix as omx +from collections import OrderedDict +import multiprocessing as mp + +import numpy as np -from activitysim.core import skim as askim -from activitysim.core import tracing -from activitysim.core import pipeline +import openmatrix as omx +from activitysim.core import skim from activitysim.core import inject logger = logging.getLogger(__name__) @@ -19,40 +21,90 @@ """ -# cache this so we don't open it again and again - skim code is not closing it.... -@inject.injectable(cache=True) -def omx_file(data_dir, settings): - logger.debug("opening omx file") +def skims_to_load(omx_file_path, tags_to_load=None): + + # select the skims to load + with omx.open_file(omx_file_path) as omx_file: + + omx_shape = omx_file.shape() + skim_keys = OrderedDict() + + for skim_name in omx_file.listMatrices(): + key1, sep, key2 = skim_name.partition('__') + + # ignore composite tags not in tags_to_load + if tags_to_load and sep and key2 not in tags_to_load: + continue + + key = (key1, key2) if sep else key1 + skim_keys[skim_name] = key + + num_skims = len(skim_keys.keys()) + + skim_data_shape = omx_shape + (num_skims, ) + skim_dtype = np.float32 + + return skim_keys, skim_data_shape, skim_dtype + - fname = os.path.join(data_dir, settings["skims_file"]) - file = omx.open_file(fname) +def shared_buffer_for_skims(skims_shape, skim_dtype, shared=False): - pipeline.close_on_exit(file, fname) + buffer_size = np.prod(skims_shape) - return file + if np.issubdtype(skim_dtype, np.float64): + typecode = 'd' + elif np.issubdtype(skim_dtype, np.float32): + typecode = 'f' + else: + raise RuntimeError("shared_buffer_for_skims unrecognized dtype %s" % skim_dtype) + + logger.info("allocating shared buffer of size %s (%s)" % (buffer_size, skims_shape, )) + + skim_buffer = mp.RawArray(typecode, buffer_size) + + return skim_buffer + + +def load_skims(omx_file_path, skim_keys, skim_data): + + # read skims into skim_data + with omx.open_file(omx_file_path) as omx_file: + n = 0 + for skim_name, key in skim_keys.iteritems(): + + omx_data = omx_file[skim_name] + assert np.issubdtype(omx_data.dtype, np.floating) + + # this will trigger omx readslice to read and copy data to skim_data's buffer + a = skim_data[:, :, n] + a[:] = omx_data[:] + n += 1 + + logger.info("load_skims loaded %s skims from %s" % (n, omx_file_path)) @inject.injectable(cache=True) -def skim_dict(omx_file, cache_skim_key_values): +def skim_dict(data_dir, settings): logger.info("skims injectable loading skims") - skim_dict = askim.SkimDict() - skim_dict.offset_mapper.set_offset_int(-1) + omx_file_path = os.path.join(data_dir, settings["skims_file"]) + tags_to_load = settings['skim_time_periods']['labels'] - skims_in_omx = omx_file.listMatrices() - for skim_name in skims_in_omx: - key, sep, key2 = skim_name.partition('__') - skim_data = omx_file[skim_name] - if not sep: - # no separator - this is a simple 2d skim - we load them all - skim_dict.set(key, skim_data) - else: - # there may be more time periods in the skim than are used by the model - # cache_skim_key_values is a list of time periods (frem settings) that are used - # FIXME - assumes that the only types of key2 are skim_time_periods - if key2 in cache_skim_key_values: - skim_dict.set((key, key2), skim_data) + # select the skims to load + skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + + skim_buffer = inject.get_injectable('skim_buffer', None) + if skim_buffer: + logger.info('Using existing skim_buffer for skims') + skim_data = np.frombuffer(skim_buffer, dtype=skim_dtype).reshape(skims_shape) + else: + skim_data = np.zeros(skims_shape, dtype=skim_dtype) + load_skims(omx_file_path, skim_keys, skim_data) + + # create skim dict + skim_dict = skim.SkimDict(skim_data, skim_keys.values()) + skim_dict.offset_mapper.set_offset_int(-1) return skim_dict @@ -61,4 +113,4 @@ def skim_dict(omx_file, cache_skim_key_values): def skim_stack(skim_dict): logger.debug("loading skim_stack") - return askim.SkimStack(skim_dict) + return skim.SkimStack(skim_dict) diff --git a/activitysim/abm/tables/table_dict.py b/activitysim/abm/tables/table_dict.py new file mode 100644 index 000000000..df7b35c43 --- /dev/null +++ b/activitysim/abm/tables/table_dict.py @@ -0,0 +1,33 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import logging + +from activitysim.core import inject + + +logger = logging.getLogger(__name__) + +""" +When the pipeline is restarted and tables are loaded, we need to know which ones +should be registered as random number generator channels. +""" + +RANDOM_CHANNELS = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips'] +TRACEABLE_TABLES = ['households', 'persons', 'tours', 'joint_tour_participants', 'trips'] + + +@inject.injectable() +def rng_channels(): + + # bug + return RANDOM_CHANNELS + + +@inject.injectable() +def traceable_tables(): + + # names of all traceable tables ordered by dependency on household_id + # e.g. 'persons' has to be registered AFTER 'households' + + return TRACEABLE_TABLES diff --git a/activitysim/abm/test/configs/initialize.yaml b/activitysim/abm/test/configs/initialize.yaml deleted file mode 100644 index addb7878a..000000000 --- a/activitysim/abm/test/configs/initialize.yaml +++ /dev/null @@ -1,43 +0,0 @@ - -#import_tables: -# - tablename: land_use -# column_map: -# #TOTHH: total_households -# #TOTEMP: total_employment -# #TOTACRE: total_acres -# COUNTY: county_id - -annotate_tables: - - tablename: land_use - column_map: - #TOTHH: total_households - #TOTEMP: total_employment - #TOTACRE: total_acres - COUNTY: county_id - annotate: - SPEC: annotate_landuse - DF: land_use - - tablename: persons - annotate: - SPEC: annotate_persons - DF: persons - TABLES: - - households - - tablename: households - column_map: - #TOTHH: total_households - #TOTEMP: total_employment - #TOTACRE: total_acres - PERSONS: hhsize - workers: num_workers -# hwork_f: num_workers_full -# hwork_p: num_workers_part -# huniv: num_univ -# hpresch: num_preschool -# hschdriv: num_driving_age_students - annotate: - SPEC: annotate_households - DF: households - TABLES: - - persons - - land_use diff --git a/activitysim/abm/test/configs/initialize_households.yaml b/activitysim/abm/test/configs/initialize_households.yaml new file mode 100644 index 000000000..572d2c565 --- /dev/null +++ b/activitysim/abm/test/configs/initialize_households.yaml @@ -0,0 +1,18 @@ + +annotate_tables: + - tablename: persons + annotate: + SPEC: annotate_persons + DF: persons + TABLES: + - households + - tablename: households + column_map: + PERSONS: hhsize + workers: num_workers + annotate: + SPEC: annotate_households + DF: households + TABLES: + - persons + - land_use diff --git a/activitysim/abm/test/configs/initialize_landuse.yaml b/activitysim/abm/test/configs/initialize_landuse.yaml new file mode 100644 index 000000000..21faccd0f --- /dev/null +++ b/activitysim/abm/test/configs/initialize_landuse.yaml @@ -0,0 +1,9 @@ + + +annotate_tables: + - tablename: land_use + column_map: + COUNTY: county_id + annotate: + SPEC: annotate_landuse + DF: land_use diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 7d96e27f8..8fd943ae6 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -1,4 +1,4 @@ -store: mtc_asim.h5 +input_store: mtc_asim.h5 skims_file: skims.omx # area_types less than this are considered urban diff --git a/activitysim/abm/test/test_misc.py b/activitysim/abm/test/test_misc.py index c454d8e25..573b3aa49 100644 --- a/activitysim/abm/test/test_misc.py +++ b/activitysim/abm/test/test_misc.py @@ -46,15 +46,5 @@ def test_misc(): data_dir = os.path.join(os.path.dirname(__file__), 'data') orca.add_injectable("data_dir", data_dir) - with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("store") - assert "store file name not specified in settings" in str(excinfo.value) - - settings = {'store': 'bogus.h5'} - orca.add_injectable("settings", settings) - with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("store") - assert "store file not found" in str(excinfo.value) - # default values if not specified in settings assert orca.get_injectable("chunk_size") == 0 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 6e799dd1c..64ecfe265 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -24,10 +24,10 @@ HOUSEHOLDS_SAMPLE_SIZE = 100 # household with mandatory, non mandatory, atwork_subtours, and joint tours -HH_ID = 1269102 +HH_ID = 1528374 # test households with all tour types -# [ 897044 1062044 1227638 1227783 1583265 2124082] +# [ 897097 921736 934309 1263203 1528374 1577292 1685008 1809710 2123173 2124138] SKIP_FULL_RUN = True @@ -84,20 +84,68 @@ def test_rng_access(): orca.clear_cache() - pipeline.set_rn_generator_base_seed(0) + inject.add_injectable('rng_base_seed', 0) pipeline.open_pipeline() - with pytest.raises(RuntimeError) as excinfo: - pipeline.set_rn_generator_base_seed(0) - assert "call set_rn_generator_base_seed before the first step" in str(excinfo.value) - rng = pipeline.get_rn_generator() pipeline.close_pipeline() orca.clear_cache() +def regress_mini_auto(): + + auto_choice = pipeline.get_table("households").auto_ownership + + # regression test: these are among the first 10 households in households table + hh_ids = [961042, 238146, 23730] + choices = [1, 1, 0] + expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), + name='auto_ownership') + + print "auto_choice\n", auto_choice.head(10) + """ + auto_choice + household_id + 961042 1 + 238146 1 + 701954 1 + 644046 1 + 23730 0 + 460343 1 + 25354 1 + 92615 1 + 1950284 0 + 2122448 0 + Name: auto_ownership, dtype: int64 + """ + pdt.assert_series_equal(auto_choice[hh_ids], expected_choice) + + +def regress_mini_mtf(): + + mtf_choice = pipeline.get_table("persons").mandatory_tour_frequency + + # these choices are for pure regression - their appropriateness has not been checked + per_ids = [26986, 92615, 93332] + choices = ['school1', 'work1', 'work1'] + expected_choice = pd.Series(choices, index=pd.Index(per_ids, name='person_id'), + name='mandatory_tour_frequency') + + print "mtf_choice\n", mtf_choice.dropna().head(5) + """ + mtf_choice + 26986 school1 + 92615 work1 + 93332 work1 + 93390 work1 + 93898 work1 + Name: mandatory_tour_frequency, dtype: object + """ + pdt.assert_series_equal(mtf_choice[per_ids], expected_choice) + + def test_mini_pipeline_run(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') @@ -118,8 +166,9 @@ def test_mini_pipeline_run(): # assert len(orca.get_table("households").index) == HOUSEHOLDS_SAMPLE_SIZE _MODELS = [ - 'initialize', + 'initialize_landuse', 'compute_accessibility', + 'initialize_households', 'school_location_sample', 'school_location_logsums', 'school_location_simulate', @@ -131,55 +180,12 @@ def test_mini_pipeline_run(): pipeline.run(models=_MODELS, resume_after=None) - auto_choice = pipeline.get_table("households").auto_ownership - - # regression test: these are among the first 10 households in households table - hh_ids = [464138, 1918238, 2201602] - choices = [1, 2, 0] - expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="HHID"), - name='auto_ownership') - - print "auto_choice\n", auto_choice.head(10) - pdt.assert_series_equal(auto_choice[hh_ids], expected_choice) + regress_mini_auto() pipeline.run_model('cdap_simulate') pipeline.run_model('mandatory_tour_frequency') - mtf_choice = pipeline.get_table("persons").mandatory_tour_frequency - - # these choices are for pure regression - their appropriateness has not been checked - per_ids = [92233, 172595, 524152] - choices = ['work1', 'school1', 'work_and_school'] - expected_choice = pd.Series(choices, index=pd.Index(per_ids, name='PERID'), - name='mandatory_tour_frequency') - - print "mtf_choice\n", mtf_choice.dropna().head(20) - """ - mtf_choice - PERID - 92233 work1 - 92382 work1 - 92744 work2 - 92823 work1 - 93172 work1 - 172491 work2 - 172595 school1 - 172596 school1 - 327171 work1 - 327172 work1 - 327912 work1 - 481948 school1 - 481949 school1 - 481959 work1 - 481961 work1 - 523907 work2 - 524151 school1 - 524152 work_and_school - 524153 school1 - 821808 work1 - Name: mandatory_tour_frequency, dtype: object - """ - pdt.assert_series_equal(mtf_choice[per_ids], expected_choice) + regress_mini_mtf() # try to get a non-existant table with pytest.raises(RuntimeError) as excinfo: @@ -221,20 +227,11 @@ def test_mini_pipeline_run2(): prev_checkpoint_count = len(checkpoints_df.index) # print "checkpoints_df\n", checkpoints_df[['checkpoint_name']] - assert prev_checkpoint_count == 11 + assert prev_checkpoint_count == 12 pipeline.open_pipeline('auto_ownership_simulate') - auto_choice = pipeline.get_table("households").auto_ownership - - # regression test: these are the same as in test_mini_pipeline_run1 - hh_ids = [464138, 1918238, 2201602] - choices = [1, 2, 0] - expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="HHID"), - name='auto_ownership') - - print "auto_choice\n", auto_choice.head(4) - pdt.assert_series_equal(auto_choice[hh_ids], expected_choice) + regress_mini_auto() # try to run a model already in pipeline with pytest.raises(RuntimeError) as excinfo: @@ -245,16 +242,7 @@ def test_mini_pipeline_run2(): pipeline.run_model('cdap_simulate') pipeline.run_model('mandatory_tour_frequency') - mtf_choice = pipeline.get_table("persons").mandatory_tour_frequency - - # this is what we got in run 1 - per_ids = [92233, 172595, 524152] - choices = ['work1', 'school1', 'work_and_school'] - expected_choice = pd.Series(choices, index=pd.Index(per_ids, name='PERID'), - name='mandatory_tour_frequency') - - print "mtf_choice\n", mtf_choice.head(20) - pdt.assert_series_equal(mtf_choice[per_ids], expected_choice) + regress_mini_mtf() # should be able to get this before pipeline is closed (from existing open store) checkpoints_df = pipeline.get_checkpoints() @@ -324,7 +312,7 @@ def get_trace_csv(file_name): return df -EXPECT_TOUR_COUNT = 186 +EXPECT_TOUR_COUNT = 158 def regress_tour_modes(tours_df): @@ -339,69 +327,48 @@ def regress_tour_modes(tours_df): """ tour_id tour_mode person_id tour_type tour_num tour_category tour_id - 84543800 SHARED2FREE 2915303 othmaint 1 joint - 84543820 WALK 2915304 eat 1 atwork - 84543843 WALK_LOC 2915304 work 1 mandatory - 84543823 DRIVEALONEFREE 2915304 escort 1 non_mandatory - 84543897 WALK_LOC 2915306 school 1 mandatory - 84543900 SHARED2FREE 2915306 social 1 non_mandatory - 84543926 WALK_LOC 2915307 school 1 mandatory - 84543910 SHARED2FREE 2915307 escort 1 non_mandatory - 84543911 SHARED2FREE 2915307 escort 2 non_mandatory - 84543929 SHARED2FREE 2915307 social 3 non_mandatory - 84543955 WALK_LOC 2915308 school 1 mandatory - 84543957 WALK 2915308 shopping 1 non_mandatory - 84543953 SHARED2FREE 2915308 othdiscr 2 non_mandatory - 84543938 WALK 2915308 eatout 3 non_mandatory + 96881402 WALK 3340738 business 1 atwork + 96881411 SHARED2FREE 3340738 eatout 1 joint + 96881429 WALK_LOC 3340738 work 1 mandatory + 96881427 WALK_LOC 3340738 shopping 1 non_mandatory + 96881454 WALK_LOC 3340739 school 1 mandatory + 96881456 DRIVEALONEFREE 3340739 shopping 1 non_mandatory + 96881437 SHARED2FREE 3340739 eatout 2 non_mandatory + 96881457 WALK_LOC 3340739 social 3 non_mandatory """ EXPECT_PERSON_IDS = [ - 2915303, - 2915304, - 2915304, - 2915304, - 2915306, - 2915306, - 2915307, - 2915307, - 2915307, - 2915307, - 2915308, - 2915308, - 2915308, - 2915308] + 3340738, + 3340738, + 3340738, + 3340738, + 3340739, + 3340739, + 3340739, + 3340739 + ] EXPECT_TOUR_TYPES = [ - 'othmaint', - 'eat', + 'business', + 'eatout', 'work', - 'escort', - 'school', - 'social', - 'school', - 'escort', - 'escort', - 'social', + 'shopping', 'school', 'shopping', - 'othdiscr', - 'eatout'] + 'eatout', + 'social' + ] EXPECT_MODES = [ - 'SHARED2FREE', 'WALK', - 'WALK_LOC', - 'DRIVEALONEFREE', - 'WALK_LOC', 'SHARED2FREE', 'WALK_LOC', - 'SHARED2FREE', - 'SHARED2FREE', - 'SHARED2FREE', 'WALK_LOC', - 'WALK', + 'WALK_LOC', + 'DRIVEALONEFREE', 'SHARED2FREE', - 'WALK'] + 'WALK_LOC' + ] assert (tours_df.person_id.values == EXPECT_PERSON_IDS).all() assert (tours_df.tour_type.values == EXPECT_TOUR_TYPES).all() diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 710a50316..32b0e6d54 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -77,7 +77,7 @@ def setting(key, default=None): return s -def read_model_settings(file_name): +def read_model_settings(file_name, mandatory=False): configs_dir = inject.get_injectable('configs_dir') @@ -90,6 +90,9 @@ def read_model_settings(file_name): if settings is None: settings = {} + if mandatory and not settings: + raise RuntimeError("Could not read settings from %s" % file_name) + return settings @@ -135,3 +138,40 @@ def get_logit_model_settings(model_settings): raise RuntimeError("No NEST found in model spec for NL model type") return nests + + +def build_output_file_path(file_name, use_prefix=None): + output_dir = inject.get_injectable('output_dir') + + if use_prefix: + file_name = "%s-%s" % (use_prefix, file_name) + + file_path = os.path.join(output_dir, file_name) + + return file_path + + +def output_file_path(file_name): + + prefix = inject.get_injectable('output_file_prefix', None) + return build_output_file_path(file_name, use_prefix=prefix) + + +def trace_file_path(file_name): + + output_dir = inject.get_injectable('output_dir') + file_name = "trace.%s" % (file_name, ) + file_path = os.path.join(output_dir, file_name) + return file_path + + +def log_file_path(file_name): + + prefix = inject.get_injectable('log_file_prefix', None) + return build_output_file_path(file_name, use_prefix=prefix) + + +def pipeline_file_path(file_name): + + prefix = inject.get_injectable('pipeline_file_prefix', None) + return build_output_file_path(file_name, use_prefix=prefix) diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index b009f80fb..b5bcfeb42 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -50,24 +50,6 @@ def decorator(func): return decorator -# def column(table_name, cache=False): -# def decorator(func): -# name = func.__name__ -# -# logger.debug("inject column %s.%s" % (table_name, name)) -# -# column_key = (table_name, name) -# -# assert not _DECORATED_COLUMNS.get(column_key, False), \ -# "column '%s' already decorated." % name -# _DECORATED_COLUMNS[column_key] = {'func': func, 'cache': cache} -# -# orca.add_column(table_name, name, func, cache=cache) -# -# return func -# return decorator - - def injectable(cache=False, override=False): def decorator(func): name = func.__name__ diff --git a/activitysim/core/inject_defaults.py b/activitysim/core/inject_defaults.py index 148f5c92d..b3b39209b 100644 --- a/activitysim/core/inject_defaults.py +++ b/activitysim/core/inject_defaults.py @@ -34,10 +34,8 @@ def output_dir(): @inject.injectable() -def extensions_dir(): - if not os.path.exists('extensions'): - raise RuntimeError("output_dir: directory does not exist") - return 'extensions' +def output_file_prefix(): + return '' @inject.injectable(cache=True) @@ -47,10 +45,15 @@ def settings(configs_dir): @inject.injectable(cache=True) -def pipeline_path(output_dir, settings): +def pipeline_file_name(settings): """ Orca injectable to return the path to the pipeline hdf5 file based on output_dir and settings """ pipeline_file_name = settings.get('pipeline', 'pipeline.h5') - pipeline_file_path = os.path.join(output_dir, pipeline_file_name) - return pipeline_file_path + + return pipeline_file_name + + +@inject.injectable() +def rng_base_seed(): + return 0 diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 72d73f040..6646e0207 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,15 +1,12 @@ import os -import time import datetime as dt -import cPickle - -import numpy as np import pandas as pd import orca import logging import inject +import config from util import memory_info from util import df_size @@ -23,8 +20,7 @@ # (which are also columns in the checkpoints dataframe stored in hte pipeline store) TIMESTAMP = 'timestamp' CHECKPOINT_NAME = 'checkpoint_name' -PRNG_STEP_NUM = 'prng_step_num' -NON_TABLE_COLUMNS = [CHECKPOINT_NAME, TIMESTAMP, PRNG_STEP_NUM] +NON_TABLE_COLUMNS = [CHECKPOINT_NAME, TIMESTAMP] # name used for storing the checkpoints dataframe to the pipeline store CHECKPOINT_TABLE_NAME = 'checkpoints' @@ -53,6 +49,8 @@ def init_state(self): self.pipeline_store = None + self.is_open = False + def rng(self): return self._rng @@ -61,6 +59,14 @@ def rng(self): _PIPELINE = Pipeline() +def pipeline_table_key(table_name, checkpoint_name): + if checkpoint_name: + key = "%s/%s" % (table_name, checkpoint_name) + else: + key = table_name + return key + + def close_on_exit(file, name): assert name not in _PIPELINE.open_files _PIPELINE.open_files[name] = file @@ -86,7 +92,7 @@ def open_pipeline_store(overwrite=False): if _PIPELINE.pipeline_store is not None: raise RuntimeError("Pipeline store is already open!") - pipeline_file_path = orca.get_injectable('pipeline_path') + pipeline_file_path = config.pipeline_file_path(orca.get_injectable('pipeline_file_name')) if overwrite: try: @@ -95,7 +101,7 @@ def open_pipeline_store(overwrite=False): os.unlink(pipeline_file_path) except Exception as e: print(e) - logger.warn("Error removing %s: %s" % (e,)) + logger.warn("Error removing %s: %s" % (pipeline_file_path, e)) _PIPELINE.pipeline_store = pd.HDFStore(pipeline_file_path, mode='a') @@ -117,36 +123,9 @@ def get_rn_generator(): ------- activitysim.random.Random """ - return _PIPELINE.rng() -def set_rn_generator_base_seed(seed): - """ - Like seed for numpy.random.RandomState, but generalized for use with all random streams. - - Provide a base seed that will be added to the seeds of all random streams. - The default base seed value is 0, so set_base_seed(0) is a NOP - - set_rn_generator_base_seed(1) will (e.g.) provide a different set of random streams - than the default, but will provide repeatable results re-running or resuming the simulation - - set_rn_generator_base_seed(None) will set the base seed to a random and unpredictable integer - and so provides "fully pseudo random" non-repeatable streams with different results every time - - Must be called before open_pipeline() or pipeline.run() - - Parameters - ---------- - seed : int or None - """ - - if _PIPELINE.last_checkpoint: - raise RuntimeError("Can only call set_rn_generator_base_seed before the first step.") - - _PIPELINE.rng().set_base_seed(seed) - - def read_df(table_name, checkpoint_name=None): """ Read a pandas dataframe from the pipeline store. @@ -170,13 +149,8 @@ def read_df(table_name, checkpoint_name=None): """ - if checkpoint_name: - key = "%s/%s" % (table_name, checkpoint_name) - else: - key = table_name - store = get_pipeline_store() - df = store[key] + df = store[pipeline_table_key(table_name, checkpoint_name)] return df @@ -203,14 +177,9 @@ def write_df(df, table_name, checkpoint_name=None): # coerce column names to str as unicode names will cause PyTables to pickle them df.columns = df.columns.astype(str) - if checkpoint_name: - key = "%s/%s" % (table_name, checkpoint_name) - else: - key = table_name - store = get_pipeline_store() - store[key] = df + store[pipeline_table_key(table_name, checkpoint_name)] = df def rewrap(table_name, df=None): @@ -305,13 +274,11 @@ def add_checkpoint(checkpoint_name): _PIPELINE.last_checkpoint[CHECKPOINT_NAME] = checkpoint_name _PIPELINE.last_checkpoint[TIMESTAMP] = timestamp - # current state of the random number generator - _PIPELINE.last_checkpoint[PRNG_STEP_NUM] = _PIPELINE.rng().step_num - # append to the array of checkpoint history _PIPELINE.checkpoints.append(_PIPELINE.last_checkpoint.copy()) # create a pandas dataframe of the checkpoint history, one row per checkpoint + checkpoints = pd.DataFrame(_PIPELINE.checkpoints) # convert empty values to str so PyTables doesn't pickle object types @@ -337,8 +304,6 @@ def checkpointed_tables(): return [name for name, checkpoint_name in _PIPELINE.last_checkpoint.iteritems() if checkpoint_name and name not in NON_TABLE_COLUMNS] - return [name for name in _PIPELINE.last_checkpoint.keys() if name not in NON_TABLE_COLUMNS] - def load_checkpoint(checkpoint_name): """ @@ -356,8 +321,10 @@ def load_checkpoint(checkpoint_name): checkpoints = read_df(CHECKPOINT_TABLE_NAME) + # '_' means load last checkpoint if checkpoint_name == '_': checkpoint_name = checkpoints[CHECKPOINT_NAME].iloc[-1] + logger.info("loading checkpoint '%s'" % checkpoint_name) try: # truncate rows after target checkpoint @@ -399,13 +366,19 @@ def load_checkpoint(checkpoint_name): loaded_tables[table_name] = df # register for tracing in order that tracing.register_traceable_table wants us to register them - for table_name in tracing.traceable_tables(): + traceable_tables = inject.get_injectable('traceable_tables', []) + for table_name in traceable_tables: if table_name in loaded_tables: tracing.register_traceable_table(table_name, loaded_tables[table_name]) - # set random state to pickled state at end of last checkpoint - logger.debug("resetting random state") - _PIPELINE.rng().load_channels(_PIPELINE.last_checkpoint[PRNG_STEP_NUM]) + # add tables of known rng channels + rng_channels = inject.get_injectable('rng_channels', []) + if rng_channels: + logger.debug("loading random channels %s" % rng_channels) + for table_name in rng_channels: + if table_name in loaded_tables: + logger.debug("adding channel %s" % (table_name,)) + _PIPELINE.rng().add_channel(loaded_tables[table_name], channel_name=table_name) def split_arg(s, sep, default=''): @@ -438,7 +411,7 @@ def run_model(model_name): model_name is assumed to be the name of a registered orca step """ - if not _PIPELINE.last_checkpoint: + if not _PIPELINE.is_open: raise RuntimeError("Pipeline not initialized! Did you call open_pipeline?") # can't run same model more than once @@ -494,11 +467,11 @@ def open_pipeline(resume_after=None): logger.info("open_pipeline...") - if orca.is_injectable('channel_info'): - channel_info = inject.get_injectable('channel_info', None) - if channel_info: - logger.info("initialize ran_generator channel_info") - get_rn_generator().set_channel_info(channel_info) + if _PIPELINE.is_open: + raise RuntimeError("Pipeline is already open!") + _PIPELINE.is_open = True + + get_rn_generator().set_base_seed(inject.get_injectable('rng_base_seed', 0)) if resume_after: # open existing pipeline @@ -522,6 +495,9 @@ def close_pipeline(): Close any known open files """ + if not _PIPELINE.is_open: + raise RuntimeError("Pipeline is not open!") + close_open_files() _PIPELINE.pipeline_store.close() @@ -550,8 +526,10 @@ def run(models, resume_after=None): model_name of checkpoint to load checkpoint and AFTER WHICH to resume model run """ - if resume_after and resume_after in models: - models = models[models.index(resume_after) + 1:] + if resume_after: + logger.info('resume_after %s' % resume_after) + if resume_after in models: + models = models[models.index(resume_after) + 1:] t0 = print_elapsed_time() @@ -565,6 +543,7 @@ def run(models, resume_after=None): t0 = print_elapsed_time() for model in models: + t1 = print_elapsed_time() run_model(model) t1 = print_elapsed_time("run_model %s" % model, t1) @@ -650,13 +629,13 @@ def get_checkpoints(): if store: df = store[CHECKPOINT_TABLE_NAME] else: - pipeline_file_path = orca.get_injectable('pipeline_path') + pipeline_file_path = config.pipeline_file_path(orca.get_injectable('pipeline_file_name')) df = pd.read_hdf(pipeline_file_path, CHECKPOINT_TABLE_NAME) # non-table columns first (column order in df is random because created from a dict) table_names = [name for name in df.columns.values if name not in NON_TABLE_COLUMNS] - df.index.name = 'step_num' + df = df[NON_TABLE_COLUMNS + table_names] return df diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 100e80ea9..c2bd84e39 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -14,11 +14,6 @@ # one more than 0xFFFFFFFF so we can wrap using: int64 % _MAX_SEED _MAX_SEED = (1 << 32) -# not arbitrary, as we count on incrementing step_num from NULL_STEP_NUM to 0 -NULL_STEP_NUM = -1 - -MAX_STEPS = 100 - class SimpleChannel(object): """ @@ -38,91 +33,96 @@ class SimpleChannel(object): speed matters because we reseed on-the-fly for every call because creating a different RandomState object for each row uses too much memory (5K per RandomState object) - So instead, multiply the domain_df index by the number of steps required for the channel - add the step_num to the row_seed to get a unique seed for each (domain_df index, step_num) - tuple. - - Currently, it is possible that random streams for rows in different tables may coincide. - This would be easy to avoid with either seed arrays or fast jump/offset. - numpy random seeds are unsigned int32 so there are 4,294,967,295 available seeds. That is probably just about enough to distribute evenly, for most cities, depending on the number of households, persons, tours, trips, and steps. + So we use (global_seed + channel_seed + step_seed + row_index) % (1 << 32) + to get an int32 seed rather than a tuple. + We do read in the whole households and persons tables at start time, so we could note the max index values. But we might then want a way to ensure stability between the test, example, and full datasets. I am punting on this for now. """ - def __init__(self, channel_name, base_seed, domain_df, step_num): + def __init__(self, channel_name, base_seed, domain_df, step_name): - self.name = channel_name self.base_seed = base_seed # ensure that every channel is different, even for the same df index values and max_steps - self.unique_channel_seed = hash(self.name) % _MAX_SEED - - assert (step_num == NULL_STEP_NUM) or step_num >= 0 - self.step_num = step_num + self.channel_name = channel_name + self.channel_seed = hash(self.channel_name) % _MAX_SEED - assert (self.step_num < MAX_STEPS) + self.step_name = None + self.step_seed = None + self.row_states = None # create dataframe to hold state for every df row - self.row_states = self.create_row_states_for_domain(domain_df) + self.extend_domain(domain_df) + assert self.row_states.shape[0] == domain_df.shape[0] + + if step_name: + self.begin_step(step_name) - def create_row_states_for_domain(self, domain_df): + def init_row_states_for_step(self, row_states): """ - Create a dataframe with same index as domain_df and a single column + initialize row states (in place) for new step + with stable, predictable, repeatable row_seeds for that domain_df index value See notes on the seed generation strategy in class comment above. Parameters ---------- - domain_df : pandas.dataframe - domain dataframe with index values for which random streams are to be generated - - Returns - ------- - row_states : pandas.DataFrame + row_states """ - # dataframe to hold state for every df row - row_states = pd.DataFrame(columns=['row_seed', 'offset'], index=domain_df.index) + assert self.step_name - if not row_states.empty: - row_states['row_seed'] = (self.base_seed + self.unique_channel_seed + - row_states.index * MAX_STEPS) % _MAX_SEED + if self.step_name and not row_states.empty: + + row_states['row_seed'] = (self.base_seed + + self.channel_seed + + self.step_seed + + row_states.index) % _MAX_SEED + + # number of rands pulled this step row_states['offset'] = 0 return row_states def extend_domain(self, domain_df): """ - Extend existing row_state df by adding seed info for each row in domain_df + Extend or create row_state df by adding seed info for each row in domain_df - It is assumed that the index values of the component tables are disjoint and - there will be no ambiguity/collisions between them + If extending, the index values of new tables must be disjoint so + there will be no ambiguity/collisions between rows Parameters ---------- domain_df : pandas.DataFrame domain dataframe with index values for which random streams are to be generated and well-known index name corresponding to the channel + """ - step_name : str or None - provided when reloading so we can restore step_name and step_num + if domain_df.empty: + logger.warn("extend_domain for channel %s for empty domain_df" % self.channel_name) - step_num : int or None - """ + # dataframe to hold state for every df row + row_states = pd.DataFrame(columns=['row_seed', 'offset'], index=domain_df.index) - # these should be new rows, no intersection with existing row_states - assert len(self.row_states.index.intersection(domain_df.index)) == 0 + if self.step_name and not row_states.empty: + self.init_row_states_for_step(row_states) - new_row_states = self.create_row_states_for_domain(domain_df) - self.row_states = pd.concat([self.row_states, new_row_states]) + if self.row_states is None: + self.row_states = row_states + else: + # row_states already exists, so we are extending + # if extending, these should be new rows, no intersection with existing row_states + assert len(self.row_states.index.intersection(domain_df.index)) == 0 + self.row_states = pd.concat([self.row_states, row_states]) - def begin_step(self, step_num): + def begin_step(self, step_name): """ Reset channel state for a new state @@ -132,18 +132,25 @@ def begin_step(self, step_num): pipeline step name for this step """ - self.step_num = step_num + assert self.step_name is None - if self.step_num >= MAX_STEPS: - raise RuntimeError("Too many steps: %s (max %s) for channel '%s'" - % (self.step_num, MAX_STEPS, self.name)) + self.step_name = step_name + self.step_seed = hash(self.step_name) % _MAX_SEED - # number of rands pulled this step - self.row_states['offset'] = 0 + self.init_row_states_for_step(self.row_states) # standard constant to use for choice_for_df instead of fast-forwarding rand stream self.multi_choice_offset = None + def end_step(self, step_name): + + assert self.step_name == step_name + + self.step_name = None + self.step_seed = None + self.row_states['offset'] = 0 + self.row_states['row_seed'] = 0 + def _generators_for_df(self, df): """ Python generator function for iterating over numpy prngs (nomenclature collision!) @@ -165,8 +172,7 @@ def _generators_for_df(self, df): prng = np.random.RandomState() for row in df_row_states.itertuples(): - seed = (row.row_seed + self.step_num) % _MAX_SEED - prng.seed(seed) + prng.seed(row.row_seed) if row.offset: # consume rands @@ -203,6 +209,10 @@ def random_for_df(self, df, step_name, n=1): rands : 2-D ndarray array the same length as df, with n floats in range [0, 1) for each df row """ + + assert self.step_name + assert self.step_name == step_name + generators = self._generators_for_df(df) rands = np.asanyarray([prng.rand(n) for prng in generators]) # update offset for rows we handled @@ -245,6 +255,9 @@ def choice_for_df(self, df, step_name, a, size, replace): The generated random samples for each row concatenated into a single (flat) array """ + assert self.step_name + assert self.step_name == step_name + # initialize the generator iterator generators = self._generators_for_df(df) @@ -264,48 +277,16 @@ class Random(object): def __init__(self): - # dict mapping df index_name to channel (table) name - self.index_to_channel_map = {} - self.channels = {} + + # dict mapping df index name to channel name + self.index_to_channel = {} + self.step_name = None - self.step_num = NULL_STEP_NUM self.step_seed = None self.base_seed = 0 self.global_rng = np.random.RandomState() - def set_channel_info(self, channel_info): - - assert len(self.channels) == 0 - assert len(self.index_to_channel_map) == 0 - - # for mapping index name to channel name - self.index_to_channel_map = \ - {index_name: channel_name for channel_name, index_name in channel_info.iteritems()} - - def get_channel_name_for_df(self, df): - """ - Return the channel name corresponding to the index name of df - - We expect that the random number channel can be determined by the name of the index of the - dataframe accompanying the request. This mapping was specified in channel_info - - This function internally encapsulates the knowledge of that mapping. - - Parameters - ---------- - df : pandas.DataFrame - domain_df or a df passed to random number/choice methods with well known index name - - Returns - ------- - channel_name : str - """ - channel_name = self.index_to_channel_map.get(df.index.name, None) - if channel_name is None: - raise RuntimeError("No channel with index name '%s'" % df.index.name) - return channel_name - def get_channel_for_df(self, df): """ Return the channel for this df. Channel should already have been loaded/added. @@ -316,9 +297,10 @@ def get_channel_for_df(self, df): either a domain_df for a channel being added or extended or a df for which random values are to be generated """ - channel_name = self.get_channel_name_for_df(df) - if channel_name not in self.channels: - raise RuntimeError("Channel '%s' has not yet been added." % channel_name) + + channel_name = self.index_to_channel.get(df.index.name, None) + if channel_name is None: + raise RuntimeError("No channel with index name '%s'" % df.index.name) return self.channels[channel_name] # step handling @@ -336,10 +318,8 @@ def begin_step(self, step_name): assert self.step_name is None assert step_name is not None - assert step_name != self.step_name self.step_name = step_name - self.step_num += 1 self.step_seed = hash(step_name) % _MAX_SEED @@ -347,7 +327,7 @@ def begin_step(self, step_name): self.global_rng = np.random.RandomState(seed) for c in self.channels: - self.channels[c].begin_step(self.step_num) + self.channels[c].begin_step(self.step_name) def end_step(self, step_name): """ @@ -362,6 +342,9 @@ def end_step(self, step_name): assert self.step_name is not None assert self.step_name == step_name + for c in self.channels: + self.channels[c].end_step(self.step_name) + self.step_name = None self.step_seed = None self.global_rng = None @@ -385,18 +368,11 @@ def add_channel(self, domain_df, channel_name): channel_name : str expected channel name provided as a consistency check - step_name : str or None - for channels being loaded (resumed) we need the step_name and step_num to maintain - consistent step numbering - - step_num : int or NULL_STEP_NUM - for channels being loaded (resumed) we need the step_name and step_num to maintain - consistent step numbering """ - assert channel_name == self.get_channel_name_for_df(domain_df) - if channel_name in self.channels: + + assert channel_name == self.index_to_channel[domain_df.index.name] logger.debug("Random: extending channel '%s' %s ids" % (channel_name, len(domain_df.index))) channel = self.channels[channel_name] @@ -404,44 +380,31 @@ def add_channel(self, domain_df, channel_name): channel.extend_domain(domain_df) else: - logger.debug("Random: adding channel '%s' %s ids" % - (channel_name, len(domain_df.index))) + logger.debug("Adding channel '%s' %s ids" % (channel_name, len(domain_df.index))) channel = SimpleChannel(channel_name, self.base_seed, domain_df, - self.step_num + self.step_name ) self.channels[channel_name] = channel + self.index_to_channel[domain_df.index.name] = channel_name - def load_channels(self, step_num): + def drop_channel(self, channel_name): """ - Called on resume to initialize channels for existing saved tables - All channels should have been registered by a call to set_channel_info. - This method checks to see if any of them have an injectable table + Drop channel that won't be used again (saves memory) Parameters ---------- - step_num - - Returns - ------- - + channel_name """ - self.step_num = step_num - for index_name in self.index_to_channel_map: - - channel_name = self.index_to_channel_map[index_name] - df = inject.get_table(channel_name, None) - - if df is not None: - logger.debug("loading channel %s" % (channel_name,)) - self.add_channel(df, channel_name=channel_name) - self.channels[channel_name].begin_step(step_num) - else: - logger.debug("skipping channel %s" % (channel_name,)) + if channel_name in self.channels: + logger.debug("Dropping channel '%s'" % (channel_name, )) + del self.channels[channel_name] + else: + logger.error("drop_channel called with unknown channel '%s'" % (channel_name,)) def set_base_seed(self, seed=None): """ @@ -497,6 +460,16 @@ def get_global_rng(self): assert self.step_name is not None return self.global_rng + def get_external_rng(self, one_off_step_name): + """ + Return a numpy random number generator for step-independent one_off use + + exists to allow sampling of input tables consistent no matter what step they are called in + """ + + seed = [self.base_seed, hash(one_off_step_name) % _MAX_SEED] + return np.random.RandomState(seed) + def random_for_df(self, df, n=1): """ Return a single floating point random number in range [0, 1) for each row in df diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 4ea4e25f1..5cb4fe10c 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -1,8 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. + import logging +from collections import OrderedDict + import numpy as np import pandas as pd @@ -137,39 +140,18 @@ class SkimDict(object): Note that keys are either strings or tuples of two strings (to support stacking of skims.) """ - def __init__(self): - self.skims = {} - self.offset_mapper = OffsetMapper() - - def set(self, key, skim_data): - """ - Set skim data for key - - Parameters - ---------- - key : hashable - The key (identifier) for this skim object - skim_data : Skim - The skim object - - Returns - ------- - Nothing - """ - - if not isinstance(key, str): - assert isinstance(key, tuple) and len(key) == 2 - assert isinstance(key[0], str) and isinstance(key[1], str) + def __init__(self, skim_data, skim_keys): - self.skims[key] = np.asanyarray(skim_data) + skim_key_to_skim_num = OrderedDict(zip(skim_keys, range(len(skim_keys)))) - # print "\n### %s" % (key,) - # print "type(skim_data)", type(skim_data) - # print "skim_data.shape", skim_data.shape + self.skim_data = skim_data + self.skim_key_to_skim_num = skim_key_to_skim_num + self.num_skims = skim_data.shape[2] + self.offset_mapper = OffsetMapper() def get(self, key): """ - Get an available skim object (not the lookup) + Get an available wrapped skim object (not the lookup) Parameters ---------- @@ -181,7 +163,13 @@ def get(self, key): skim: Skim The skim object """ - return SkimWrapper(self.skims[key], self.offset_mapper) + assert key in self.skim_key_to_skim_num + n = self.skim_key_to_skim_num[key] + assert n < self.num_skims + + data = self.skim_data[:, :, n] + + return SkimWrapper(data, self.offset_mapper) def wrap(self, left_key, right_key): """ @@ -318,62 +306,35 @@ class SkimStack(object): def __init__(self, skim_dict): - self.skims_data = {} - self.skim_keys_to_indexes = {} self.offset_mapper = skim_dict.offset_mapper + self.skim_dict = skim_dict + + skim_dim3 = OrderedDict() + for key, n in skim_dict.skim_key_to_skim_num.iteritems(): + if isinstance(key, tuple): + key1, key2 = key + skim_dim3.setdefault(key1, OrderedDict())[key2] = n - # pass to make dictionary of dictionaries where highest level is unique - # first items of the tuples and the 2nd level is the second items of - # the tuples - for key, skim_data in skim_dict.skims.iteritems(): - if not isinstance(key, tuple) or not len(key) == 2: - logger.debug("SkimStack __init__ skipping key: %s" % key) - continue - logger.debug("SkimStack __init__ loading key: %s" % (key,)) - skim_key1, skim_key2 = key - # logger.debug("SkimStack init key: key1='%s' key2='%s'" % (skim_key1, skim_key2)) - # FIXME - this copys object reference - self.skims_data.setdefault(skim_key1, {})[skim_key2] = skim_data - - # print "\n### %s" % (key,) - # print "type(skim_data)", type(skim_data) - # print "skim_data.shape", skim_data.shape - - # second pass to turn the each highest level value into a 3D array - # with a dictionary to make second level keys to indexes - for skim_key1, value in self.skims_data.iteritems(): - # FIXME - this actually copies/creates new stacked data - self.skims_data[skim_key1] = np.dstack(value.values()) - self.skim_keys_to_indexes[skim_key1] = dict(zip(value.keys(), range(len(value)))) + self.skim_dim3 = skim_dim3 logger.info("SkimStack.__init__ loaded %s keys with %s total skims" - % (len(self.skim_keys_to_indexes), - sum([len(d) for d in self.skim_keys_to_indexes.values()]))) + % (len(self.skim_dim3), + sum([len(d) for d in self.skim_dim3.values()]))) def __str__(self): return "\n".join( "%s %s" % (key1, sub_dict) - for key1, sub_dict in self.skim_keys_to_indexes.iteritems()) - - # def key_count(self): - # return len(self.skim_keys_to_indexes.keys()) - # - # def contains(self, key): - # return key in self.skims_data - - def get(self, key): - return self.skims_data[key], self.skim_keys_to_indexes[key] + for key1, sub_dict in self.skim_dim3.iteritems()) def lookup(self, orig, dest, dim3, key): orig = self.offset_mapper.map(orig) dest = self.offset_mapper.map(dest) - assert key in self.skims_data, "SkimStack key %s missing" % key - - stacked_skim_data = self.skims_data[key] - skim_keys_to_indexes = self.skim_keys_to_indexes[key] + assert key in self.skim_dim3, "SkimStack key %s missing" % key + stacked_skim_data = self.skim_dict.skim_data + skim_keys_to_indexes = self.skim_dim3[key] # skim_indexes = dim3.map(skim_keys_to_indexes).astype('int') # this should be faster than map diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index c19f3a09d..616c62135 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -7,6 +7,7 @@ from activitysim.core import pipeline from activitysim.core import inject +from activitysim.core import config from activitysim.core.config import setting @@ -28,16 +29,15 @@ def write_data_dictionary(output_dir): output_tables = pipeline.checkpointed_tables() # write data dictionary for all checkpointed_tables - with open(os.path.join(output_dir, 'data_dict.txt'), 'w') as file: + + with open(config.output_file_path('data_dict.txt'), 'w') as file: for table_name in output_tables: df = inject.get_table(table_name, None).to_frame() print >> file, "\n### %s %s" % (table_name, df.shape) + print >> file, 'index:', df.index.name, df.index.dtype print >> file, df.dtypes - rows, columns = df.shape - bytes = df.memory_usage(index=True).sum() - def write_tables(output_dir): """ @@ -76,8 +76,6 @@ def write_tables(output_dir): output_tables_settings = setting(output_tables_settings_name) - output_tables_list = pipeline.checkpointed_tables() - if output_tables_settings is None: logger.info("No output_tables specified in settings file. Nothing to write.") return @@ -90,32 +88,26 @@ def write_tables(output_dir): raise "expected %s action '%s' to be either 'include' or 'skip'" % \ (output_tables_settings_name, action) + checkpointed_tables = pipeline.checkpointed_tables() if action == 'include': output_tables_list = tables elif action == 'skip': - output_tables_list = [t for t in output_tables_list if t not in tables] - - # should provide option to also write checkpoints? - # output_tables_list.append("checkpoints.csv") + output_tables_list = [t for t in checkpointed_tables if t not in tables] for table_name in output_tables_list: - table = inject.get_table(table_name, None) - if table is None: - logger.warn("Skipping '%s': Table not found." % table_name) - continue + if table_name == 'checkpoints': + df = pipeline.get_checkpoints() + else: + if table_name not in checkpointed_tables: + logger.warn("Skipping '%s': Table not found." % table_name) + continue + df = pipeline.get_table(table_name) - df = table.to_frame() file_name = "%s%s.csv" % (prefix, table_name) - logger.info("writing output file %s" % file_name) - file_path = os.path.join(output_dir, file_name) + file_path = config.output_file_path(file_name) # include the index if it has a name or is a MultiIndex write_index = df.index.name is not None or isinstance(df.index, pd.core.index.MultiIndex) df.to_csv(file_path, index=write_index) - - if (action == 'include') == ('checkpoints' in tables): - # write checkpoints - file_name = "%s%s.csv" % (prefix, 'checkpoints') - pipeline.get_checkpoints().to_csv(os.path.join(output_dir, file_name)) diff --git a/activitysim/core/test/configs/custom_logging.yaml b/activitysim/core/test/configs/custom_logging.yaml index 5ab887121..92335d307 100644 --- a/activitysim/core/test/configs/custom_logging.yaml +++ b/activitysim/core/test/configs/custom_logging.yaml @@ -28,7 +28,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.tracing.log_file_path ['xasim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['xasim.log'] mode: w formatter: simpleFormatter level: !!python/name:logging.NOTSET diff --git a/activitysim/core/test/configs/logging.yaml b/activitysim/core/test/configs/logging.yaml index 28f5a159a..5e78d3824 100644 --- a/activitysim/core/test/configs/logging.yaml +++ b/activitysim/core/test/configs/logging.yaml @@ -28,7 +28,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.tracing.log_file_path ['activitysim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] mode: w formatter: simpleFormatter level: !!python/name:logging.NOTSET diff --git a/activitysim/core/test/extensions/steps.py b/activitysim/core/test/extensions/steps.py index 7a76abd29..00341685d 100644 --- a/activitysim/core/test/extensions/steps.py +++ b/activitysim/core/test/extensions/steps.py @@ -58,7 +58,7 @@ def step_forget_tab(): @inject.step() def create_households(trace_hh_id): - df = pd.DataFrame({'HHID': [1, 2, 3], 'TAZ': {100, 100, 101}}) + df = pd.DataFrame({'household_id': [1, 2, 3], 'TAZ': {100, 100, 101}}) inject.add_table('households', df) pipeline.get_rn_generator().add_channel(df, 'households') diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index cca1a53da..c9a5af3e7 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -78,7 +78,7 @@ def test_utils_to_probs_raises(): add_canonical_dirs() - idx = pd.Index(name='HHID', data=[1]) + idx = pd.Index(name='household_id', data=[1]) with pytest.raises(RuntimeError) as excinfo: logit.utils_to_probs(pd.DataFrame([[1, 2, np.inf, 3]], index=idx), trace_label=None) assert "infinite exponentiated utilities" in str(excinfo.value) diff --git a/activitysim/core/test/test_random.py b/activitysim/core/test/test_random.py index c1369bf65..0ef475d64 100644 --- a/activitysim/core/test/test_random.py +++ b/activitysim/core/test/test_random.py @@ -37,21 +37,20 @@ def test_basic(): def test_channel(): channels = { - 'households': 'HHID', - 'persons': 'PERID', + 'households': 'household_id', + 'persons': 'person_id', } rng = random.Random() - rng.set_channel_info(channels) persons = pd.DataFrame({ "household_id": [1, 1, 2, 2, 2], }, index=[1, 2, 3, 4, 5]) - persons.index.name = 'PERID' + persons.index.name = 'person_id' households = pd.DataFrame({ "data": [1, 1, 2, 2, 2], }, index=[1, 2, 3, 4, 5]) - households.index.name = 'HHID' + households.index.name = 'household_id' rng.begin_step('test_step') @@ -60,30 +59,32 @@ def test_channel(): rands = rng.random_for_df(persons) + print "rands", np.asanyarray(rands).flatten() + assert rands.shape == (5, 1) - expected_rands = [0.0305274, 0.6452407, 0.1686045, 0.9529088, 0.1994755] - npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) + test1_expected_rands = [0.9060891, 0.4576382, 0.2154094, 0.2801035, 0.6196645] + npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands) # second call should return something different rands = rng.random_for_df(persons) - expected_rands = [0.9912599, 0.5523497, 0.4580549, 0.3668453, 0.134653] - npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) + test1_expected_rands2 = [0.5991157, 0.5516594, 0.5529548, 0.3586653, 0.5844314] + npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands2) rng.end_step('test_step') rng.begin_step('test_step2') rands = rng.random_for_df(households) - expected_rands = [0.7992435, 0.5682545, 0.8956348, 0.6326098, 0.630408] + expected_rands = [0.7970902, 0.2633469, 0.7662205, 0.7544782, 0.129741] npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) choices = rng.choice_for_df(households, [1, 2, 3, 4], 2, replace=True) - expected_choices = [1, 3, 3, 2, 1, 1, 1, 3, 1, 3] + expected_choices = [2, 1, 3, 1, 3, 1, 3, 4, 4, 2] npt.assert_almost_equal(choices, expected_choices) # should be DIFFERENT the second time choices = rng.choice_for_df(households, [1, 2, 3, 4], 2, replace=True) - expected_choices = [2, 3, 3, 3, 2, 2, 2, 2, 2, 4] + expected_choices = [1, 1, 3, 2, 3, 2, 2, 3, 2, 3] npt.assert_almost_equal(choices, expected_choices) rng.end_step('test_step2') @@ -92,9 +93,22 @@ def test_channel(): rands = rng.random_for_df(households, n=2) - expected_rands = [0.4633051, 0.4924085, 0.8627697, 0.854059, 0.0689231, - 0.3818341, 0.0301041, 0.7765588, 0.2082694, 0.4542789] + expected_rands = [0.8635927, 0.3258157, 0.7970902, 0.365523, 0.2633469, 0.5388047, + 0.7662205, 0.8067344, 0.7544782, 0.024577] npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) rng.end_step('test_step3') + + # if we use the same step name a second time, we should get the same results as before + rng.begin_step('test_step') + + rands = rng.random_for_df(persons) + + print "rands", np.asanyarray(rands).flatten() + npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands) + + rands = rng.random_for_df(persons) + npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands2) + + rng.end_step('test_step') diff --git a/activitysim/core/test/test_skim.py b/activitysim/core/test/test_skim.py index bc55038bd..4de21927b 100644 --- a/activitysim/core/test/test_skim.py +++ b/activitysim/core/test/test_skim.py @@ -71,10 +71,13 @@ def test_skim_nans(data): def test_skims(data): - skim_dict = skim.SkimDict() + skims_shape = data.shape + (2,) - skim_dict.set('AM', data) - skim_dict.set('PM', data*10) + skim_data = np.zeros(skims_shape, dtype=data.dtype) + skim_data[:, :, 0] = data + skim_data[:, :, 1] = data*10 + + skim_dict = skim.SkimDict(skim_data, ['AM', 'PM']) skims = skim_dict.wrap("taz_l", "taz_r") @@ -104,10 +107,13 @@ def test_skims(data): def test_3dskims(data): - skim_dict = skim.SkimDict() + skims_shape = data.shape + (2,) + + skim_data = np.zeros(skims_shape, dtype=int) + skim_data[:, :, 0] = data + skim_data[:, :, 1] = data*10 - skim_dict.set(("SOV", "AM"), data) - skim_dict.set(("SOV", "PM"), data*10) + skim_dict = skim.SkimDict(skim_data, [("SOV", "AM"), ("SOV", "PM")]) stack = skim.SkimStack(skim_dict) diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index 7b9b8490c..49c7df18f 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -24,6 +24,8 @@ def close_handlers(): def add_canonical_dirs(): + orca.clear_cache() + configs_dir = os.path.join(os.path.dirname(__file__), 'configs') orca.add_injectable("configs_dir", configs_dir) @@ -158,19 +160,23 @@ def test_register_households(capsys): df = pd.DataFrame({'zort': ['a', 'b', 'c']}, index=[1, 2, 3]) - tracing.register_households(df, 5) + orca.add_injectable('traceable_tables', ['households']) + orca.add_injectable("trace_hh_id", 5) + tracing.register_traceable_table('households', df) out, err = capsys.readouterr() + # print out # don't consume output - # don't consume output - print out + assert "Can't register table 'households' without index name" in out + + df.index.name = 'household_id' + tracing.register_traceable_table('households', df) + out, err = capsys.readouterr() + # print out # don't consume output # should warn that household id not in index assert 'trace_hh_id 5 not in dataframe' in out - # should warn and rename index if index name is None - assert "households table index had no name. renamed index 'household_id'" in out - close_handlers() @@ -180,43 +186,42 @@ def test_register_tours(capsys): tracing.config_logger() + orca.add_injectable('traceable_tables', ['households', 'tours']) + orca.add_injectable('traceable_table_refs', None) + # in case another test injected this - orca.add_injectable("trace_person_ids", []) + orca.add_injectable("trace_tours", []) - df = pd.DataFrame({'zort': ['a', 'b', 'c']}, index=[1, 2, 3]) + tours_df = pd.DataFrame({'zort': ['a', 'b', 'c']}, index=[10, 11, 12]) + tours_df.index.name = 'tour_id' - tracing.register_tours(df, 5) + tracing.register_traceable_table('tours', tours_df) out, err = capsys.readouterr() + # print out # don't consume output - # don't consume output - print out - - assert "no person ids registered for trace_hh_id 5" in out - - close_handlers() + assert "can't find a registered table to slice table 'tours' index name 'tour_id'" in out + orca.add_injectable("trace_hh_id", 3) + households_df = pd.DataFrame({'dzing': ['a', 'b', 'c']}, index=[1, 2, 3]) + households_df.index.name = 'household_id' + tracing.register_traceable_table('households', households_df) -def test_register_persons(capsys): + tracing.register_traceable_table('tours', tours_df) - add_canonical_dirs() - - tracing.config_logger() + out, err = capsys.readouterr() + # print out # don't consume output + assert "can't find a registered table to slice table 'tours'" in out - df = pd.DataFrame({'household_id': [1, 2, 3]}, index=[11, 12, 13]) + tours_df['household_id'] = [1, 5, 3] - tracing.register_persons(df, 5) + tracing.register_traceable_table('tours', tours_df) out, err = capsys.readouterr() + print out # don't consume output - # don't consume output - print out - - # should warn that household id not in index - assert 'trace_hh_id 5 not found' in out - - # should warn and rename index if index name is None - assert "persons table index had no name. renamed index 'person_id'" in out + # should be tracing tour with tour_id 3 + assert orca.get_injectable('trace_tours') == [12] close_handlers() @@ -232,8 +237,7 @@ def test_write_csv(capsys): out, err = capsys.readouterr() - # don't consume output - print out + print out # don't consume output assert "write_df_csv object 'baddie' of unexpected type" in out diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index f69c6134f..00265f8a5 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -132,7 +132,7 @@ def create_timetable_windows(rows, tdd_alts): so if start is 5 and end is 23, we return something like this: 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 - PERID + person_id 30 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 109 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -233,11 +233,10 @@ def slice_windows_by_row_id_and_period(self, window_row_ids, periods): def get_windows_df(self): # It appears that assignments into windows write through to underlying pandas table. - # Because we set windows = windows_df.as_matrix, though as_matrix does not - # document this feature. - + # because we set windows = windows_df.values, and since all the columns are the same type # so no need to refresh pandas dataframe, but if we had to it would go here + # assert (self.windows_df.values == self.windows).all() return self.windows_df def replace_table(self): diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index e57bf4036..d0f4cb7fa 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -6,7 +6,7 @@ import logging.config import sys import time -from contextlib import contextmanager +from collections import OrderedDict import yaml @@ -16,6 +16,7 @@ from activitysim.core import inject import inject_defaults +import config # Configurations @@ -49,9 +50,9 @@ def print_elapsed_time(msg=None, t0=None, debug=False): return t1 -def delete_csv_files(output_dir): +def delete_output_files(file_type): """ - Delete CSV files + Delete files in output directory of specified type Parameters ---------- @@ -62,8 +63,12 @@ def delete_csv_files(output_dir): ------- Nothing """ + + output_dir = inject.get_injectable('output_dir') + logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) + for the_file in os.listdir(output_dir): - if the_file.endswith(CSV_FILE_TYPE): + if the_file.endswith(file_type): file_path = os.path.join(output_dir, the_file) try: if os.path.isfile(file_path): @@ -72,25 +77,20 @@ def delete_csv_files(output_dir): print(e) -def log_file_path(name): +def delete_csv_files(): """ - For use in logging.yaml tag to inject log file path - - filename: !!python/object/apply:activitysim.defaults.tracing.log_file_path ['asim.log'] + Delete CSV files Parameters ---------- - name: str - output folder name + output_dir: str + Directory of trace output CSVs Returns ------- - f: str - output folder name + Nothing """ - output_dir = inject.get_injectable('output_dir') - f = os.path.join(output_dir, name) - return f + delete_output_files(CSV_FILE_TYPE) def config_logger(custom_config_file=None, basic=False): @@ -143,10 +143,6 @@ def config_logger(custom_config_file=None, basic=False): print "Configured logging using basicConfig" logger.info("Configured logging using basicConfig") - output_dir = inject.get_injectable('output_dir') - logger.debug("Deleting files in output_dir %s" % output_dir) - delete_csv_files(output_dir) - def print_summary(label, df, describe=False, value_counts=False): """ @@ -178,229 +174,93 @@ def print_summary(label, df, describe=False, value_counts=False): logger.info("%s summary:\n%s" % (label, df.describe())) -def register_households(df, trace_hh_id): - """ - Register with orca households for tracing - - Parameters - ---------- - df: pandas.DataFrame - traced dataframe - - trace_hh_id: int - household id we are tracing - - Returns - ------- - Nothing - """ - - logger.info("tracing household id %s in %s households" % (trace_hh_id, len(df.index))) - - if trace_hh_id not in df.index: - logger.warn("trace_hh_id %s not in dataframe" % trace_hh_id) - - # inject persons_index name of person dataframe index - if df.index.name is None: - df.index.names = ['household_id'] - logger.warn("households table index had no name. renamed index '%s'" % df.index.name) - inject.add_injectable("hh_index_name", df.index.name) - - logger.debug("register_households injected hh_index_name '%s'" % df.index.name) - - -def register_persons(df, trace_hh_id): +def register_traceable_table(table_name, df): """ - Register with orca persons for tracing + Register traceable table Parameters ---------- df: pandas.DataFrame traced dataframe - trace_hh_id: int - household id we are tracing - Returns ------- Nothing """ - # inject persons_index name of person dataframe index - if df.index.name is None: - df.index.names = ['person_id'] - logger.warn("persons table index had no name. renamed index '%s'" % df.index.name) - inject.add_injectable("persons_index_name", df.index.name) - - logger.debug("register_persons injected persons_index_name '%s'" % df.index.name) - - # inject list of person_ids in household we are tracing - # this allows us to slice by person_id without requiring presence of household_id column - traced_persons_df = df[df['household_id'] == trace_hh_id] - trace_person_ids = traced_persons_df.index.tolist() - if len(trace_person_ids) == 0: - logger.warn("register_persons: trace_hh_id %s not found." % trace_hh_id) - - inject.add_injectable("trace_person_ids", trace_person_ids) - logger.debug("register_persons injected trace_person_ids %s" % trace_person_ids) - - logger.info("tracing person_ids %s in %s persons" % (trace_person_ids, len(df.index))) - - -def register_tours(df, trace_hh_id): - """ - Register with inject for tracing - - create an injectable 'trace_tour_ids' with a list of tour_ids in household we are tracing. - This allows us to slice by tour_id without requiring presence of person_id column - - Parameters - ---------- - df: pandas.DataFrame - traced dataframe - - trace_hh_id: int - household id we are tracing - - Returns - ------- - Nothing - """ + trace_hh_id = inject.get_injectable("trace_hh_id", None) - # get list of persons in traced household (should already have been registered) - person_ids = inject.get_injectable("trace_person_ids", []) + trace_injectable = 'trace_%s' % table_name + new_traced_ids = [] - if len(person_ids) == 0: - # trace_hh_id not in households table or register_persons was not not called - logger.warn("no person ids registered for trace_hh_id %s" % trace_hh_id) + if trace_hh_id is None: return - # but if household_id is in households, then we may have some tours - traced_tours_df = slice_ids(df, person_ids, column='person_id') - trace_tour_ids = traced_tours_df.index.tolist() - if len(trace_tour_ids) == 0: - logger.info("register_tours: no tours found for person_ids %s." % person_ids) - else: - logger.info("tracing tour_ids %s in %s tours" % (trace_tour_ids, len(df.index))) - - inject.add_injectable("trace_tour_ids", trace_tour_ids) - logger.debug("register_tours injected trace_tour_ids %s" % trace_tour_ids) - - -def register_trips(df, trace_hh_id): - """ - Register with inject for tracing - - create an injectable 'trace_trip_ids' with a list of tour_ids in household we are tracing. - This allows us to slice by trip_id without requiring presence of person_id column - - Parameters - ---------- - df: pandas.DataFrame - traced dataframe - - trace_hh_id: int - household id we are tracin - - Returns - ------- - Nothing - """ - - # get list of tours in traced household (should already have been registered) - tour_ids = inject.get_injectable("trace_tour_ids", []) - - if len(tour_ids) == 0: - # register_persons was not not called - logger.warn("no tour ids registered for trace_hh_id %s" % trace_hh_id) + traceable_tables = inject.get_injectable('traceable_tables', []) + if table_name not in traceable_tables: + logger.error("table '%s' not in traceable_tables" % table_name) return - # but if household_id is in households, then we may have some trips - traced_trips_df = slice_ids(df, tour_ids, column='tour_id') - trace_trip_ids = traced_trips_df.index.tolist() - if len(traced_trips_df) == 0: - logger.info("register_trips: no trips found for tour_ids %s." % tour_ids) - else: - logger.info("tracing trip_ids %s in %s trips" % (trace_trip_ids, len(df.index))) - - inject.add_injectable("trace_trip_ids", trace_trip_ids) - logger.debug("register_trips injected trace_tour_ids %s" % trace_trip_ids) - - -def register_participants(df, trace_hh_id): - """ - Register with inject for tracing - - create an injectable 'trace_participant_ids' with a list of participant_ids in - household we are tracing. - This allows us to slice by participant_ids without requiring presence of household_id column - - Parameters - ---------- - df: pandas.DataFrame - traced dataframe - - trace_hh_id: int - household id we are tracing - - Returns - ------- - Nothing - """ - - # but if household_id is in households, then we may have some tours - traced_participants_df = slice_ids(df, trace_hh_id, column='household_id') - trace_participant_ids = traced_participants_df.index.tolist() - if len(trace_participant_ids) == 0: - logger.info("register_participants: no participants found for household_id %s." % - trace_hh_id) - else: - logger.info("tracing participant_ids %s in %s participants" % - (trace_participant_ids, len(df.index))) - - inject.add_injectable("trace_participant_ids", trace_participant_ids) - logger.debug("register_participants injected trace_participant_ids %s" % trace_participant_ids) - - -def register_traceable_table(table_name, df): - """ - Register traceable table - - Parameters - ---------- - df: pandas.DataFrame - traced dataframe - - Returns - ------- - Nothing - """ + idx_name = df.index.name + if idx_name is None: + logger.error("Can't register table '%s' without index name" % table_name) + return - trace_hh_id = inject.get_injectable("trace_hh_id", None) + traceable_table_refs = inject.get_injectable('traceable_table_refs', None) - if trace_hh_id is None: + # traceable_table_refs is OrderedDict so we can find first registered table to slice by ref_con + if traceable_table_refs is None: + traceable_table_refs = OrderedDict() + if idx_name in traceable_table_refs and traceable_table_refs[idx_name] != table_name: + logger.error("table '%s' index name '%s' already registered for table '%s'" % + (table_name, idx_name, traceable_table_refs[idx_name])) return if table_name == 'households': - register_households(df, trace_hh_id) - elif table_name == 'persons': - register_persons(df, trace_hh_id) - elif table_name == 'trips': - register_trips(df, trace_hh_id) - elif table_name == 'tours': - register_tours(df, trace_hh_id) - elif table_name == 'participants': - register_participants(df, trace_hh_id) + if trace_hh_id not in df.index: + logger.warn("trace_hh_id %s not in dataframe" % trace_hh_id) + new_traced_ids = [] + else: + logger.info("tracing household id %s in %s households" % (trace_hh_id, len(df.index))) + new_traced_ids = [trace_hh_id] else: - logger.warn("register_traceable_table - don't grok '%s'" % table_name) - -def traceable_tables(): - - # names of all traceable tables ordered by dependency on household_id - # e.g. 'persons' has to be registered AFTER 'households' - - return ['households', 'persons', 'tours', 'trips', 'joint_tours', 'participants'] + # find first already registered ref_col we can use to slice this table + ref_con = next((c for c in traceable_table_refs if c in df.columns), None) + if ref_con is None: + logger.error("can't find a registered table to slice table '%s' index name '%s'" + " in traceable_table_refs: %s" % + (table_name, idx_name, traceable_table_refs)) + return + + # get traceable_ids for ref_con table + ref_con_trace_injectable = 'trace_%s' % traceable_table_refs[ref_con] + ref_con_traced_ids = inject.get_injectable(ref_con_trace_injectable, []) + + # inject list of ids in table we are tracing + # this allows us to slice by id without requiring presence of a household id column + traced_df = df[df[ref_con].isin(ref_con_traced_ids)] + new_traced_ids = traced_df.index.tolist() + if len(new_traced_ids) == 0: + logger.warn("register %s: no rows with %s in %s." % + (table_name, ref_con, ref_con_traced_ids)) + + # update traceable_table_refs with this traceable_table's ref_con + if idx_name not in traceable_table_refs: + traceable_table_refs[idx_name] = table_name + print "adding table %s.%s to traceable_table_refs" % (table_name, idx_name) + inject.add_injectable('traceable_table_refs', traceable_table_refs) + + # update the list of trace_ids for this table + prior_traced_ids = inject.get_injectable(trace_injectable, []) + if prior_traced_ids: + logger.info("register %s: adding %s ids to %s existing trace ids" % + (table_name, len(new_traced_ids), len(prior_traced_ids))) + traced_ids = prior_traced_ids + new_traced_ids + logger.info("register %s: tracing ids %s in %s %s" % + (table_name, traced_ids, len(df.index), table_name)) + if new_traced_ids: + inject.add_injectable(trace_injectable, traced_ids) def write_df_csv(df, file_path, index_label=None, columns=None, column_labels=None, transpose=True): @@ -479,7 +339,7 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, assert len(file_name) > 0 - file_path = log_file_path('%s.%s' % (file_name, CSV_FILE_TYPE)) + file_path = config.trace_file_path('%s.%s' % (file_name, CSV_FILE_TYPE)) if os.path.isfile(file_path): logger.error("write_csv file exists %s %s" % (type(df).__name__, file_name)) @@ -488,11 +348,11 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, # logger.debug("dumping %s dataframe to %s" % (df.shape, file_name)) write_df_csv(df, file_path, index_label, columns, column_labels, transpose=transpose) elif isinstance(df, pd.Series): - # logger.debug("dumping %s element series to %s" % (len(df.index), file_name)) + # logger.debug("dumping %s element series to %s" % (df.shape[0], file_name)) write_series_csv(df, file_path, index_label, columns, column_labels) elif isinstance(df, dict): df = pd.Series(data=df) - # logger.debug("dumping %s element dict to %s" % (len(df.index), file_name)) + # logger.debug("dumping %s element dict to %s" % (df.shape[0], file_name)) write_series_csv(df, file_path, index_label, columns, column_labels) else: logger.error("write_df_csv object '%s' of unexpected type: %s" % (file_name, type(df))) @@ -564,45 +424,29 @@ def get_trace_target(df, slicer): if slicer is None: slicer = df.index.name - # always slice by household id if we can if isinstance(df, pd.DataFrame): - if ('household_id' in df.columns): + # always slice by household id if we can + if 'household_id' in df.columns: slicer = 'household_id' - elif ('person_id' in df.columns): - slicer = 'person_id' + if slicer in df.columns: + column = slicer + + if column is None and df.index.name != slicer: + raise RuntimeError("bad slicer '%s' for df with index '%s'" % (slicer, df.index.name)) + + table_refs = inject.get_injectable('traceable_table_refs', {}) - if len(df.index) == 0: + if df.empty: target_ids = None - elif slicer == 'PERID' or slicer == inject.get_injectable('persons_index_name', None): - target_ids = inject.get_injectable('trace_person_ids', []) - elif slicer == 'HHID' or slicer == inject.get_injectable('hh_index_name', None): - target_ids = inject.get_injectable('trace_hh_id', []) - elif slicer == 'person_id': - target_ids = inject.get_injectable('trace_person_ids', []) - column = slicer - elif slicer == 'household_id': - target_ids = inject.get_injectable('trace_hh_id', []) - column = slicer - elif slicer == 'tour_id': - target_ids = inject.get_injectable('trace_tour_ids', []) - elif slicer == 'trip_id': - target_ids = inject.get_injectable('trace_trip_ids', []) - elif slicer == 'joint_tour_id': - target_ids = inject.get_injectable('trace_tour_ids', []) - elif slicer == 'participant_id': - target_ids = inject.get_injectable('trace_participant_ids', []) - elif slicer == 'TAZ' or slicer == 'ZONE': + elif slicer in table_refs: + # maps 'person_id' to 'persons', etc + table_name = table_refs[slicer] + target_ids = inject.get_injectable('trace_%s' % table_name, []) + elif slicer == 'TAZ': target_ids = inject.get_injectable('trace_od', []) - elif slicer == 'NONE': - target_ids = None else: - print df.head() - return None, 'NONE' raise RuntimeError("slice_canonically: bad slicer '%s'" % (slicer, )) - if target_ids and not isinstance(target_ids, (list, tuple)): - target_ids = [target_ids] - return target_ids, column @@ -629,7 +473,7 @@ def slice_canonically(df, slicer, label, warn_if_empty=False): if target_ids is not None: df = slice_ids(df, target_ids, column) - if warn_if_empty and len(df.index) == 0: + if warn_if_empty and df.shape[0] == 0: column_name = column or slicer logger.warn("slice_canonically: no rows in %s with %s == %s" % (label, column_name, target_ids)) @@ -682,8 +526,7 @@ def hh_id_for_chooser(id, choosers): scalar household_id or series of household_ids """ - if choosers.index.name == 'HHID' or \ - choosers.index.name == inject.get_injectable('hh_index_name', 'HHID'): + if choosers.index.name == 'household_id': hh_id = id elif 'household_id' in choosers.columns: hh_id = choosers.loc[id]['household_id'] @@ -730,7 +573,7 @@ def trace_df(df, label, slicer=None, columns=None, df = slice_canonically(df, slicer, label, warn_if_empty) - if len(df.index) > 0: + if df.shape[0] > 0: write_csv(df, file_name=label, index_label=(index_label or slicer), columns=columns, column_labels=column_labels, transpose=transpose) @@ -762,16 +605,17 @@ def interaction_trace_rows(interaction_df, choosers, sample_size=None): # slicer column name and id targets to use for chooser id added to model_design dataframe # currently we only ever slice by person_id, but that could change, so we check here... - if choosers.index.name == 'PERID' \ - or choosers.index.name == inject.get_injectable('persons_index_name', None): + table_refs = inject.get_injectable('traceable_table_refs', {}) + + if choosers.index.name == 'person_id' and inject.get_injectable('trace_persons', False): slicer_column_name = choosers.index.name - targets = inject.get_injectable('trace_person_ids', []) - elif ('household_id' in choosers.columns and inject.get_injectable('trace_hh_id', False)): + targets = inject.get_injectable('trace_persons', []) + elif 'household_id' in choosers.columns and inject.get_injectable('trace_hh_id', False): slicer_column_name = 'household_id' targets = inject.get_injectable('trace_hh_id', []) - elif ('person_id' in choosers.columns and inject.get_injectable('trace_person_ids', False)): + elif 'person_id' in choosers.columns and inject.get_injectable('trace_persons', False): slicer_column_name = 'person_id' - targets = inject.get_injectable('trace_person_ids', []) + targets = inject.get_injectable('trace_persons', []) else: print choosers.columns raise RuntimeError("interaction_trace_rows don't know how to slice index '%s'" @@ -847,7 +691,7 @@ def trace_interaction_eval_results(trace_results, trace_ids, label): return # write out the raw dataframe - file_path = log_file_path('%s.raw.csv' % label) + file_path = config.trace_file_path('%s.raw.csv' % label) trace_results.to_csv(file_path, mode="a", index=True, header=True) # if there are multiple targets, we want them in separate tables for readability diff --git a/docs/abmexample.rst b/docs/abmexample.rst index 7a6fbcc92..36300b421 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -262,7 +262,7 @@ Inputs In order to run the example, you first need two input files in the ``data`` folder as identified in the ``configs\settings.yaml`` file: -* store: mtc_asim.h5 - an HDF5 file containing the following MTC TM1 tables as pandas DataFrames for a subset of zones: +* input_store: mtc_asim.h5 - an HDF5 file containing the following MTC TM1 tables as pandas DataFrames for a subset of zones: * skims/accessibility - Zone-based accessibility measures * land_use/taz_data - Zone-based land use data (population and employment for example) @@ -507,8 +507,9 @@ The ``models`` setting contains the specification of the data pipeline model ste :: models: - - initialize + - initialize_landuse - compute_accessibility + - initialize_households - school_location_sample - school_location_logsums - school_location_simulate diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 00288459e..4292f6412 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -555,12 +555,12 @@ Data Tables The following tables are currently implemented: - * households - household attributes for each household being simulated. Index: ``HHID`` (see ``scripts\data_mover.ipynb``) - * landuse - zonal land use (such as population and employment) attributes. Index: ``TAZ`` (see ``scripts\data_mover.ipynb``) - * persons - person attributes for each person being simulated. Index: ``PERID`` (see ``scripts\data_mover.ipynb``) - * time windows - manages person time windows throughout the simulation. See :ref:`time_windows`. Index: ``PERID`` (see the person_windows table create decorator in ``activitysim.abm.tables.time_windows.py``) - * tours - tour attributes for each tour (mandatory, non-mandatory, joint, and atwork-subtour) being simulated. Index: ``TOURID`` (see ``activitysim.abm.models.util.tour_frequency.py``) - * trips - trip attributes for each trip being simulated. Index: ``TRIPID`` (see ``activitysim.abm.models.stop_frequency.py``) + * households - household attributes for each household being simulated. Index: ``household_id`` (see ``activitysim.abm.tables.households.py``) + * landuse - zonal land use (such as population and employment) attributes. Index: ``TAZ`` (see ``activitysim.abm.tables.landuse.py``) + * persons - person attributes for each person being simulated. Index: ``person_id`` (see ``activitysim.abm.tables.persons.py``) + * time windows - manages person time windows throughout the simulation. See :ref:`time_windows`. Index: ``person_id`` (see the person_windows table create decorator in ``activitysim.abm.tables.time_windows.py``) + * tours - tour attributes for each tour (mandatory, non-mandatory, joint, and atwork-subtour) being simulated. Index: ``tour_id`` (see ``activitysim.abm.models.util.tour_frequency.py``) + * trips - trip attributes for each trip being simulated. Index: ``trip_id`` (see ``activitysim.abm.models.stop_frequency.py``) A few additional tables are also used, which are not really tables, but classes: diff --git a/example/configs/initialize.yaml b/example/configs/initialize.yaml deleted file mode 100644 index addb7878a..000000000 --- a/example/configs/initialize.yaml +++ /dev/null @@ -1,43 +0,0 @@ - -#import_tables: -# - tablename: land_use -# column_map: -# #TOTHH: total_households -# #TOTEMP: total_employment -# #TOTACRE: total_acres -# COUNTY: county_id - -annotate_tables: - - tablename: land_use - column_map: - #TOTHH: total_households - #TOTEMP: total_employment - #TOTACRE: total_acres - COUNTY: county_id - annotate: - SPEC: annotate_landuse - DF: land_use - - tablename: persons - annotate: - SPEC: annotate_persons - DF: persons - TABLES: - - households - - tablename: households - column_map: - #TOTHH: total_households - #TOTEMP: total_employment - #TOTACRE: total_acres - PERSONS: hhsize - workers: num_workers -# hwork_f: num_workers_full -# hwork_p: num_workers_part -# huniv: num_univ -# hpresch: num_preschool -# hschdriv: num_driving_age_students - annotate: - SPEC: annotate_households - DF: households - TABLES: - - persons - - land_use diff --git a/example/configs/initialize_households.yaml b/example/configs/initialize_households.yaml new file mode 100644 index 000000000..572d2c565 --- /dev/null +++ b/example/configs/initialize_households.yaml @@ -0,0 +1,18 @@ + +annotate_tables: + - tablename: persons + annotate: + SPEC: annotate_persons + DF: persons + TABLES: + - households + - tablename: households + column_map: + PERSONS: hhsize + workers: num_workers + annotate: + SPEC: annotate_households + DF: households + TABLES: + - persons + - land_use diff --git a/example/configs/initialize_landuse.yaml b/example/configs/initialize_landuse.yaml new file mode 100644 index 000000000..21faccd0f --- /dev/null +++ b/example/configs/initialize_landuse.yaml @@ -0,0 +1,9 @@ + + +annotate_tables: + - tablename: land_use + column_map: + COUNTY: county_id + annotate: + SPEC: annotate_landuse + DF: land_use diff --git a/example/configs/logging.yaml b/example/configs/logging.yaml index cc16aecc5..a7e8155dc 100644 --- a/example/configs/logging.yaml +++ b/example/configs/logging.yaml @@ -29,7 +29,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.tracing.log_file_path ['asim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] mode: w formatter: fileFormatter level: !!python/name:logging.NOTSET diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 5c9521e32..508e70d9a 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -1,5 +1,5 @@ #input data store and skims -store: mtc_asim.h5 +input_store: mtc_asim.h5 skims_file: skims.omx #number of households to simulate @@ -20,8 +20,9 @@ chunk_size: 400000000 check_for_variability: False models: - - initialize + - initialize_landuse - compute_accessibility + - initialize_households - school_location_sample - school_location_logsums - school_location_simulate @@ -58,7 +59,8 @@ models: - write_data_dictionary - write_tables -#resume_after: joint_tour_destination_simulate +# to resume after last successful checkpoint, specify resume_after: _ +#resume_after: trip_mode_choice output_tables: action: include diff --git a/example/simulation.py b/example/simulation.py index da3a17d58..b163fd1a7 100644 --- a/example/simulation.py +++ b/example/simulation.py @@ -1,40 +1,54 @@ -import orca -from activitysim import abm -from activitysim.core import tracing -import pandas as pd -import numpy as np import os +import logging +import time +from random import randint + +import numpy as np +import multiprocessing as mp + +from activitysim.core import inject +from activitysim.core import tracing from activitysim.core.tracing import print_elapsed_time from activitysim.core.config import handle_standard_args from activitysim.core.config import setting + +from activitysim import abm from activitysim.core import pipeline -handle_standard_args() +logger = logging.getLogger('activitysim') + + +def run(): + + handle_standard_args() + + # specify None for a pseudo random base seed + # inject.add_injectable('rng_base_seed', 0) -# comment out the line below to default base seed to 0 random seed -# so that run results are reproducible -# pipeline.set_rn_generator_base_seed(seed=None) + tracing.config_logger() + tracing.delete_csv_files() -tracing.config_logger() + t0 = print_elapsed_time() -t0 = print_elapsed_time() + MODELS = setting('models') -MODELS = setting('models') + # If you provide a resume_after argument to pipeline.run + # the pipeline manager will attempt to load checkpointed tables from the checkpoint store + # and resume pipeline processing on the next submodel step after the specified checkpoint + resume_after = setting('resume_after', None) + if resume_after: + print "resume_after", resume_after -# If you provide a resume_after argument to pipeline.run -# the pipeline manager will attempt to load checkpointed tables from the checkpoint store -# and resume pipeline processing on the next submodel step after the specified checkpoint -resume_after = setting('resume_after', None) + pipeline.run(models=MODELS, resume_after=resume_after) -if resume_after: - print "resume_after", resume_after + # tables will no longer be available after pipeline is closed + pipeline.close_pipeline() -pipeline.run(models=MODELS, resume_after=resume_after) + t0 = print_elapsed_time("all models", t0) -# tables will no longer be available after pipeline is closed -pipeline.close_pipeline() -t0 = print_elapsed_time("all models", t0) +if __name__ == '__main__': + run() diff --git a/example_multi/configs/logging.yaml b/example_multi/configs/logging.yaml index 9d340dfdf..71b34b60e 100644 --- a/example_multi/configs/logging.yaml +++ b/example_multi/configs/logging.yaml @@ -28,7 +28,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.tracing.log_file_path ['asim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] mode: w formatter: fileFormatter level: !!python/name:logging.NOTSET diff --git a/example_multi/extensions/skims.py b/example_multi/extensions/skims.py index 307280fad..d61d07570 100644 --- a/example_multi/extensions/skims.py +++ b/example_multi/extensions/skims.py @@ -19,6 +19,11 @@ """ +@inject.injectable(cache=True) +def cache_skim_key_values(settings): + return settings['skim_time_periods']['labels'] + + def add_to_skim_dict(skim_dict, omx_file, cache_skim_key_values, offset_int=None): if offset_int is None: diff --git a/scripts/make_pipeline_output.py b/scripts/make_pipeline_output.py index b4ebd1590..460aefc94 100644 --- a/scripts/make_pipeline_output.py +++ b/scripts/make_pipeline_output.py @@ -4,8 +4,8 @@ import pandas as pd -pipeline_filename = 'output\pipeline.h5' -out_fields_filename = 'output\pipeline_fields.csv' +pipeline_filename = 'output\\pipeline.h5' +out_fields_filename = 'output\\pipeline_fields.csv' # get pipeline tables pipeline = pd.io.pytables.HDFStore(pipeline_filename) diff --git a/setup.py b/setup.py index 0f4719a90..38322014d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='activitysim', - version='0.7', + version='0.8', description='Activity-Based Travel Modeling', author='contributing authors', author_email='ben.stabler@rsginc.com', From 97d1cde0fb2fc551a09bf59bc50788b6f5497a5f Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 5 Sep 2018 11:25:43 -0400 Subject: [PATCH 005/122] atwork subtour scheduling expression files and model support --- .../abm/models/atwork_subtour_scheduling.py | 46 +++++++++----- .../abm/models/joint_tour_participation.py | 8 +++ activitysim/abm/models/trip_mode_choice.py | 4 +- .../models/util/vectorize_tour_scheduling.py | 3 +- activitysim/core/timetable.py | 5 ++ activitysim/core/tracing.py | 2 +- example/configs/annotate_persons_jtp.csv | 3 + example/configs/joint_tour_participation.yaml | 6 ++ example/configs/tour_scheduling_atwork.csv | 60 +++++++++++++++++-- example/configs/tour_scheduling_atwork.yaml | 9 +++ .../tour_scheduling_atwork_preprocessor.csv | 3 + example/configs/tour_scheduling_school.csv | 3 +- example/configs/tour_scheduling_work.csv | 3 +- 13 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 example/configs/annotate_persons_jtp.csv create mode 100644 example/configs/tour_scheduling_atwork.yaml create mode 100644 example/configs/tour_scheduling_atwork_preprocessor.csv diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index 3523e2664..0c3adcde3 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -17,12 +17,13 @@ from activitysim.core import timetable as tt from .util.vectorize_tour_scheduling import vectorize_subtour_scheduling from .util import expressions +from .util.mode import annotate_preprocessors from activitysim.core.util import assign_in_place logger = logging.getLogger(__name__) -DUMP = False +DUMP = True @inject.injectable() @@ -36,6 +37,7 @@ def atwork_subtour_scheduling( persons_merged, tdd_alts, tour_scheduling_atwork_spec, + skim_dict, chunk_size, trace_hh_id): """ @@ -43,9 +45,7 @@ def atwork_subtour_scheduling( """ trace_label = 'atwork_subtour_scheduling' - model_settings = config.read_model_settings('atwork_subtour_scheduling.yaml') - - constants = config.get_model_constants(model_settings) + model_settings = config.read_model_settings('tour_scheduling_atwork.yaml') persons_merged = persons_merged.to_frame() @@ -59,19 +59,23 @@ def atwork_subtour_scheduling( logger.info("Running %s with %d tours" % (trace_label, len(subtours))) + # preprocessor + constants = config.get_model_constants(model_settings) + od_skim_wrapper = skim_dict.wrap('origin', 'destination') + do_skim_wrapper = skim_dict.wrap('destination', 'origin') + skims = { + "od_skims": od_skim_wrapper, + "do_skims": do_skim_wrapper, + } + annotate_preprocessors( + subtours, constants, skims, + model_settings, trace_label) + # parent_tours table with columns ['tour_id', 'tdd'] index = tour_id parent_tour_ids = subtours.parent_tour_id.astype(int).unique() parent_tours = pd.DataFrame({'tour_id': parent_tour_ids}, index=parent_tour_ids) parent_tours = parent_tours.merge(tours[['tdd']], left_index=True, right_index=True) - """ - parent_tours - tour_id tdd - 20973389 20973389 26 - 44612864 44612864 3 - 48954854 48954854 7 - """ - tdd_choices = vectorize_subtour_scheduling( parent_tours, subtours, @@ -84,13 +88,23 @@ def atwork_subtour_scheduling( assign_in_place(tours, tdd_choices) pipeline.replace_table("tours", tours) - tracing.dump_df(DUMP, - tt.tour_map(parent_tours, subtours, tdd_alts, persons_id_col='parent_tour_id'), - trace_label, 'tour_map') - if trace_hh_id: tracing.trace_df(tours[tours.tour_category == 'atwork'], label="atwork_subtour_scheduling", slicer='person_id', index_label='tour_id', columns=None) + + if DUMP: + subtours = tours[tours.tour_category == 'atwork'] + parent_tours = tours[tours.index.isin(subtours.parent_tour_id)] + + tracing.dump_df(DUMP, subtours, trace_label, 'sub_tours') + tracing.dump_df(DUMP, parent_tours, trace_label, 'parent_tours') + + parent_tours['parent_tour_id'] = parent_tours.index + subtours = pd.concat([parent_tours, subtours]) + tracing.dump_df(DUMP, + tt.tour_map(parent_tours, subtours, tdd_alts, + persons_id_col='parent_tour_id'), + trace_label, 'tour_map') diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 696ed7cc0..7c5b84b2f 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -312,6 +312,14 @@ def joint_tour_participation( pipeline.replace_table("tours", tours) + # - annotate persons + persons = inject.get_table('persons').to_frame() + expressions.assign_columns( + df=persons, + model_settings=model_settings.get('annotate_persons'), + trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) + pipeline.replace_table("persons", persons) + if trace_hh_id: tracing.trace_df(participants, label="joint_tour_participation.participants") diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index cc690e866..064fda729 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -75,11 +75,11 @@ def trip_mode_choice( odt_skim_stack_wrapper = skim_stack.wrap(left_key=orig_col, right_key=dest_col, skim_key='trip_period') - od_skim_stack_wrapper = skim_dict.wrap('origin', 'destination') + od_skim_wrapper = skim_dict.wrap('origin', 'destination') skims = { "odt_skims": odt_skim_stack_wrapper, - "od_skims": od_skim_stack_wrapper, + "od_skims": od_skim_wrapper, } constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index b240f3158..f9165f483 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -438,7 +438,8 @@ def vectorize_subtour_scheduling(parent_tours, subtours, persons_merged, alts, s The spec which will be passed to interaction_simulate. (all subtours share same spec regardless of subtour type) constants : dict - dict of model-specific constants for eval chunk_size + dict of model-specific constants for eval + chunk_size trace_label Returns diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index 00265f8a5..1db224dba 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -96,6 +96,8 @@ def tour_map(persons, tours, tdd_alts, persons_id_col='person_id'): tour_type = keys[0] tour_sigil = sigil[tour_type] + print "xxx tour_type", tour_type, tour_sigil + # numpy array with one time window row for each row in nth_tours tour_windows = window_periods_df.loc[nth_tours.tdd].values @@ -114,6 +116,9 @@ def tour_map(persons, tours, tdd_alts, persons_id_col='person_id'): # a = pd.Series([' '.join(a) for a in agenda], index=persons.index) a = pd.DataFrame(data=agenda, columns=[str(w) for w in range(min_period, max_period+1)]) + a.index = persons.index + a.index.name = persons_id_col + return a diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index d0f4cb7fa..adfb6a89b 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -539,7 +539,7 @@ def hh_id_for_chooser(id, choosers): def dump_df(dump_switch, df, trace_label, fname): if dump_switch: trace_label = extend_trace_label(trace_label, 'DUMP.%s' % fname) - trace_df(df, trace_label, slicer='NONE', transpose=False) + trace_df(df, trace_label, index_label=df.index.name, slicer='NONE', transpose=False) def trace_df(df, label, slicer=None, columns=None, diff --git a/example/configs/annotate_persons_jtp.csv b/example/configs/annotate_persons_jtp.csv new file mode 100644 index 000000000..460eba9b5 --- /dev/null +++ b/example/configs/annotate_persons_jtp.csv @@ -0,0 +1,3 @@ +Description,Target,Expression +#,, annotate persons table after joint_tour_participation model has run +num_joint_tours,num_joint_tours,"joint_tour_participants.groupby('person_id').size().reindex(persons.index).fillna(0)" diff --git a/example/configs/joint_tour_participation.yaml b/example/configs/joint_tour_participation.yaml index 13538900a..f7b3a765b 100644 --- a/example/configs/joint_tour_participation.yaml +++ b/example/configs/joint_tour_participation.yaml @@ -8,3 +8,9 @@ preprocessor: # TABLES: # - persons # - accessibility + +annotate_persons: + SPEC: annotate_persons_jtp + DF: persons + TABLES: + - joint_tour_participants diff --git a/example/configs/tour_scheduling_atwork.csv b/example/configs/tour_scheduling_atwork.csv index 73ce01a43..26e413a70 100644 --- a/example/configs/tour_scheduling_atwork.csv +++ b/example/configs/tour_scheduling_atwork.csv @@ -1,4 +1,56 @@ -Description,Expression,Coefficient -Free-flow round trip auto time shift effects - departure,roundtrip_auto_time_to_work * start,-0.00114 -Free-flow round trip auto time shift effects - duration,roundtrip_auto_time_to_work * duration,0.00221 -Part-time worker departure shift effects,(ptype == 2) * start,0.06736 +Description,Expression,Coefficient +# Departure Constants,, +Early start at 5,start < 6,-7.765548476 +AM peak start at 6,start == 6,-6.156717827 +AM peak start at 7,start == 7,-4.061708142 +AM peak start at 8,start == 8,-2.330535201 +AM peak start at 9,start == 9,-1.881593386 +Midday start at 10/11/12,(start > 9) & (start < 13),0 +Midday start at 13/14/15,(start > 12) & (start < 16),-0.77502158 +PM peak start at 16/17/18,(start > 15) & (start < 19),-0.227528489 +Evening start at 19/20/21,(start > 18) & (start < 22),-1.015090023 +Late start at 22/23,start > 21,-0.737570054 +# Arrival Constants,, +Early end at 5/6 ,end < 7,-2.928312295 +AM peak end,(end > 6) & (end < 10),-2.928312295 +Midday end at 10/11/12,(end > 9) & (end < 13),-2.297264374 +Midday end at 13/14,(end > 12) & (end < 15),0 +PM peak end at 15,end == 15,-0.578344457 +PM peak end at 16,end == 16,-1.09408722 +PM peak end at 17,end == 17,-1.1658466 +PM peak end at 18,end == 18,-1.496131081 +Evening end at 19/20/21,(end > 18) & (end < 22),-2.31998226 +Late end at 22/23,end > 21,-2.31998226 +#,, +Duration of 0 hours,duration==0,-0.906681512 +Duration of 1 hour,duration==1,0 +Duration of 2 to 3 hours,(duration >=1) and (duration <= 4),-1.362175802 +Duration of 4 to 5 hours,(duration >=4) and (duration <=5),-0.819617616 +Duration of 6 to 7 hours,(duration >=6) and (duration <=7),1.088111072 +Duration of 8 to 10 hours,(duration >=8) and (duration <=10),1.734038505 +Duration of 11 to 13 hours,(duration >=11) and (duration <=13),0.3 +Duration of 14 to 18 hours,(duration >=14) and (duration <=18),0 +#,, +Start shift for outbound auto travel time for off-peak,"@df.start * np.minimum(df.sovtimemd, time_cap)",0.00065 +Start shift for inbound auto travel time for off-peak,"@df.start * np.minimum(df.sovtimemd_t, time_cap)",0.00065 +Duration shift for outbound auto travel time for off-peak,"@df.duration * np.minimum(df.sovtimemd, time_cap)",0.00981 +Duration shift for inbound auto travel time for off-peak,"@df.duration * np.minimum(df.sovtimemd_t, time_cap)",0.00981 +#,, +Start shift for business-related sub-tour purpose,(tour_type == 'business') * start,-0.1113 +Duration shift for business-related sub-tour purpose,(tour_type == 'business') * duration,0.2646 +Start shift for first sub-tour of the same work tour,(tour_type_num == 1) * start,-0.5433 +Duration shift for first sub-tour of the same work tour,(tour_type_num == 1) * duration,-0.3992 +Start shift for subsequent sub-tour of the same work tour,(tour_type_num == 2) * start,-0.1844 +Duration shift for subsequent sub-tour of the same work tour,(tour_type_num == 2) * duration,-0.1844 +Start shift for number of mandatory tours made by the person,start * num_mand,-0.0193 +Duration shift for number of mandatory tours made by the person,duration * num_mand,-0.7702 +Start shift for number of joint tours in which the person participated,start * num_joint_tours,-0.0206 +Duration shift for number of joint tours in which the person participated,duration * num_joint_tours,-0.2497 +Start shift for number of individual nonm tours (including escort) made by the person,start * num_non_mand,-0.0128 +Duration shift for number of individual nonm tours (including escort) made by the person,duration * num_non_mand,-0.0422 +#,, +Dummy for business-related purpose and duration from 0 to 1,(tour_type == 'business') & (duration <=1),-1.543 +Dummy for eating-out purpose and duration of 1 hour,(tour_type == 'business') & (duration ==1),0.3999 +Dummy for eating-out purpose and departure at 11,(tour_type == 'business') & (start == 11),1.511 +Dummy for eating-out purpose and departure at 12,(tour_type == 'business') & (start == 12),2.721 +Dummy for eating-out purpose and departure at 13,(tour_type == 'business') & (start == 13),2.122 diff --git a/example/configs/tour_scheduling_atwork.yaml b/example/configs/tour_scheduling_atwork.yaml new file mode 100644 index 000000000..ee6eb396b --- /dev/null +++ b/example/configs/tour_scheduling_atwork.yaml @@ -0,0 +1,9 @@ +preprocessor: + SPEC: tour_scheduling_atwork_preprocessor + DF: df +# TABLES: +# - land_use +# - tours + +CONSTANTS: + time_cap: 30 diff --git a/example/configs/tour_scheduling_atwork_preprocessor.csv b/example/configs/tour_scheduling_atwork_preprocessor.csv new file mode 100644 index 000000000..c44e65d05 --- /dev/null +++ b/example/configs/tour_scheduling_atwork_preprocessor.csv @@ -0,0 +1,3 @@ +Description,Target,Expression +,sovtimemd,"od_skims[('SOV_TIME', 'MD')]" +,sovtimemd_t,"do_skims[('SOV_TIME', 'MD')]" diff --git a/example/configs/tour_scheduling_school.csv b/example/configs/tour_scheduling_school.csv index 649de0e44..b58fc8065 100644 --- a/example/configs/tour_scheduling_school.csv +++ b/example/configs/tour_scheduling_school.csv @@ -18,7 +18,8 @@ First of 2+ school/univ tours- duration<6 hrs,(tour_count>1) & (tour_num == 1) & Subsequent of 2+ school/univ tours- duration<6 hrs,(tour_num > 1) & (duration < 6),2.142 School+work tours by student- duration<6 hrs,work_and_school_and_worker & (duration < 6),1.73 School+work tours by worker- duration<6 hrs,work_and_school_and_student & (duration < 6),2.142 -# Mode Choice Logsum,@@modeChoiceLogsumAlt,2.127 +#,, +# FIXME - Mode Choice Logsum,@@modeChoiceLogsumAlt,2.127 #,, Previously-scheduled tour ends in this departure hour,"@tt.previous_tour_ends(df.person_id, df.start)",-0.5995 Previously-scheduled tour begins in this arrival hour,"@tt.previous_tour_begins(df.person_id, df.end)",-1.102 diff --git a/example/configs/tour_scheduling_work.csv b/example/configs/tour_scheduling_work.csv index 3eaf2406f..51624a5ac 100644 --- a/example/configs/tour_scheduling_work.csv +++ b/example/configs/tour_scheduling_work.csv @@ -26,7 +26,8 @@ First of 2+ work tours- duration<8 hrs,((tour_count>1) & (tour_num == 1)) & (dur Subsequent of 2+ work tours- duration<8 hrs,(tour_num == 2) & (duration < 8),2.582 Work+school tours by worker- duration<8 hrs,(mandatory_tour_frequency == 'work_and_school') & is_worker & (duration < 8),0.9126 School+work tours by student- duration<8 hrs,(mandatory_tour_frequency == 'work_and_school') & is_student & (duration < 8),2.582 -# Mode Choice Logsum,mode_choice_logsum,1.027 +#,, +# FIXME - Mode Choice Logsum,mode_choice_logsum,1.027 #,, Previously-scheduled tour ends in this departure hour,"@tt.previous_tour_ends(df.person_id, df.start)",-0.8935 Previously-scheduled tour begins in this arrival hour,"@tt.previous_tour_begins(df.person_id, df.end)",-1.334 From b43bf16c795a3087053dbd2e0a1698310d2446ca Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 6 Sep 2018 10:23:30 -0400 Subject: [PATCH 006/122] AccessibilitySkims slices skims to handle partial orig/dest zone coverage --- activitysim/abm/models/accessibility.py | 54 +++++++++++++++++++----- activitysim/abm/models/auto_ownership.py | 2 +- activitysim/abm/tables/skims.py | 19 +++++++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 9fb9837f1..c623dcd0a 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -34,9 +34,11 @@ class AccessibilitySkims(object): whether to transpose the matrix before flattening. (i.e. act as a D-O instead of O-D skim) """ - def __init__(self, skim_dict, dest_zones, orig_zones, transpose=False): + def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): + + logger.info("init AccessibilitySkims with %d dest zones %d orig zones skim_data.shape %s" % + (len(dest_zones), len(orig_zones), skim_dict.skim_data.shape, )) - assert skim_dict.skim_data.shape[0] == len(dest_zones) assert len(orig_zones) <= len(dest_zones) assert np.isin(orig_zones, dest_zones).all() assert len(np.unique(orig_zones)) == len(orig_zones) @@ -44,12 +46,26 @@ def __init__(self, skim_dict, dest_zones, orig_zones, transpose=False): self.skim_dict = skim_dict self.transpose = transpose - self.orig_map = None - if len(orig_zones) < len(dest_zones): - self.orig_map = np.isin(dest_zones, orig_zones) + if skim_dict.skim_data.shape[0] == len(orig_zones): + # no slicing required + self.slice_map = None else: - self.orig_map = None + # 2-d boolean slicing in numpy is a bit tricky + # data = data[orig_map, dest_map] # <- WRONG! + # data = data[orig_map, :][:, dest_map] # <- RIGHT + # data = data[np.ix_(orig_map, dest_map)] # <- ALSO RIGHT + + skim_index = range(skim_dict.skim_data.shape[0]) + orig_map = np.isin(skim_index, skim_dict.offset_mapper.map(orig_zones)) + dest_map = np.isin(skim_index, skim_dict.offset_mapper.map(dest_zones)) + + if not dest_map.all(): + # not using the whole skim matrix + logger.info("%s skim zones not in dest_map: %s" % + ((~dest_map).sum(), np.ix_(~dest_map))) + + self.slice_map = np.ix_(orig_map, dest_map) def __getitem__(self, key): """ @@ -65,8 +81,10 @@ def __getitem__(self, key): if self.transpose: data = data.transpose() - if self.orig_map is not None: - data = data[self.orig_map, :] + if self.slice_map is not None: + # slice skim to include only orig rows and dest columns + # 2-d boolean slicing in numpy is a bit tricky - see explanation in __init__ + data = data[self.slice_map] return data.flatten() @@ -97,22 +115,36 @@ def compute_accessibility(accessibility, accessibility_spec, steeper than automobile or transit. The minimum accessibility is zero. """ - logger.info("Running compute_accessibility") + trace_label = 'compute_accessibility' model_settings = config.read_model_settings('accessibility.yaml') accessibility_df = accessibility.to_frame() + logger.info("Running %s with %d dest zones" % (trace_label, len(accessibility_df))) + constants = config.get_model_constants(model_settings) land_use_columns = model_settings.get('land_use_columns', []) land_use_df = land_use.to_frame() + # #bug + # + # land_use_df = land_use_df[land_use_df.index % 2 == 1] + # accessibility_df = accessibility_df[accessibility_df.index.isin(land_use_df.index)].head(5) + # + # print "land_use_df", land_use_df.index + # print "accessibility_df", accessibility_df.index + # #bug + orig_zones = accessibility_df.index.values dest_zones = land_use_df.index.values orig_zone_count = len(orig_zones) dest_zone_count = len(dest_zones) + logger.info("Running %s with %d dest zones %d orig zones" % + (trace_label, dest_zone_count, orig_zone_count)) + # create OD dataframe od_df = pd.DataFrame( data={ @@ -134,8 +166,8 @@ def compute_accessibility(accessibility, accessibility_spec, locals_d = { 'log': np.log, 'exp': np.exp, - 'skim_od': AccessibilitySkims(skim_dict, dest_zones, orig_zones), - 'skim_do': AccessibilitySkims(skim_dict, dest_zones, orig_zones, transpose=True) + 'skim_od': AccessibilitySkims(skim_dict, orig_zones, dest_zones), + 'skim_do': AccessibilitySkims(skim_dict, orig_zones, dest_zones, transpose=True) } if constants is not None: locals_d.update(constants) diff --git a/activitysim/abm/models/auto_ownership.py b/activitysim/abm/models/auto_ownership.py index d40902b4c..c42491fe0 100644 --- a/activitysim/abm/models/auto_ownership.py +++ b/activitysim/abm/models/auto_ownership.py @@ -33,7 +33,7 @@ def auto_ownership_simulate(households, trace_label = 'auto_ownership_simulate' model_settings = config.read_model_settings('auto_ownership.yaml') - logger.info("Running auto_ownership_simulate with %d households" % len(households_merged)) + logger.info("Running %s with %d households" % (trace_label, len(households_merged))) nest_spec = config.get_logit_model_settings(model_settings) constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 4aefa18b9..81569c691 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -13,6 +13,7 @@ from activitysim.core import skim from activitysim.core import inject +from activitysim.core import util logger = logging.getLogger(__name__) @@ -39,11 +40,14 @@ def skims_to_load(omx_file_path, tags_to_load=None): key = (key1, key2) if sep else key1 skim_keys[skim_name] = key - num_skims = len(skim_keys.keys()) + num_skims = len(skim_keys.keys()) skim_data_shape = omx_shape + (num_skims, ) skim_dtype = np.float32 + logger.debug("skims_to_load from %s" % (omx_file_path, )) + logger.debug("skims_to_load skim_data_shape %s skim_dtype %s" % (skim_data_shape, skim_dtype)) + return skim_keys, skim_data_shape, skim_dtype @@ -72,6 +76,8 @@ def load_skims(omx_file_path, skim_keys, skim_data): n = 0 for skim_name, key in skim_keys.iteritems(): + logger.debug("load_skims skim_name %s key %s" % (skim_name, key)) + omx_data = omx_file[skim_name] assert np.issubdtype(omx_data.dtype, np.floating) @@ -86,14 +92,16 @@ def load_skims(omx_file_path, skim_keys, skim_data): @inject.injectable(cache=True) def skim_dict(data_dir, settings): - logger.info("skims injectable loading skims") - omx_file_path = os.path.join(data_dir, settings["skims_file"]) tags_to_load = settings['skim_time_periods']['labels'] + logger.info("loading skim_dict from %s" % (omx_file_path, )) + # select the skims to load skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + logger.debug("skim_data_shape %s skim_dtype %s" % (skims_shape, skim_dtype)) + skim_buffer = inject.get_injectable('skim_buffer', None) if skim_buffer: logger.info('Using existing skim_buffer for skims') @@ -102,6 +110,9 @@ def skim_dict(data_dir, settings): skim_data = np.zeros(skims_shape, dtype=skim_dtype) load_skims(omx_file_path, skim_keys, skim_data) + logger.info("skim_data dtype %s shape %s bytes %s (%s)" % + (skim_dtype, skims_shape, skim_data.nbytes, util.GB(skim_data.nbytes))) + # create skim dict skim_dict = skim.SkimDict(skim_data, skim_keys.values()) skim_dict.offset_mapper.set_offset_int(-1) @@ -112,5 +123,5 @@ def skim_dict(data_dir, settings): @inject.injectable(cache=True) def skim_stack(skim_dict): - logger.debug("loading skim_stack") + logger.debug("loading skim_stack injectable") return skim.SkimStack(skim_dict) From 8cbbc8a3344eb9f5d1cddff0364edbbcbf2b790d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 11 Sep 2018 09:51:38 -0400 Subject: [PATCH 007/122] skim usage tracing --- activitysim/abm/models/initialize.py | 2 + activitysim/core/skim.py | 26 +++++- activitysim/core/steps/output.py | 132 +++++++++++++++++++++++++++ activitysim/core/tracing.py | 5 + 4 files changed, 160 insertions(+), 5 deletions(-) diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index f6a901dc7..024dd0b1a 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -15,6 +15,7 @@ from activitysim.core.steps.output import write_data_dictionary from activitysim.core.steps.output import write_tables +from activitysim.core.steps.output import track_skim_usage from .util import expressions @@ -95,6 +96,7 @@ def preload_injectables(): logger.info("preload_injectables") + inject.add_step('track_skim_usage', track_skim_usage) inject.add_step('write_data_dictionary', write_data_dictionary) inject.add_step('write_tables', write_tables) diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 5cb4fe10c..b0da50332 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -149,6 +149,12 @@ def __init__(self, skim_data, skim_keys): self.num_skims = skim_data.shape[2] self.offset_mapper = OffsetMapper() + self.usage = set() + + def touch(self, key): + + self.usage.add(key) + def get(self, key): """ Get an available wrapped skim object (not the lookup) @@ -167,6 +173,8 @@ def get(self, key): n = self.skim_key_to_skim_num[key] assert n < self.num_skims + self.touch(key) + data = self.skim_data[:, :, n] return SkimWrapper(data, self.offset_mapper) @@ -309,23 +317,29 @@ def __init__(self, skim_dict): self.offset_mapper = skim_dict.offset_mapper self.skim_dict = skim_dict + # build a dict mapping key1 to dict of key2 (dim3) indexes in skim_data skim_dim3 = OrderedDict() for key, n in skim_dict.skim_key_to_skim_num.iteritems(): if isinstance(key, tuple): key1, key2 = key skim_dim3.setdefault(key1, OrderedDict())[key2] = n - self.skim_dim3 = skim_dim3 logger.info("SkimStack.__init__ loaded %s keys with %s total skims" % (len(self.skim_dim3), sum([len(d) for d in self.skim_dim3.values()]))) - def __str__(self): + self.usage = set() + + def touch(self, key): - return "\n".join( - "%s %s" % (key1, sub_dict) - for key1, sub_dict in self.skim_dim3.iteritems()) + self.usage.add(key) + + # def __str__(self): + # + # return "\n".join( + # "%s %s" % (key1, sub_dict) + # for key1, sub_dict in self.skim_dim3.iteritems()) def lookup(self, orig, dest, dim3, key): @@ -336,6 +350,8 @@ def lookup(self, orig, dest, dim3, key): stacked_skim_data = self.skim_dict.skim_data skim_keys_to_indexes = self.skim_dim3[key] + self.touch(key) + # skim_indexes = dim3.map(skim_keys_to_indexes).astype('int') # this should be faster than map skim_indexes = np.vectorize(skim_keys_to_indexes.get)(dim3) diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index 616c62135..ebdd1f046 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -5,6 +5,8 @@ import os import pandas as pd +from collections import OrderedDict + from activitysim.core import pipeline from activitysim.core import inject from activitysim.core import config @@ -14,6 +16,59 @@ logger = logging.getLogger(__name__) +def track_skim_usage(output_dir): + """ + write statistics on skim usage (diagnostic to detect loading of un-needed skims) + + FIXME - have not yet implemented a facility to avoid loading of unused skims + + Parameters + ---------- + output_dir: str + + """ + pd.options.display.max_columns = 500 + pd.options.display.max_rows = 100 + + checkpoints = pipeline.get_checkpoints() + tables = OrderedDict() + + skim_dict = inject.get_injectable('skim_dict') + skim_stack = inject.get_injectable('skim_stack', None) + + with open(config.output_file_path('skim_usage.txt'), 'w') as file: + + print >> file, "\n### skim_dict usage" + for key in skim_dict.usage: + print>> file, key + + if skim_stack is None: + unused_keys = {k for k in skim_dict.skim_key_to_skim_num} - {k for k in skim_dict.usage} + + print >> file, "\n### unused skim keys" + for key in unused_keys: + print>> file, key + + else: + + print >> file, "\n### skim_stack usage" + for key in skim_stack.usage: + print>> file, key + + unused = {k for k in skim_dict.skim_key_to_skim_num if not isinstance(k, tuple)} - \ + {k for k in skim_dict.usage if not isinstance(k, tuple)} + print >> file, "\n### unused skim str keys" + for key in unused: + print>> file, key + + unused = {k[0] for k in skim_dict.skim_key_to_skim_num if isinstance(k, tuple)} - \ + {k[0] for k in skim_dict.usage if isinstance(k, tuple)} - \ + {k for k in skim_stack.usage} + print >> file, "\n### unused skim dim3 keys" + for key in unused: + print>> file, key + + def write_data_dictionary(output_dir): """ Write table_name, number of rows, columns, and bytes for each checkpointed table @@ -39,6 +94,83 @@ def write_data_dictionary(output_dir): print >> file, df.dtypes +# def write_data_dictionary(output_dir): +# """ +# Write table_name, number of rows, columns, and bytes for each checkpointed table +# +# Parameters +# ---------- +# output_dir: str +# +# """ +# pd.options.display.max_columns = 500 +# pd.options.display.max_rows = 100 +# +# checkpoints = pipeline.get_checkpoints() +# tables = OrderedDict() +# +# table_names = [c for c in checkpoints if c not in pipeline.NON_TABLE_COLUMNS] +# +# with open(config.output_file_path('data_dict.txt'), 'w') as file: +# +# for index, row in checkpoints.iterrows(): +# +# checkpoint = row[pipeline.CHECKPOINT_NAME] +# +# print >> file, "\n##########################################" +# print >> file, "# %s" % checkpoint +# print >> file, "##########################################" +# +# for table_name in table_names: +# +# if row[table_name] == '' and table_name in tables: +# print >> file, "\n### %s dropped %s" % (checkpoint, table_name, ) +# del tables[table_name] +# +# if row[table_name] == checkpoint: +# df = pipeline.get_table(table_name, checkpoint) +# info = tables.get(table_name, None) +# if info is None: +# +# print >> file, "\n### %s created %s %s\n" % \ +# (checkpoint, table_name, df.shape) +# +# print >> file, df.dtypes +# print >> file, 'index:', df.index.name, df.index.dtype +# +# else: +# new_cols = [c for c in df.columns.values if c not in info['columns']] +# dropped_cols = [c for c in info['columns'] if c not in df.columns.values] +# new_rows = df.shape[0] - info['num_rows'] +# if new_cols: +# +# print >> file, "\n### %s added %s columns to %s %s\n" % \ +# (checkpoint, len(new_cols), table_name, df.shape) +# print >> file, df[new_cols].dtypes +# +# if dropped_cols: +# print >> file, "\n### %s dropped %s columns from %s %s\n" % \ +# (checkpoint, len(dropped_cols), table_name, df.shape) +# print >> file, dropped_cols +# +# if new_rows > 0: +# print >> file, "\n### %s added %s rows to %s %s" % \ +# (checkpoint, new_rows, table_name, df.shape) +# elif new_rows < 0: +# print >> file, "\n### %s dropped %s rows from %s %s" % \ +# (checkpoint, new_rows, table_name, df.shape) +# else: +# if not new_cols and not dropped_cols: +# print >> file, "\n### %s modified %s %s" % \ +# (checkpoint, table_name, df.shape) +# +# tables[table_name] = { +# 'checkpoint_name': checkpoint, +# 'columns': df.columns.values, +# 'num_rows': df.shape[0] +# } + + def write_tables(output_dir): """ Write pipeline tables as csv files (in output directory) as specified by output_tables list diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index adfb6a89b..b945f0ad8 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -28,6 +28,11 @@ logger = logging.getLogger(__name__) +def log_file_path(file_name): + # FIXME - for compatability with v0.7 + return config.log_file_path(file_name) + + def check_for_variability(): return inject.get_injectable('check_for_variability', False) From 1c72c48add787aea569c7303a0187cd770e3b8d7 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 12 Sep 2018 09:34:28 -0400 Subject: [PATCH 008/122] throwaway targets recognized by assign.assign_variables --- activitysim/core/assign.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 237706f0c..7bc82285e 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -16,7 +16,7 @@ def uniquify_key(dict, key, template="{} ({})"): """ - rename key so there are no duplicates with keys in dice + rename key so there are no duplicates with keys in dict e.g. if there is already a key named "dog", the second key will be reformatted to "dog (2)" """ @@ -183,7 +183,10 @@ def assign_variables(assignment_expressions, df, locals_dict, df_alias=None, tra np_logger = NumpyLogger(logger) - def is_local(target): + def is_throwaway(target): + return target == '_' + + def is_temp_scalar(target): return target.startswith('_') and target.isupper() def is_temp(target): @@ -229,11 +232,12 @@ def to_series(x, target=None): if target in local_keys: logger.warn("assign_variables target obscures local_d name '%s'" % str(target)) - if is_local(target): + if is_temp_scalar(target) or is_throwaway(target): x = eval(expression, globals(), _locals_dict) - _locals_dict[target] = x - if trace_assigned_locals is not None: - trace_assigned_locals[uniquify_key(trace_assigned_locals, target)] = x + if not is_throwaway(target): + _locals_dict[target] = x + if trace_assigned_locals is not None: + trace_assigned_locals[uniquify_key(trace_assigned_locals, target)] = x continue try: From ce2a4b52171b47688d1ac88f17a30e7f41090e20 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 17 Sep 2018 08:14:29 -0400 Subject: [PATCH 009/122] add axis argument to pipeline.extend_table --- activitysim/core/pipeline.py | 16 ++++++++++++---- activitysim/core/test/test_tracing.py | 2 +- activitysim/core/tracing.py | 10 +++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 6646e0207..9664f7899 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -665,7 +665,7 @@ def replace_table(table_name, df): _PIPELINE.replaced_tables[table_name] = True -def extend_table(table_name, df): +def extend_table(table_name, df, axis=0): """ add new table or extend (add rows) to an existing table @@ -676,15 +676,23 @@ def extend_table(table_name, df): df : pandas DataFrame """ + assert axis in [0, 1] + if orca.is_table(table_name): table_df = orca.get_table(table_name).to_frame() - # don't expect indexes to overlap - assert len(table_df.index.intersection(df.index)) == 0 + if axis == 0: + # don't expect indexes to overlap + assert len(table_df.index.intersection(df.index)) == 0 + else: + # expect indexes be same + assert table_df.index.equals(df.index) + new_columns = [c for c in df.columns if c not in table_df.columns] + df = df[new_columns] # preserve existing column order - df = pd.concat([table_df, df], sort=False) + df = pd.concat([table_df, df], sort=False, axis=axis) replace_table(table_name, df) diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index 49c7df18f..dc29f1bff 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -239,7 +239,7 @@ def test_write_csv(capsys): print out # don't consume output - assert "write_df_csv object 'baddie' of unexpected type" in out + assert "unexpected type" in out close_handlers() diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index b945f0ad8..7fbe0e365 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -276,7 +276,7 @@ def write_df_csv(df, file_path, index_label=None, columns=None, column_labels=No df = df[columns] if not transpose: - df.to_csv(file_path, mode="a", index=True, header=True) + df.to_csv(file_path, mode="a", index=df.index.name is not None, header=True) return df_t = df.transpose() @@ -344,7 +344,10 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, assert len(file_name) > 0 - file_path = config.trace_file_path('%s.%s' % (file_name, CSV_FILE_TYPE)) + if not file_name.endswith(".%s" % CSV_FILE_TYPE): + file_name = '%s.%s' % (file_name, CSV_FILE_TYPE) + + file_path = config.trace_file_path(file_name) if os.path.isfile(file_path): logger.error("write_csv file exists %s %s" % (type(df).__name__, file_name)) @@ -360,7 +363,8 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, # logger.debug("dumping %s element dict to %s" % (df.shape[0], file_name)) write_series_csv(df, file_path, index_label, columns, column_labels) else: - logger.error("write_df_csv object '%s' of unexpected type: %s" % (file_name, type(df))) + logger.error("write_csv object for file_name '%s' of unexpected type: %s" % + (file_name, type(df))) def slice_ids(df, ids, column=None): From 0bfd858bc2f038be3978d7bab1004ea5bb6a30f2 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 17 Sep 2018 14:53:19 -0400 Subject: [PATCH 010/122] accept output_dir arg to tracing.delete_csv_files for 0.7 compatibility --- activitysim/core/tracing.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 7fbe0e365..d2ab9ad08 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -55,7 +55,7 @@ def print_elapsed_time(msg=None, t0=None, debug=False): return t1 -def delete_output_files(file_type): +def delete_output_files(file_type, output_dir=None): """ Delete files in output directory of specified type @@ -69,7 +69,9 @@ def delete_output_files(file_type): Nothing """ - output_dir = inject.get_injectable('output_dir') + if output_dir is None: + output_dir = inject.get_injectable('output_dir') + logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) for the_file in os.listdir(output_dir): @@ -82,20 +84,15 @@ def delete_output_files(file_type): print(e) -def delete_csv_files(): +def delete_csv_files(output_dir=None): """ - Delete CSV files - - Parameters - ---------- - output_dir: str - Directory of trace output CSVs + Delete CSV files in output_dir Returns ------- Nothing """ - delete_output_files(CSV_FILE_TYPE) + delete_output_files(CSV_FILE_TYPE, output_dir) def config_logger(custom_config_file=None, basic=False): From ece63a6e3816815a4908b06930beef0023cce6e2 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 20 Sep 2018 10:22:55 -0400 Subject: [PATCH 011/122] call read_model_spec from model code instead of injectable --- activitysim/abm/misc.py | 12 +++--- activitysim/abm/models/accessibility.py | 12 ++---- activitysim/abm/models/annotate_table.py | 2 +- .../abm/models/atwork_subtour_destination.py | 21 +++------- .../abm/models/atwork_subtour_frequency.py | 27 ++++--------- .../abm/models/atwork_subtour_scheduling.py | 11 ++--- activitysim/abm/models/auto_ownership.py | 11 ++--- activitysim/abm/models/cdap.py | 14 +++---- activitysim/abm/models/initialize.py | 4 +- .../abm/models/joint_tour_composition.py | 11 ++--- .../abm/models/joint_tour_destination.py | 30 +++++--------- .../abm/models/joint_tour_frequency.py | 26 ++++-------- .../abm/models/joint_tour_participation.py | 40 ++++++++++--------- .../abm/models/joint_tour_scheduling.py | 10 +---- .../abm/models/mandatory_scheduling.py | 16 ++------ .../abm/models/mandatory_tour_frequency.py | 26 ++++-------- .../abm/models/non_mandatory_destination.py | 13 ++---- .../abm/models/non_mandatory_scheduling.py | 11 ++--- .../models/non_mandatory_tour_frequency.py | 31 ++++++-------- activitysim/abm/models/school_location.py | 19 +++------ activitysim/abm/models/stop_frequency.py | 14 +++---- activitysim/abm/models/trip_destination.py | 18 +++++---- activitysim/abm/models/trip_mode_choice.py | 17 ++++---- activitysim/abm/models/trip_purpose.py | 3 +- activitysim/abm/models/trip_scheduling.py | 10 +---- activitysim/abm/models/util/expressions.py | 4 +- activitysim/abm/models/util/mode.py | 12 +++--- .../cdap_indiv_and_hhsize1.csv | 0 .../cdap_interaction_coefficients.csv | 0 activitysim/abm/models/util/test/test_cdap.py | 18 ++++++--- activitysim/abm/models/util/trip_mode.py | 31 -------------- activitysim/abm/models/workplace_location.py | 23 ++++------- activitysim/abm/tables/size_terms.py | 5 ++- activitysim/abm/tables/time_windows.py | 5 ++- activitysim/core/assign.py | 5 +++ activitysim/core/config.py | 33 ++++++++++----- activitysim/core/simulate.py | 20 ++++++---- activitysim/core/test/test_simulate.py | 4 +- activitysim/core/tracing.py | 5 +-- example_multi/extensions/models.py | 5 +-- 40 files changed, 222 insertions(+), 357 deletions(-) rename activitysim/abm/models/util/test/{data => configs}/cdap_indiv_and_hhsize1.csv (100%) rename activitysim/abm/models/util/test/{data => configs}/cdap_interaction_coefficients.csv (100%) delete mode 100644 activitysim/abm/models/util/trip_mode.py diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index 5b177fd5f..4f34bce23 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -9,7 +9,7 @@ import pandas as pd import yaml -from activitysim.core import pipeline +from activitysim.core import config from activitysim.core import inject # FIXME @@ -29,18 +29,18 @@ def households_sample_size(settings, override_hh_ids): @inject.injectable(cache=True) -def override_hh_ids(settings, configs_dir): +def override_hh_ids(settings): hh_ids_filename = settings.get('hh_ids', None) if hh_ids_filename is None: return None - f = os.path.join(configs_dir, hh_ids_filename) - if not os.path.exists(f): - logger.error('hh_ids file name specified in settings, but file not found: %s' % f) + file_path = config.config_file_path(hh_ids_filename, mandatory=False) + if not file_path: + logger.error("hh_ids file name '%s' specified in settings not found: %s" % hh_ids_filename) return None - df = pd.read_csv(f, comment='#') + df = pd.read_csv(file_path, comment='#') if 'household_id' not in df.columns: logger.error("No 'household_id' column in hh_ids file %s" % hh_ids_filename) diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index c623dcd0a..5249054f5 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -89,15 +89,8 @@ def __getitem__(self, key): return data.flatten() -@inject.injectable() -def accessibility_spec(configs_dir): - f = os.path.join(configs_dir, 'accessibility.csv') - return assign.read_assignment_spec(f) - - @inject.step() -def compute_accessibility(accessibility, accessibility_spec, - skim_dict, land_use, trace_od): +def compute_accessibility(accessibility, skim_dict, land_use, trace_od): """ Compute accessibility for each zone in land use file using expressions from accessibility_spec @@ -117,6 +110,7 @@ def compute_accessibility(accessibility, accessibility_spec, trace_label = 'compute_accessibility' model_settings = config.read_model_settings('accessibility.yaml') + assignment_spec = assign.read_assignment_spec(config.config_file_path('accessibility.csv')) accessibility_df = accessibility.to_frame() @@ -173,7 +167,7 @@ def compute_accessibility(accessibility, accessibility_spec, locals_d.update(constants) results, trace_results, trace_assigned_locals \ - = assign.assign_variables(accessibility_spec, od_df, locals_d, trace_rows=trace_od_rows) + = assign.assign_variables(assignment_spec, od_df, locals_d, trace_rows=trace_od_rows) for column in results.columns: data = np.asanyarray(results[column]) diff --git a/activitysim/abm/models/annotate_table.py b/activitysim/abm/models/annotate_table.py index b14780b76..4077dd25b 100644 --- a/activitysim/abm/models/annotate_table.py +++ b/activitysim/abm/models/annotate_table.py @@ -21,7 +21,7 @@ @inject.step() -def annotate_table(configs_dir): +def annotate_table(): # model_settings name should have been provided as a step argument model_name = inject.get_step_arg('model_name') diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index 6928a1dcf..3f1ca85da 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -30,21 +30,17 @@ DUMP = False -@inject.injectable() -def atwork_subtour_destination_sample_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'atwork_subtour_destination_sample.csv') - - @inject.step() def atwork_subtour_destination_sample(tours, persons_merged, - atwork_subtour_destination_sample_spec, skim_dict, land_use, size_terms, chunk_size, trace_hh_id): trace_label = 'atwork_subtour_location_sample' model_settings = config.read_model_settings('atwork_subtour_destination.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('atwork_subtour_destination_sample.csv')) persons_merged = persons_merged.to_frame() @@ -87,7 +83,7 @@ def atwork_subtour_destination_sample(tours, alternatives, sample_size=sample_size, alt_col_name=alt_dest_col_name, - spec=atwork_subtour_destination_sample_spec, + spec=model_spec, skims=skims, locals_d=locals_d, chunk_size=chunk_size, @@ -101,7 +97,7 @@ def atwork_subtour_destination_sample(tours, @inject.step() def atwork_subtour_destination_logsums(persons_merged, skim_dict, skim_stack, - configs_dir, chunk_size, trace_hh_id): + chunk_size, trace_hh_id): """ add logsum column to existing workplace_location_sample able @@ -169,15 +165,9 @@ def atwork_subtour_destination_logsums(persons_merged, inject.add_column("atwork_subtour_destination_sample", "mode_choice_logsum", logsums) -@inject.injectable() -def atwork_subtour_destination_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'atwork_subtour_destination.csv') - - @inject.step() def atwork_subtour_destination_simulate(tours, persons_merged, - atwork_subtour_destination_spec, skim_dict, land_use, size_terms, chunk_size, trace_hh_id): @@ -197,6 +187,7 @@ def atwork_subtour_destination_simulate(tours, destination_sample = destination_sample.to_frame() model_settings = config.read_model_settings('atwork_subtour_destination.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('atwork_subtour_destination.csv')) tours = tours.to_frame() subtours = tours[tours.tour_category == 'atwork'] @@ -245,7 +236,7 @@ def atwork_subtour_destination_simulate(tours, choices = interaction_sample_simulate( choosers, alternatives, - spec=atwork_subtour_destination_spec, + spec=model_spec, choice_column=alt_dest_col_name, skims=skims, locals_d=locals_d, diff --git a/activitysim/abm/models/atwork_subtour_frequency.py b/activitysim/abm/models/atwork_subtour_frequency.py index 6b3587e6a..330a77b3b 100644 --- a/activitysim/abm/models/atwork_subtour_frequency.py +++ b/activitysim/abm/models/atwork_subtour_frequency.py @@ -23,20 +23,6 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def atwork_subtour_frequency_spec(configs_dir): - return read_model_spec(configs_dir, 'atwork_subtour_frequency.csv') - - -@inject.injectable() -def atwork_subtour_frequency_alternatives(configs_dir): - # alt file for building tours even though simulation is simple_simulate not interaction_simulate - f = os.path.join(configs_dir, 'atwork_subtour_frequency_alternatives.csv') - df = pd.read_csv(f, comment='#') - df.set_index('alt', inplace=True) - return df - - def add_null_results(trace_label, tours): logger.info("Skipping %s: add_null_results" % trace_label) tours['atwork_subtour_frequency'] = np.nan @@ -46,8 +32,6 @@ def add_null_results(trace_label, tours): @inject.step() def atwork_subtour_frequency(tours, persons_merged, - atwork_subtour_frequency_spec, - atwork_subtour_frequency_alternatives, chunk_size, trace_hh_id): """ @@ -59,6 +43,11 @@ def atwork_subtour_frequency(tours, trace_label = 'atwork_subtour_frequency' model_settings = config.read_model_settings('atwork_subtour_frequency.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('atwork_subtour_frequency.csv')) + + alternatives = simulate.read_model_alts( + config.config_file_path('atwork_subtour_frequency_alternatives.csv'), set_index='alt') tours = tours.to_frame() @@ -81,7 +70,7 @@ def atwork_subtour_frequency(tours, choices = simulate.simple_simulate( choosers=work_tours, - spec=atwork_subtour_frequency_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, @@ -89,7 +78,7 @@ def atwork_subtour_frequency(tours, trace_choice_name='atwork_subtour_frequency') # convert indexes to alternative names - choices = pd.Series(atwork_subtour_frequency_spec.columns[choices.values], index=choices.index) + choices = pd.Series(model_spec.columns[choices.values], index=choices.index) tracing.print_summary('atwork_subtour_frequency', choices, value_counts=True) @@ -102,7 +91,7 @@ def atwork_subtour_frequency(tours, work_tours = tours[tours.tour_type == 'work'] assert not work_tours.atwork_subtour_frequency.isnull().any() - subtours = process_atwork_subtours(work_tours, atwork_subtour_frequency_alternatives) + subtours = process_atwork_subtours(work_tours, alternatives) tours = pipeline.extend_table("tours", subtours) diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index 0c3adcde3..27769137a 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -9,7 +9,7 @@ from activitysim.core.simulate import read_model_spec from activitysim.core.interaction_simulate import interaction_simulate -from activitysim.core import simulate as asim +from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import config @@ -26,17 +26,11 @@ DUMP = True -@inject.injectable() -def tour_scheduling_atwork_spec(configs_dir): - return asim.read_model_spec(configs_dir, 'tour_scheduling_atwork.csv') - - @inject.step() def atwork_subtour_scheduling( tours, persons_merged, tdd_alts, - tour_scheduling_atwork_spec, skim_dict, chunk_size, trace_hh_id): @@ -46,6 +40,7 @@ def atwork_subtour_scheduling( trace_label = 'atwork_subtour_scheduling' model_settings = config.read_model_settings('tour_scheduling_atwork.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_atwork.csv')) persons_merged = persons_merged.to_frame() @@ -80,7 +75,7 @@ def atwork_subtour_scheduling( parent_tours, subtours, persons_merged, - tdd_alts, tour_scheduling_atwork_spec, + tdd_alts, model_spec, constants=constants, chunk_size=chunk_size, trace_label=trace_label) diff --git a/activitysim/abm/models/auto_ownership.py b/activitysim/abm/models/auto_ownership.py index c42491fe0..5a36a6e36 100644 --- a/activitysim/abm/models/auto_ownership.py +++ b/activitysim/abm/models/auto_ownership.py @@ -14,16 +14,9 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def auto_ownership_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'auto_ownership.csv') - - @inject.step() def auto_ownership_simulate(households, households_merged, - auto_ownership_spec, - configs_dir, chunk_size, trace_hh_id): """ @@ -35,12 +28,14 @@ def auto_ownership_simulate(households, logger.info("Running %s with %d households" % (trace_label, len(households_merged))) + model_spec = simulate.read_model_spec(config.config_file_path('auto_ownership.csv')) + nest_spec = config.get_logit_model_settings(model_settings) constants = config.get_model_constants(model_settings) choices = simulate.simple_simulate( choosers=households_merged.to_frame(), - spec=auto_ownership_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, diff --git a/activitysim/abm/models/cdap.py b/activitysim/abm/models/cdap.py index 0dd654dcb..6dbbfab63 100644 --- a/activitysim/abm/models/cdap.py +++ b/activitysim/abm/models/cdap.py @@ -6,7 +6,7 @@ import pandas as pd -from activitysim.core import simulate as asim +from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import config @@ -19,25 +19,25 @@ @inject.injectable() -def cdap_indiv_spec(configs_dir): +def cdap_indiv_spec(): """ spec to compute the activity utilities for each individual hh member with no interactions with other household members taken into account """ - return asim.read_model_spec(configs_dir, 'cdap_indiv_and_hhsize1.csv') + return simulate.read_model_spec(config.config_file_path('cdap_indiv_and_hhsize1.csv')) @inject.injectable() -def cdap_interaction_coefficients(configs_dir): +def cdap_interaction_coefficients(): """ Rules and coefficients for generating interaction specs for different household sizes """ - f = os.path.join(configs_dir, 'cdap_interaction_coefficients.csv') + f = config.config_file_path('cdap_interaction_coefficients.csv') return pd.read_csv(f, comment='#') @inject.injectable() -def cdap_fixed_relative_proportions(configs_dir): +def cdap_fixed_relative_proportions(): """ spec to compute/specify the relative proportions of each activity (M, N, H) that should be used to choose activities for additional household members @@ -47,7 +47,7 @@ def cdap_fixed_relative_proportions(configs_dir): EXCEPT that the values computed are relative proportions, not utilities (i.e. values are not exponentiated before being normalized to probabilities summing to 1.0) """ - return asim.read_model_spec(configs_dir, 'cdap_fixed_relative_proportions.csv') + return simulate.read_model_spec(config.config_file_path('cdap_fixed_relative_proportions.csv')) @inject.step() diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 024dd0b1a..dce9f1a8d 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -57,7 +57,7 @@ def annotate_tables(model_settings, trace_label): @inject.step() -def initialize_landuse(configs_dir): +def initialize_landuse(): trace_label = 'initialize_landuse' @@ -75,7 +75,7 @@ def initialize_landuse(configs_dir): @inject.step() -def initialize_households(configs_dir): +def initialize_households(): trace_label = 'initialize_households' diff --git a/activitysim/abm/models/joint_tour_composition.py b/activitysim/abm/models/joint_tour_composition.py index a29ae1808..fd6f30b28 100644 --- a/activitysim/abm/models/joint_tour_composition.py +++ b/activitysim/abm/models/joint_tour_composition.py @@ -23,11 +23,6 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def joint_tour_composition_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'joint_tour_composition.csv') - - def add_null_results(trace_label, tours): logger.info("Skipping %s: add_null_results" % trace_label) tours['composition'] = np.nan @@ -37,7 +32,6 @@ def add_null_results(trace_label, tours): @inject.step() def joint_tour_composition( tours, households, persons, - joint_tour_composition_spec, chunk_size, trace_hh_id): """ @@ -46,6 +40,7 @@ def joint_tour_composition( trace_label = 'joint_tour_composition' model_settings = config.read_model_settings('joint_tour_composition.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_composition.csv')) tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] @@ -89,7 +84,7 @@ def joint_tour_composition( choices = simulate.simple_simulate( choosers=joint_tours_merged, - spec=joint_tour_composition_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, @@ -97,7 +92,7 @@ def joint_tour_composition( trace_choice_name='composition') # convert indexes to alternative names - choices = pd.Series(joint_tour_composition_spec.columns[choices.values], index=choices.index) + choices = pd.Series(model_spec.columns[choices.values], index=choices.index) # add composition column to tours joint_tours['composition'] = choices diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index 2829ffd45..49cb0f81e 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -16,6 +16,7 @@ from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline +from activitysim.core import simulate from activitysim.core.util import reindex from activitysim.core.util import assign_in_place @@ -40,20 +41,13 @@ ]) -@inject.injectable() -def joint_tour_destination_sample_spec(configs_dir): - # - tour types are subset of non_mandatory tour types and use same expressions - return read_model_spec(configs_dir, 'non_mandatory_tour_destination_sample.csv') - - @inject.step() def joint_tour_destination_sample( tours, households_merged, - joint_tour_destination_sample_spec, skim_dict, land_use, size_terms, - configs_dir, chunk_size, trace_hh_id): + chunk_size, trace_hh_id): """ Chooses a sample of destinations from all possible tour destinations by choosing times from among destination alternatives. @@ -88,7 +82,6 @@ def joint_tour_destination_sample( joint_tour_destination_sample_spec land_use size_terms - configs_dir chunk_size trace_hh_id @@ -101,6 +94,8 @@ def joint_tour_destination_sample( trace_label = 'joint_tour_destination_sample' model_settings = config.read_model_settings('joint_tour_destination.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('non_mandatory_tour_destination_sample.csv')) joint_tours = tours.to_frame() joint_tours = joint_tours[joint_tours.tour_category == 'joint'] @@ -173,7 +168,7 @@ def joint_tour_destination_sample( alternatives_segment, sample_size=sample_size, alt_col_name=alt_dest_col_name, - spec=joint_tour_destination_sample_spec[[tour_type]], + spec=model_spec[[tour_type]], skims=skims, locals_d=locals_d, chunk_size=chunk_size, @@ -278,20 +273,13 @@ def joint_tour_destination_logsums( label="joint_tour_destination_logsums") -@inject.injectable() -def joint_tour_destination_spec(configs_dir): - # - tour types are subset of non_mandatory tour types and use same expressions - return read_model_spec(configs_dir, 'non_mandatory_tour_destination.csv') - - @inject.step() def joint_tour_destination_simulate( tours, households_merged, - joint_tour_destination_spec, skim_dict, land_use, size_terms, - configs_dir, chunk_size, trace_hh_id): + chunk_size, trace_hh_id): """ choose a joint tour destination from amont the destination sample alternatives (annotated with logsums) and add destination TAZ column to joint_tours table @@ -307,6 +295,10 @@ def joint_tour_destination_simulate( model_settings = config.read_model_settings('joint_tour_destination.yaml') + # - tour types are subset of non_mandatory tour types and use same expressions + model_spec = simulate.read_model_spec( + config.config_file_path('non_mandatory_tour_destination.csv')) + destination_sample = destination_sample.to_frame() tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] @@ -374,7 +366,7 @@ def joint_tour_destination_simulate( choices = interaction_sample_simulate( choosers_segment, alts_segment, - spec=joint_tour_destination_spec[[tour_type]], + spec=model_spec[[tour_type]], choice_column=alt_dest_col_name, skims=skims, locals_d=locals_d, diff --git a/activitysim/abm/models/joint_tour_frequency.py b/activitysim/abm/models/joint_tour_frequency.py index 1e45051c9..afa9e9386 100644 --- a/activitysim/abm/models/joint_tour_frequency.py +++ b/activitysim/abm/models/joint_tour_frequency.py @@ -21,25 +21,9 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def joint_tour_frequency_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'joint_tour_frequency.csv') - - -@inject.injectable() -def joint_tour_frequency_alternatives(configs_dir): - # alt file for building tours even though simulation is simple_simulate not interaction_simulate - f = os.path.join(configs_dir, 'joint_tour_frequency_alternatives.csv') - df = pd.read_csv(f, comment='#') - df.set_index('alt', inplace=True) - return df - - @inject.step() def joint_tour_frequency( households, persons, - joint_tour_frequency_spec, - joint_tour_frequency_alternatives, chunk_size, trace_hh_id): """ @@ -48,6 +32,10 @@ def joint_tour_frequency( """ trace_label = 'joint_tour_frequency' model_settings = config.read_model_settings('joint_tour_frequency.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_frequency.csv')) + + alternatives = simulate.read_model_alts( + config.config_file_path('joint_tour_frequency_alternatives.csv'), set_index='alt') # - only interested in households with more than one cdap travel_active person households = households.to_frame() @@ -83,7 +71,7 @@ def joint_tour_frequency( choices = simulate.simple_simulate( choosers=multi_person_households, - spec=joint_tour_frequency_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, @@ -91,7 +79,7 @@ def joint_tour_frequency( trace_choice_name='joint_tour_frequency') # convert indexes to alternative names - choices = pd.Series(joint_tour_frequency_spec.columns[choices.values], index=choices.index) + choices = pd.Series(model_spec.columns[choices.values], index=choices.index) # - create joint_tours based on joint_tour_frequency choices @@ -105,7 +93,7 @@ def joint_tour_frequency( temp_point_persons = temp_point_persons[['person_id', 'home_taz']] joint_tours = \ - process_joint_tours(choices, joint_tour_frequency_alternatives, temp_point_persons) + process_joint_tours(choices, alternatives, temp_point_persons) tours = pipeline.extend_table("tours", joint_tours) diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 7c5b84b2f..42bb26c9c 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -23,11 +23,6 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def joint_tour_participation_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'joint_tour_participation.csv') - - def joint_tour_participation_candidates(joint_tours, persons_merged): # - only interested in persons from households with joint_tours @@ -204,18 +199,31 @@ def participants_chooser(probs, choosers, spec, trace_label): return choices, rands -def add_null_results(trace_label): +def annotate_jtp(model_settings, trace_label): + + # - annotate persons + persons = inject.get_table('persons').to_frame() + expressions.assign_columns( + df=persons, + model_settings=model_settings.get('annotate_persons'), + trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) + pipeline.replace_table("persons", persons) + + +def add_null_results(model_settings, trace_label): logger.info("Skipping %s: joint tours" % trace_label) # participants table is used downstream in non-joint tour expressions participants = pd.DataFrame(columns=['person_id']) participants.index.name = 'participant_id' pipeline.replace_table("joint_tour_participants", participants) + # - run annotations + annotate_jtp(model_settings, trace_label) + @inject.step() def joint_tour_participation( tours, persons_merged, - joint_tour_participation_spec, chunk_size, trace_hh_id): """ @@ -223,13 +231,14 @@ def joint_tour_participation( """ trace_label = 'joint_tour_participation' model_settings = config.read_model_settings('joint_tour_participation.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_participation.csv')) tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] # - if no joint tours if joint_tours.shape[0] == 0: - add_null_results(trace_label) + add_null_results(model_settings, trace_label) return persons_merged = persons_merged.to_frame() @@ -264,7 +273,7 @@ def joint_tour_participation( choices = simulate.simple_simulate( choosers=candidates, - spec=joint_tour_participation_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, @@ -274,9 +283,9 @@ def joint_tour_participation( # choice is boolean (participate or not) choice_col = model_settings.get('participation_choice', 'participate') - assert choice_col in joint_tour_participation_spec.columns, \ + assert choice_col in model_spec.columns, \ "couldn't find participation choice column '%s' in spec" - PARTICIPATE_CHOICE = joint_tour_participation_spec.columns.get_loc(choice_col) + PARTICIPATE_CHOICE = model_spec.columns.get_loc(choice_col) participate = (choices == PARTICIPATE_CHOICE) @@ -312,13 +321,8 @@ def joint_tour_participation( pipeline.replace_table("tours", tours) - # - annotate persons - persons = inject.get_table('persons').to_frame() - expressions.assign_columns( - df=persons, - model_settings=model_settings.get('annotate_persons'), - trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) - pipeline.replace_table("persons", persons) + # - run annotations + annotate_jtp(model_settings, trace_label) if trace_hh_id: tracing.trace_df(participants, diff --git a/activitysim/abm/models/joint_tour_scheduling.py b/activitysim/abm/models/joint_tour_scheduling.py index 500a2b14b..9679090a8 100644 --- a/activitysim/abm/models/joint_tour_scheduling.py +++ b/activitysim/abm/models/joint_tour_scheduling.py @@ -20,18 +20,11 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def joint_tour_scheduling_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'tour_scheduling_joint.csv') - - @inject.step() def joint_tour_scheduling( tours, persons_merged, tdd_alts, - joint_tour_scheduling_spec, - configs_dir, chunk_size, trace_hh_id): """ @@ -39,6 +32,7 @@ def joint_tour_scheduling( """ trace_label = 'joint_tour_scheduling' model_settings = config.read_model_settings('joint_tour_scheduling.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_joint.csv')) tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] @@ -86,7 +80,7 @@ def joint_tour_scheduling( joint_tours, joint_tour_participants, persons_merged, tdd_alts, - spec=joint_tour_scheduling_spec, + spec=model_spec, constants=locals_d, chunk_size=chunk_size, trace_label=trace_label) diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index 008e6510b..ed4096b9b 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -23,22 +23,10 @@ DUMP = False -@inject.injectable() -def tour_scheduling_work_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'tour_scheduling_work.csv') - - -@inject.injectable() -def tour_scheduling_school_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'tour_scheduling_school.csv') - - @inject.step() def mandatory_tour_scheduling(tours, persons_merged, tdd_alts, - tour_scheduling_work_spec, - tour_scheduling_school_spec, chunk_size, trace_hh_id): """ @@ -46,6 +34,8 @@ def mandatory_tour_scheduling(tours, """ trace_label = 'mandatory_tour_scheduling' model_settings = config.read_model_settings('mandatory_tour_scheduling.yaml') + work_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_work.csv')) + school_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_school.csv')) tours = tours.to_frame() persons_merged = persons_merged.to_frame() @@ -62,7 +52,7 @@ def mandatory_tour_scheduling(tours, tdd_choices = vectorize_tour_scheduling( mandatory_tours, persons_merged, tdd_alts, - spec={'work': tour_scheduling_work_spec, 'school': tour_scheduling_school_spec}, + spec={'work': work_spec, 'school': school_spec}, constants=model_constants, chunk_size=chunk_size, trace_label=trace_label) diff --git a/activitysim/abm/models/mandatory_tour_frequency.py b/activitysim/abm/models/mandatory_tour_frequency.py index f665ab0b4..bed55f8dc 100644 --- a/activitysim/abm/models/mandatory_tour_frequency.py +++ b/activitysim/abm/models/mandatory_tour_frequency.py @@ -19,20 +19,6 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def mandatory_tour_frequency_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'mandatory_tour_frequency.csv') - - -@inject.injectable() -def mandatory_tour_frequency_alternatives(configs_dir): - # alt file for building tours even though simulation is simple_simulate not interaction_simulate - f = os.path.join(configs_dir, 'mandatory_tour_frequency_alternatives.csv') - df = pd.read_csv(f, comment='#') - df.set_index('alt', inplace=True) - return df - - def add_null_results(trace_label, mandatory_tour_frequency_settings): logger.info("Skipping %s: add_null_results" % trace_label) @@ -56,8 +42,6 @@ def add_null_results(trace_label, mandatory_tour_frequency_settings): @inject.step() def mandatory_tour_frequency(persons_merged, - mandatory_tour_frequency_spec, - mandatory_tour_frequency_alternatives, chunk_size, trace_hh_id): """ @@ -67,6 +51,10 @@ def mandatory_tour_frequency(persons_merged, trace_label = 'mandatory_tour_frequency' model_settings = config.read_model_settings('mandatory_tour_frequency.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('mandatory_tour_frequency.csv')) + alternatives = simulate.read_model_alts( + config.config_file_path('mandatory_tour_frequency_alternatives.csv'), set_index='alt') choosers = persons_merged.to_frame() # filter based on results of CDAP @@ -95,7 +83,7 @@ def mandatory_tour_frequency(persons_merged, choices = simulate.simple_simulate( choosers=choosers, - spec=mandatory_tour_frequency_spec, + spec=model_spec, nest_spec=nest_spec, locals_d=constants, chunk_size=chunk_size, @@ -104,7 +92,7 @@ def mandatory_tour_frequency(persons_merged, # convert indexes to alternative names choices = pd.Series( - mandatory_tour_frequency_spec.columns[choices.values], + model_spec.columns[choices.values], index=choices.index).reindex(persons_merged.local.index) # - create mandatory tours @@ -116,7 +104,7 @@ def mandatory_tour_frequency(persons_merged, choosers['mandatory_tour_frequency'] = choices mandatory_tours = process_mandatory_tours( persons=choosers, - mandatory_tour_frequency_alts=mandatory_tour_frequency_alternatives + mandatory_tour_frequency_alts=alternatives ) tours = pipeline.extend_table("tours", mandatory_tours) diff --git a/activitysim/abm/models/non_mandatory_destination.py b/activitysim/abm/models/non_mandatory_destination.py index 7a3f0417d..f46f10ddf 100644 --- a/activitysim/abm/models/non_mandatory_destination.py +++ b/activitysim/abm/models/non_mandatory_destination.py @@ -12,25 +12,19 @@ from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline +from activitysim.core import simulate -from .util import expressions from activitysim.core.util import assign_in_place from .util.tour_destination import tour_destination_size_terms logger = logging.getLogger(__name__) -@inject.injectable() -def non_mandatory_tour_destination_spec(configs_dir): - return read_model_spec(configs_dir, 'non_mandatory_tour_destination_sample.csv') - - @inject.step() def non_mandatory_tour_destination( tours, persons_merged, skim_dict, - non_mandatory_tour_destination_spec, land_use, size_terms, chunk_size, trace_hh_id): @@ -43,12 +37,13 @@ def non_mandatory_tour_destination( trace_label = 'non_mandatory_tour_destination' model_settings = config.read_model_settings('non_mandatory_tour_destination.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('non_mandatory_tour_destination_sample.csv')) tours = tours.to_frame() persons_merged = persons_merged.to_frame() alternatives = tour_destination_size_terms(land_use, size_terms, 'non_mandatory') - spec = non_mandatory_tour_destination_spec # choosers are tours - in a sense tours are choosing their destination non_mandatory_tours = tours[tours.tour_category == 'non_mandatory'] @@ -105,7 +100,7 @@ def non_mandatory_tour_destination( choices = interaction_simulate( segment, alternatives_segment, - spec[[kludge_name]], + model_spec[[kludge_name]], skims=skims, locals_d=locals_d, sample_size=sample_size, diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index d6c28cebe..eb6e4638a 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -12,6 +12,7 @@ from activitysim.core import inject from activitysim.core import pipeline from activitysim.core import timetable as tt +from activitysim.core import simulate from .util import expressions from .util.vectorize_tour_scheduling import vectorize_tour_scheduling @@ -22,16 +23,10 @@ DUMP = False -@inject.injectable() -def tour_scheduling_nonmandatory_spec(configs_dir): - return asim.read_model_spec(configs_dir, 'tour_scheduling_nonmandatory.csv') - - @inject.step() def non_mandatory_tour_scheduling(tours, persons_merged, tdd_alts, - tour_scheduling_nonmandatory_spec, chunk_size, trace_hh_id): """ @@ -40,6 +35,8 @@ def non_mandatory_tour_scheduling(tours, trace_label = 'non_mandatory_tour_scheduling' model_settinsg = config.read_model_settings('non_mandatory_tour_scheduling.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('tour_scheduling_nonmandatory.csv')) tours = tours.to_frame() persons_merged = persons_merged.to_frame() @@ -66,7 +63,7 @@ def non_mandatory_tour_scheduling(tours, tdd_choices = vectorize_tour_scheduling( non_mandatory_tours, persons_merged, - tdd_alts, tour_scheduling_nonmandatory_spec, + tdd_alts, model_spec, constants=constants, chunk_size=chunk_size, trace_label=trace_label) diff --git a/activitysim/abm/models/non_mandatory_tour_frequency.py b/activitysim/abm/models/non_mandatory_tour_frequency.py index bb63310bf..400768167 100644 --- a/activitysim/abm/models/non_mandatory_tour_frequency.py +++ b/activitysim/abm/models/non_mandatory_tour_frequency.py @@ -13,6 +13,7 @@ from activitysim.core import pipeline from activitysim.core import config from activitysim.core import inject +from activitysim.core import simulate from .util import expressions from .util.overlap import person_max_window @@ -24,22 +25,8 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def non_mandatory_tour_frequency_spec(configs_dir): - return read_model_spec(configs_dir, 'non_mandatory_tour_frequency.csv') - - -@inject.injectable() -def non_mandatory_tour_frequency_alts(configs_dir): - f = os.path.join(configs_dir, 'non_mandatory_tour_frequency_alternatives.csv') - df = pd.read_csv(f, comment='#') - return df - - @inject.step() def non_mandatory_tour_frequency(persons, persons_merged, - non_mandatory_tour_frequency_alts, - non_mandatory_tour_frequency_spec, chunk_size, trace_hh_id): @@ -52,11 +39,17 @@ def non_mandatory_tour_frequency(persons, persons_merged, trace_label = 'non_mandatory_tour_frequency' model_settings = config.read_model_settings('non_mandatory_tour_frequency.yaml') + model_spec = simulate.read_model_spec( + config.config_file_path('non_mandatory_tour_frequency.csv')) + + alternatives = simulate.read_model_alts( + config.config_file_path('non_mandatory_tour_frequency_alternatives.csv'), + set_index=None) choosers = persons_merged.to_frame() # FIXME kind of tacky both that we know to add this here and del it below - non_mandatory_tour_frequency_alts['tot_tours'] = non_mandatory_tour_frequency_alts.sum(axis=1) + alternatives['tot_tours'] = alternatives.sum(axis=1) # - preprocessor preprocessor_settings = model_settings.get('preprocessor', None) @@ -86,7 +79,7 @@ def non_mandatory_tour_frequency(persons, persons_merged, name = PTYPE_NAME[ptype] # pick the spec column for the segment - spec = non_mandatory_tour_frequency_spec[[name]] + spec = model_spec[[name]] # drop any zero-valued rows spec = spec[spec[name] != 0] @@ -95,7 +88,7 @@ def non_mandatory_tour_frequency(persons, persons_merged, choices = interaction_simulate( segment, - non_mandatory_tour_frequency_alts, + alternatives, spec=spec, locals_d=constants, chunk_size=chunk_size, @@ -119,10 +112,10 @@ def non_mandatory_tour_frequency(persons, persons_merged, Now we create a "tours" table which has one row per tour that has been generated (and the person id it is associated with) """ - del non_mandatory_tour_frequency_alts['tot_tours'] # del tot_tours column we added above + del alternatives['tot_tours'] # del tot_tours column we added above non_mandatory_tours = process_non_mandatory_tours( persons[~persons.mandatory_tour_frequency.isnull()], - non_mandatory_tour_frequency_alts, + alternatives, ) tours = pipeline.extend_table("tours", non_mandatory_tours) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 76352af80..96dc07987 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -35,20 +35,9 @@ SCHOOL_TYPE_ID = OrderedDict([('university', 1), ('highschool', 2), ('gradeschool', 3)]) -@inject.injectable() -def school_location_sample_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'school_location_sample.csv') - - -@inject.injectable() -def school_location_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'school_location.csv') - - @inject.step() def school_location_sample( persons_merged, - school_location_sample_spec, skim_dict, land_use, size_terms, chunk_size, @@ -74,6 +63,8 @@ def school_location_sample( trace_label = 'school_location_sample' model_settings = config.read_model_settings('school_location.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('school_location_sample.csv')) + choosers = persons_merged.to_frame() # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] @@ -123,7 +114,7 @@ def school_location_sample( alternatives_segment, sample_size=sample_size, alt_col_name=alt_dest_col_name, - spec=school_location_sample_spec[[school_type]], + spec=model_spec[[school_type]], skims=skims, locals_d=locals_d, chunk_size=chunk_size, @@ -233,7 +224,6 @@ def school_location_logsums( @inject.step() def school_location_simulate(persons_merged, persons, school_location_sample, - school_location_spec, skim_dict, land_use, size_terms, chunk_size, @@ -244,6 +234,7 @@ def school_location_simulate(persons_merged, persons, """ trace_label = 'school_location_simulate' model_settings = config.read_model_settings('school_location.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('school_location.csv')) NO_SCHOOL_TAZ = -1 @@ -296,7 +287,7 @@ def school_location_simulate(persons_merged, persons, choices = interaction_sample_simulate( choosers_segment, alts_segment, - spec=school_location_spec[[school_type]], + spec=model_spec[[school_type]], choice_column=alt_dest_col_name, skims=skims, locals_d=locals_d, diff --git a/activitysim/abm/models/stop_frequency.py b/activitysim/abm/models/stop_frequency.py index 3240fe085..9c75de605 100644 --- a/activitysim/abm/models/stop_frequency.py +++ b/activitysim/abm/models/stop_frequency.py @@ -22,20 +22,16 @@ def get_stop_frequency_spec(tour_type): - configs_dir = inject.get_injectable('configs_dir') file_name = 'stop_frequency_%s.csv' % tour_type - - if not os.path.exists(os.path.join(configs_dir, file_name)): - return None - - return simulate.read_model_spec(configs_dir, file_name) + file_path = config.config_file_path(file_name) + return simulate.read_model_spec(file_path) @inject.injectable() -def stop_frequency_alts(configs_dir): +def stop_frequency_alts(): # alt file for building trips even though simulation is simple_simulate not interaction_simulate - f = os.path.join(configs_dir, 'stop_frequency_alternatives.csv') - df = pd.read_csv(f, comment='#') + file_path = config.config_file_path('stop_frequency_alternatives.csv') + df = pd.read_csv(file_path, comment='#') df.set_index('alt', inplace=True) return df diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index c554ed5ad..6963c3840 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -22,9 +22,7 @@ from .util import expressions from .util.mode import annotate_preprocessors -from .util.trip_mode import trip_mode_choice_spec -from .util.trip_mode import trip_mode_choice_coeffecients_spec -from activitysim.core.assign import evaluate_constants +from activitysim.core import assign from .util.tour_destination import tour_destination_size_terms from activitysim.core.skim import DataFrameMatrix @@ -41,8 +39,9 @@ def get_spec_for_purpose(model_settings, spec_name, purpose): - configs_dir = inject.get_injectable('configs_dir') - omnibus_spec = simulate.read_model_spec(configs_dir, model_settings[spec_name]) + + omnibus_spec = simulate.read_model_spec(config.config_file_path(model_settings[spec_name])) + spec = omnibus_spec[[purpose]] # might as well ignore any spec rows with 0 utility @@ -127,7 +126,7 @@ def compute_ood_logsums( trace_label) nest_spec = config.get_logit_model_settings(logsum_settings) - logsum_spec = trip_mode_choice_spec(logsum_settings) + logsum_spec = simulate.read_model_spec(config.config_file_path(logsum_settings['SPEC'])) logsums = simulate.simple_simulate_logsums( choosers, @@ -187,11 +186,14 @@ def compute_logsums( assert choosers.index.equals(destination_sample.index) logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - omnibus_coefficient_spec = trip_mode_choice_coeffecients_spec(logsum_settings) + + omnibus_coefficient_spec = \ + assign.read_constant_spec(config.config_file_path(logsum_settings['COEFFS'])) + coefficient_spec = omnibus_coefficient_spec[primary_purpose] constants = config.get_model_constants(logsum_settings) - locals_dict = evaluate_constants(coefficient_spec, constants=constants) + locals_dict = assign.evaluate_constants(coefficient_spec, constants=constants) locals_dict.update(constants) # - od_logsums diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 064fda729..938e54fe8 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -17,9 +17,7 @@ from .util.mode import annotate_preprocessors -from .util.trip_mode import trip_mode_choice_spec -from .util.trip_mode import trip_mode_choice_coeffecients_spec -from activitysim.core.assign import evaluate_constants +from activitysim.core import assign from .util.expressions import skim_time_period_label @@ -43,8 +41,10 @@ def trip_mode_choice( trace_label = 'trip_mode_choice' model_settings = config.read_model_settings('trip_mode_choice.yaml') - spec = trip_mode_choice_spec(model_settings) - omnibus_coefficients = trip_mode_choice_coeffecients_spec(model_settings) + model_spec = \ + simulate.read_model_spec(config.config_file_path(model_settings['SPEC'])) + omnibus_coefficients = \ + assign.read_constant_spec(config.config_file_path(model_settings['COEFFS'])) trips_df = trips.to_frame() logger.info("Running %s with %d trips" % (trace_label, trips_df.shape[0])) @@ -99,7 +99,8 @@ def trip_mode_choice( # name index so tracing knows how to slice assert trips_segment.index.name == 'trip_id' - locals_dict = evaluate_constants(omnibus_coefficients[primary_purpose], constants=constants) + locals_dict = assign.evaluate_constants(omnibus_coefficients[primary_purpose], + constants=constants) locals_dict.update(constants) annotate_preprocessors( @@ -109,7 +110,7 @@ def trip_mode_choice( locals_dict.update(skims) choices = simulate.simple_simulate( choosers=trips_segment, - spec=spec, + spec=model_spec, nest_spec=nest_spec, skims=skims, locals_d=locals_dict, @@ -117,7 +118,7 @@ def trip_mode_choice( trace_label=segment_trace_label, trace_choice_name='trip_mode_choice') - alts = spec.columns + alts = model_spec.columns choices = choices.map(dict(zip(range(len(alts)), alts))) # tracing.print_summary('trip_mode_choice %s choices' % primary_purpose, diff --git a/activitysim/abm/models/trip_purpose.py b/activitysim/abm/models/trip_purpose.py index c5c3d2a7c..2b9751aa7 100644 --- a/activitysim/abm/models/trip_purpose.py +++ b/activitysim/abm/models/trip_purpose.py @@ -22,8 +22,7 @@ def trip_purpose_probs(): - configs_dir = inject.get_injectable('configs_dir') - f = os.path.join(configs_dir, 'trip_purpose_probs.csv') + f = config.config_file_path('trip_purpose_probs.csv') df = pd.read_csv(f, comment='#') return df diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 3bd1b0e49..9a65378c9 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -43,13 +43,6 @@ FAILFIX_DEFAULT = FAILFIX_CHOOSE_MOST_INITIAL -def trip_scheduling_probs(configs_dir): - - f = os.path.join(configs_dir, 'trip_scheduling_probs.csv') - df = pd.read_csv(f, comment='#') - return df - - def set_tour_hour(trips, tours): """ add columns 'tour_hour', 'earliest', 'latest' to trips @@ -449,7 +442,6 @@ def run_trip_scheduling( def trip_scheduling( trips, tours, - configs_dir, chunk_size, trace_hh_id): @@ -506,7 +498,7 @@ def trip_scheduling( failfix = model_settings.get(FAILFIX, FAILFIX_DEFAULT) - probs_spec = trip_scheduling_probs(configs_dir) + probs_spec = pd.read_csv(config.config_file_path('trip_scheduling_probs.csv'), comment='#') trips_df = trips.to_frame() tours = tours.to_frame() diff --git a/activitysim/abm/models/util/expressions.py b/activitysim/abm/models/util/expressions.py index 89c816c4c..eaf76b2f3 100644 --- a/activitysim/abm/models/util/expressions.py +++ b/activitysim/abm/models/util/expressions.py @@ -78,8 +78,6 @@ def compute_columns(df, model_settings, locals_dict={}, trace_label=None): same index as df """ - configs_dir = inject.get_injectable('configs_dir') - if isinstance(model_settings, str): model_settings_name = model_settings model_settings = config.read_model_settings('%s.yaml' % model_settings) @@ -102,7 +100,7 @@ def compute_columns(df, model_settings, locals_dict={}, trace_label=None): if not expressions_spec_name.endswith(".csv"): expressions_spec_name = '%s.csv' % expressions_spec_name - expressions_spec = assign.read_assignment_spec(os.path.join(configs_dir, expressions_spec_name)) + expressions_spec = assign.read_assignment_spec(config.config_file_path(expressions_spec_name)) assert expressions_spec.shape[0] > 0, \ "Expected to find some assignment expressions in %s" % expressions_spec_name diff --git a/activitysim/abm/models/util/mode.py b/activitysim/abm/models/util/mode.py index c0a1c776a..fa68cd6ff 100644 --- a/activitysim/abm/models/util/mode.py +++ b/activitysim/abm/models/util/mode.py @@ -10,6 +10,7 @@ from activitysim.core import tracing from activitysim.core import inject from activitysim.core import simulate +from activitysim.core import config from activitysim.core.assign import evaluate_constants from activitysim.core.util import assign_in_place @@ -26,21 +27,18 @@ def tour_mode_choice_spec(model_settings): - configs_dir = inject.get_injectable('configs_dir') - assert 'SPEC' in model_settings - return simulate.read_model_spec(configs_dir, model_settings['SPEC']) + return simulate.read_model_spec(config.config_file_path(model_settings['SPEC'])) -def tour_mode_choice_coeffecients_spec(model_settings): - configs_dir = inject.get_injectable('configs_dir') +def tour_mode_choice_coeffecients_spec(model_settings): assert 'COEFFS' in model_settings coeffs_file_name = model_settings['COEFFS'] - with open(os.path.join(configs_dir, coeffs_file_name)) as f: - return pd.read_csv(f, comment='#', index_col='Expression') + file_path = config.config_file_path(coeffs_file_name) + return pd.read_csv(file_path, comment='#', index_col='Expression') def run_tour_mode_choice_simulate( diff --git a/activitysim/abm/models/util/test/data/cdap_indiv_and_hhsize1.csv b/activitysim/abm/models/util/test/configs/cdap_indiv_and_hhsize1.csv similarity index 100% rename from activitysim/abm/models/util/test/data/cdap_indiv_and_hhsize1.csv rename to activitysim/abm/models/util/test/configs/cdap_indiv_and_hhsize1.csv diff --git a/activitysim/abm/models/util/test/data/cdap_interaction_coefficients.csv b/activitysim/abm/models/util/test/configs/cdap_interaction_coefficients.csv similarity index 100% rename from activitysim/abm/models/util/test/data/cdap_interaction_coefficients.csv rename to activitysim/abm/models/util/test/configs/cdap_interaction_coefficients.csv diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 55da8f085..4015a0e4d 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -19,6 +19,11 @@ def data_dir(): return os.path.join(os.path.dirname(__file__), 'data') +@pytest.fixture(scope='module') +def configs_dir(): + return os.path.join(os.path.dirname(__file__), 'configs') + + @pytest.fixture(scope='module') def people(data_dir): return pd.read_csv( @@ -27,13 +32,14 @@ def people(data_dir): @pytest.fixture(scope='module') -def cdap_indiv_and_hhsize1(data_dir): - return read_model_spec(data_dir, 'cdap_indiv_and_hhsize1.csv') +def cdap_indiv_and_hhsize1(configs_dir): + f = os.path.join(configs_dir, 'cdap_indiv_and_hhsize1.csv') + return read_model_spec(f) @pytest.fixture(scope='module') -def cdap_interaction_coefficients(data_dir): - f = os.path.join(data_dir, 'cdap_interaction_coefficients.csv') +def cdap_interaction_coefficients(configs_dir): + f = os.path.join(configs_dir, 'cdap_interaction_coefficients.csv') coefficients = pd.read_csv(f, comment='#') coefficients = cdap.preprocess_interaction_coefficients(coefficients) return coefficients @@ -57,9 +63,9 @@ def individual_utils( # return cdap.make_household_choices(hh_utils) -def test_bad_coefficients(data_dir): +def test_bad_coefficients(configs_dir): - f = os.path.join(data_dir, 'cdap_interaction_coefficients.csv') + f = os.path.join(configs_dir, 'cdap_interaction_coefficients.csv') coefficients = pd.read_csv(f, comment='#') coefficients.loc[2, 'activity'] = 'AA' diff --git a/activitysim/abm/models/util/trip_mode.py b/activitysim/abm/models/util/trip_mode.py deleted file mode 100644 index 6a74a6eb3..000000000 --- a/activitysim/abm/models/util/trip_mode.py +++ /dev/null @@ -1,31 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -import os -import pandas as pd -import numpy as np - -from activitysim.core import tracing -from activitysim.core import simulate -from activitysim.core import inject - -from activitysim.core.util import assign_in_place - -import expressions - - -def trip_mode_choice_spec(model_settings): - - configs_dir = inject.get_injectable('configs_dir') - - assert 'SPEC' in model_settings - return simulate.read_model_spec(configs_dir, model_settings['SPEC']) - - -def trip_mode_choice_coeffecients_spec(model_settings): - - configs_dir = inject.get_injectable('configs_dir') - - assert 'COEFFS' in model_settings - with open(os.path.join(configs_dir, model_settings['COEFFS'])) as f: - return pd.read_csv(f, comment='#', index_col='Expression') diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index cd86cef34..a6668932e 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -39,14 +39,8 @@ logger = logging.getLogger(__name__) -@inject.injectable() -def workplace_location_sample_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'workplace_location_sample.csv') - - @inject.step() def workplace_location_sample(persons_merged, - workplace_location_sample_spec, skim_dict, land_use, size_terms, chunk_size, trace_hh_id): @@ -63,6 +57,7 @@ def workplace_location_sample(persons_merged, trace_label = 'workplace_location_sample' model_settings = config.read_model_settings('workplace_location.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('workplace_location_sample.csv')) # FIXME - only choose workplace_location of workers? is this the right criteria? choosers = persons_merged.to_frame() @@ -101,7 +96,7 @@ def workplace_location_sample(persons_merged, alternatives, sample_size=sample_size, alt_col_name=alt_dest_col_name, - spec=workplace_location_sample_spec, + spec=model_spec, skims=skims, locals_d=locals_d, chunk_size=chunk_size, @@ -114,7 +109,7 @@ def workplace_location_sample(persons_merged, def workplace_location_logsums(persons_merged, skim_dict, skim_stack, workplace_location_sample, - configs_dir, chunk_size, trace_hh_id): + chunk_size, trace_hh_id): """ add logsum column to existing workplace_location_sample able @@ -174,18 +169,12 @@ def workplace_location_logsums(persons_merged, inject.add_column('workplace_location_sample', 'mode_choice_logsum', logsums) -@inject.injectable() -def workplace_location_spec(configs_dir): - return simulate.read_model_spec(configs_dir, 'workplace_location.csv') - - @inject.step() def workplace_location_simulate(persons_merged, persons, workplace_location_sample, - workplace_location_spec, skim_dict, land_use, size_terms, - configs_dir, chunk_size, trace_hh_id): + chunk_size, trace_hh_id): """ Workplace location model on workplace_location_sample annotated with mode_choice logsum to select a work_taz from sample alternatives @@ -193,6 +182,8 @@ def workplace_location_simulate(persons_merged, persons, trace_label = 'workplace_location_simulate' model_settings = config.read_model_settings('workplace_location.yaml') + model_spec = simulate.read_model_spec(config.config_file_path('workplace_location.csv')) + NO_WORKPLACE_TAZ = -1 location_sample = workplace_location_sample.to_frame() @@ -235,7 +226,7 @@ def workplace_location_simulate(persons_merged, persons, choices = interaction_sample_simulate( choosers, alternatives, - spec=workplace_location_spec, + spec=model_spec, choice_column=alt_dest_col_name, skims=skims, locals_d=locals_d, diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index e83e74a9e..7354843f0 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -8,12 +8,13 @@ import pandas as pd from activitysim.core import inject +from activitysim.core import config logger = logging.getLogger(__name__) @inject.table() -def size_terms(configs_dir): - f = os.path.join(configs_dir, 'destination_choice_size_terms.csv') +def size_terms(): + f = config.config_file_path('destination_choice_size_terms.csv') return pd.read_csv(f, comment='#', index_col='segment') diff --git a/activitysim/abm/tables/time_windows.py b/activitysim/abm/tables/time_windows.py index cbdb57df3..4078f2294 100644 --- a/activitysim/abm/tables/time_windows.py +++ b/activitysim/abm/tables/time_windows.py @@ -8,15 +8,16 @@ from activitysim.core import inject +from activitysim.core import config from activitysim.core import timetable as tt logger = logging.getLogger(__name__) @inject.injectable(cache=True) -def tdd_alts(configs_dir): +def tdd_alts(): # right now this file just contains the start and end hour - f = os.path.join(configs_dir, 'tour_departure_and_duration_alternatives.csv') + f = config.config_file_path('tour_departure_and_duration_alternatives.csv') df = pd.read_csv(f) df['duration'] = df.end - df.start diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 7bc82285e..8cfd3e352 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -29,6 +29,11 @@ def uniquify_key(dict, key, template="{} ({})"): return new_key +def read_constant_spec(file_path): + + return pd.read_csv(file_path, comment='#', index_col='Expression') + + def evaluate_constants(expressions, constants): """ Evaluate a list of constant expressions - each one can depend on the one before diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 32b0e6d54..9be5ae627 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -79,21 +79,21 @@ def setting(key, default=None): def read_model_settings(file_name, mandatory=False): - configs_dir = inject.get_injectable('configs_dir') + model_settings = None + + file_path = config_file_path(file_name, mandatory=False) - settings = None - file_path = os.path.join(configs_dir, file_name) - if os.path.isfile(file_path): + if file_path is not None and os.path.isfile(file_path): with open(file_path) as f: - settings = yaml.load(f) + model_settings = yaml.load(f) - if settings is None: - settings = {} + if model_settings is None: + model_settings = {} - if mandatory and not settings: + if mandatory and not model_settings: raise RuntimeError("Could not read settings from %s" % file_name) - return settings + return model_settings def get_model_constants(model_settings): @@ -175,3 +175,18 @@ def pipeline_file_path(file_name): prefix = inject.get_injectable('pipeline_file_prefix', None) return build_output_file_path(file_name, use_prefix=prefix) + + +def config_file_path(file_name, mandatory=True): + + configs_dir = inject.get_injectable('configs_dir') + + file_path = os.path.join(configs_dir, file_name) + + if not os.path.exists(os.path.join(configs_dir, file_name)): + if mandatory: + raise RuntimeError("config_file_path: file does not exist: %s" % file_path) + else: + return None + + return file_path diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index b1775934d..90bba6eb7 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -50,7 +50,14 @@ def uniquify_spec_index(spec): assert spec.index.is_unique -def read_model_spec(fpath, fname, +def read_model_alts(file_path, set_index=None): + df = pd.read_csv(file_path, comment='#') + if set_index: + df.set_index(set_index, inplace=True) + return df + + +def read_model_spec(file_path, description_name="Description", expression_name="Expression"): """ @@ -65,10 +72,8 @@ def read_model_spec(fpath, fname, Parameters ---------- - fpath : str - path to directory containing file. - fname : str - Name of a CSV spec file + file_path : str + path to CSV spec file description_name : str, optional Name of the column in `fname` that contains the component description. expression_name : str, optional @@ -81,8 +86,7 @@ def read_model_spec(fpath, fname, expression values are set as the table index. """ - with open(os.path.join(fpath, fname)) as f: - spec = pd.read_csv(f, comment='#') + spec = pd.read_csv(file_path, comment='#') spec = spec.dropna(subset=[expression_name]) @@ -99,7 +103,7 @@ def read_model_spec(fpath, fname, # drop any rows with all zeros since they won't have any effect (0 marginal utility) zero_rows = (spec == 0).all(axis=1) if zero_rows.any(): - logger.debug("dropping %s all-zero rows from %s" % (zero_rows.sum(), fname)) + logger.debug("dropping %s all-zero rows from %s" % (zero_rows.sum(), file_path)) spec = spec.loc[~zero_rows] return spec diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 9cf91a03f..8765d4c3e 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -26,7 +26,7 @@ def spec_name(data_dir): @pytest.fixture(scope='module') def spec(data_dir, spec_name): return simulate.read_model_spec( - data_dir, spec_name, + os.path.join(data_dir, spec_name), description_name='description', expression_name='expression') @@ -39,7 +39,7 @@ def data(data_dir): def test_read_model_spec(data_dir, spec_name): spec = simulate.read_model_spec( - data_dir, spec_name, + os.path.join(data_dir, spec_name), description_name='description', expression_name='expression') assert len(spec) == 4 diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index d2ab9ad08..cd0da1cb6 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -120,10 +120,7 @@ def config_logger(custom_config_file=None, basic=False): log_config_file = custom_config_file elif not basic: # look for conf file in configs_dir - configs_dir = inject.get_injectable('configs_dir') - default_config_file = os.path.join(configs_dir, LOGGING_CONF_FILE_NAME) - if os.path.isfile(default_config_file): - log_config_file = default_config_file + log_config_file = config.config_file_path(LOGGING_CONF_FILE_NAME, mandatory=False) if log_config_file: with open(log_config_file) as f: diff --git a/example_multi/extensions/models.py b/example_multi/extensions/models.py index 3efaecbb5..e49aff9cc 100644 --- a/example_multi/extensions/models.py +++ b/example_multi/extensions/models.py @@ -19,9 +19,8 @@ @inject.injectable() -def best_transit_path_spec(configs_dir): - f = os.path.join(configs_dir, 'best_transit_path.csv') - return assign.read_assignment_spec(f) +def best_transit_path_spec(): + return assign.read_assignment_spec(config.config_file_path('best_transit_path.csv')) VECTOR_TEST_SIZE = 100000 From e7996ad121fc2abb2ca6bd36298df3849fda9a84 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 20 Sep 2018 11:24:16 -0400 Subject: [PATCH 012/122] more options for read_model_spec --- .../abm/models/atwork_subtour_destination.py | 5 ++-- .../abm/models/atwork_subtour_frequency.py | 4 +-- .../abm/models/atwork_subtour_scheduling.py | 5 +--- activitysim/abm/models/auto_ownership.py | 2 +- activitysim/abm/models/cdap.py | 4 +-- .../abm/models/joint_tour_composition.py | 2 +- .../abm/models/joint_tour_destination.py | 7 ++--- .../abm/models/joint_tour_frequency.py | 2 +- .../abm/models/joint_tour_participation.py | 2 +- .../abm/models/joint_tour_scheduling.py | 2 +- .../abm/models/mandatory_scheduling.py | 4 +-- .../abm/models/mandatory_tour_frequency.py | 3 +-- .../abm/models/non_mandatory_destination.py | 4 +-- .../abm/models/non_mandatory_scheduling.py | 3 +-- .../models/non_mandatory_tour_frequency.py | 4 +-- activitysim/abm/models/school_location.py | 4 +-- activitysim/abm/models/stop_frequency.py | 9 +------ activitysim/abm/models/trip_destination.py | 4 +-- activitysim/abm/models/trip_mode_choice.py | 2 +- activitysim/abm/models/util/mode.py | 2 +- activitysim/abm/models/util/test/test_cdap.py | 3 +-- activitysim/abm/models/workplace_location.py | 4 +-- activitysim/core/simulate.py | 26 ++++++++++++++++--- activitysim/core/test/test_simulate.py | 6 +++-- 24 files changed, 56 insertions(+), 57 deletions(-) diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index 3f1ca85da..f8bd306e9 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -39,8 +39,7 @@ def atwork_subtour_destination_sample(tours, trace_label = 'atwork_subtour_location_sample' model_settings = config.read_model_settings('atwork_subtour_destination.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('atwork_subtour_destination_sample.csv')) + model_spec = simulate.read_model_spec(file_name='atwork_subtour_destination_sample.csv') persons_merged = persons_merged.to_frame() @@ -187,7 +186,7 @@ def atwork_subtour_destination_simulate(tours, destination_sample = destination_sample.to_frame() model_settings = config.read_model_settings('atwork_subtour_destination.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('atwork_subtour_destination.csv')) + model_spec = simulate.read_model_spec(file_name='atwork_subtour_destination.csv') tours = tours.to_frame() subtours = tours[tours.tour_category == 'atwork'] diff --git a/activitysim/abm/models/atwork_subtour_frequency.py b/activitysim/abm/models/atwork_subtour_frequency.py index 330a77b3b..0f93679a1 100644 --- a/activitysim/abm/models/atwork_subtour_frequency.py +++ b/activitysim/abm/models/atwork_subtour_frequency.py @@ -7,7 +7,6 @@ import pandas as pd import numpy as np -from activitysim.core.simulate import read_model_spec from activitysim.core.interaction_simulate import interaction_simulate from activitysim.core import simulate @@ -43,8 +42,7 @@ def atwork_subtour_frequency(tours, trace_label = 'atwork_subtour_frequency' model_settings = config.read_model_settings('atwork_subtour_frequency.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('atwork_subtour_frequency.csv')) + model_spec = simulate.read_model_spec(file_name='atwork_subtour_frequency.csv') alternatives = simulate.read_model_alts( config.config_file_path('atwork_subtour_frequency_alternatives.csv'), set_index='alt') diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index 27769137a..b8850fb45 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -6,9 +6,6 @@ import pandas as pd -from activitysim.core.simulate import read_model_spec -from activitysim.core.interaction_simulate import interaction_simulate - from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import pipeline @@ -40,7 +37,7 @@ def atwork_subtour_scheduling( trace_label = 'atwork_subtour_scheduling' model_settings = config.read_model_settings('tour_scheduling_atwork.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_atwork.csv')) + model_spec = simulate.read_model_spec(file_name='tour_scheduling_atwork.csv') persons_merged = persons_merged.to_frame() diff --git a/activitysim/abm/models/auto_ownership.py b/activitysim/abm/models/auto_ownership.py index 5a36a6e36..a8e0d3143 100644 --- a/activitysim/abm/models/auto_ownership.py +++ b/activitysim/abm/models/auto_ownership.py @@ -28,7 +28,7 @@ def auto_ownership_simulate(households, logger.info("Running %s with %d households" % (trace_label, len(households_merged))) - model_spec = simulate.read_model_spec(config.config_file_path('auto_ownership.csv')) + model_spec = simulate.read_model_spec(file_name='auto_ownership.csv') nest_spec = config.get_logit_model_settings(model_settings) constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/models/cdap.py b/activitysim/abm/models/cdap.py index 6dbbfab63..de8ab01bc 100644 --- a/activitysim/abm/models/cdap.py +++ b/activitysim/abm/models/cdap.py @@ -24,7 +24,7 @@ def cdap_indiv_spec(): spec to compute the activity utilities for each individual hh member with no interactions with other household members taken into account """ - return simulate.read_model_spec(config.config_file_path('cdap_indiv_and_hhsize1.csv')) + return simulate.read_model_spec(file_name='cdap_indiv_and_hhsize1.csv') @inject.injectable() @@ -47,7 +47,7 @@ def cdap_fixed_relative_proportions(): EXCEPT that the values computed are relative proportions, not utilities (i.e. values are not exponentiated before being normalized to probabilities summing to 1.0) """ - return simulate.read_model_spec(config.config_file_path('cdap_fixed_relative_proportions.csv')) + return simulate.read_model_spec(file_name='cdap_fixed_relative_proportions.csv') @inject.step() diff --git a/activitysim/abm/models/joint_tour_composition.py b/activitysim/abm/models/joint_tour_composition.py index fd6f30b28..4c5faec91 100644 --- a/activitysim/abm/models/joint_tour_composition.py +++ b/activitysim/abm/models/joint_tour_composition.py @@ -40,7 +40,7 @@ def joint_tour_composition( trace_label = 'joint_tour_composition' model_settings = config.read_model_settings('joint_tour_composition.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_composition.csv')) + model_spec = simulate.read_model_spec(file_name='joint_tour_composition.csv') tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index 49cb0f81e..7979ec5bb 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -8,7 +8,6 @@ import numpy as np import pandas as pd -from activitysim.core.simulate import read_model_spec from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.interaction_sample import interaction_sample @@ -94,8 +93,7 @@ def joint_tour_destination_sample( trace_label = 'joint_tour_destination_sample' model_settings = config.read_model_settings('joint_tour_destination.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('non_mandatory_tour_destination_sample.csv')) + model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_destination_sample.csv') joint_tours = tours.to_frame() joint_tours = joint_tours[joint_tours.tour_category == 'joint'] @@ -296,8 +294,7 @@ def joint_tour_destination_simulate( model_settings = config.read_model_settings('joint_tour_destination.yaml') # - tour types are subset of non_mandatory tour types and use same expressions - model_spec = simulate.read_model_spec( - config.config_file_path('non_mandatory_tour_destination.csv')) + model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_destination.csv') destination_sample = destination_sample.to_frame() tours = tours.to_frame() diff --git a/activitysim/abm/models/joint_tour_frequency.py b/activitysim/abm/models/joint_tour_frequency.py index afa9e9386..55b8cc48f 100644 --- a/activitysim/abm/models/joint_tour_frequency.py +++ b/activitysim/abm/models/joint_tour_frequency.py @@ -32,7 +32,7 @@ def joint_tour_frequency( """ trace_label = 'joint_tour_frequency' model_settings = config.read_model_settings('joint_tour_frequency.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_frequency.csv')) + model_spec = simulate.read_model_spec(file_name='joint_tour_frequency.csv') alternatives = simulate.read_model_alts( config.config_file_path('joint_tour_frequency_alternatives.csv'), set_index='alt') diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 42bb26c9c..13214ebf4 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -231,7 +231,7 @@ def joint_tour_participation( """ trace_label = 'joint_tour_participation' model_settings = config.read_model_settings('joint_tour_participation.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('joint_tour_participation.csv')) + model_spec = simulate.read_model_spec(file_name='joint_tour_participation.csv') tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] diff --git a/activitysim/abm/models/joint_tour_scheduling.py b/activitysim/abm/models/joint_tour_scheduling.py index 9679090a8..7febe3980 100644 --- a/activitysim/abm/models/joint_tour_scheduling.py +++ b/activitysim/abm/models/joint_tour_scheduling.py @@ -32,7 +32,7 @@ def joint_tour_scheduling( """ trace_label = 'joint_tour_scheduling' model_settings = config.read_model_settings('joint_tour_scheduling.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_joint.csv')) + model_spec = simulate.read_model_spec(file_name='tour_scheduling_joint.csv') tours = tours.to_frame() joint_tours = tours[tours.tour_category == 'joint'] diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index ed4096b9b..7071521e9 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -34,8 +34,8 @@ def mandatory_tour_scheduling(tours, """ trace_label = 'mandatory_tour_scheduling' model_settings = config.read_model_settings('mandatory_tour_scheduling.yaml') - work_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_work.csv')) - school_spec = simulate.read_model_spec(config.config_file_path('tour_scheduling_school.csv')) + work_spec = simulate.read_model_spec(file_name='tour_scheduling_work.csv') + school_spec = simulate.read_model_spec(file_name='tour_scheduling_school.csv') tours = tours.to_frame() persons_merged = persons_merged.to_frame() diff --git a/activitysim/abm/models/mandatory_tour_frequency.py b/activitysim/abm/models/mandatory_tour_frequency.py index bed55f8dc..8f5db1564 100644 --- a/activitysim/abm/models/mandatory_tour_frequency.py +++ b/activitysim/abm/models/mandatory_tour_frequency.py @@ -51,8 +51,7 @@ def mandatory_tour_frequency(persons_merged, trace_label = 'mandatory_tour_frequency' model_settings = config.read_model_settings('mandatory_tour_frequency.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('mandatory_tour_frequency.csv')) + model_spec = simulate.read_model_spec(file_name='mandatory_tour_frequency.csv') alternatives = simulate.read_model_alts( config.config_file_path('mandatory_tour_frequency_alternatives.csv'), set_index='alt') diff --git a/activitysim/abm/models/non_mandatory_destination.py b/activitysim/abm/models/non_mandatory_destination.py index f46f10ddf..ba9146d8f 100644 --- a/activitysim/abm/models/non_mandatory_destination.py +++ b/activitysim/abm/models/non_mandatory_destination.py @@ -5,7 +5,6 @@ import pandas as pd -from activitysim.core.simulate import read_model_spec from activitysim.core.interaction_simulate import interaction_simulate from activitysim.core import tracing @@ -37,8 +36,7 @@ def non_mandatory_tour_destination( trace_label = 'non_mandatory_tour_destination' model_settings = config.read_model_settings('non_mandatory_tour_destination.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('non_mandatory_tour_destination_sample.csv')) + model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_destination_sample.csv') tours = tours.to_frame() diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index eb6e4638a..15992b1f5 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -35,8 +35,7 @@ def non_mandatory_tour_scheduling(tours, trace_label = 'non_mandatory_tour_scheduling' model_settinsg = config.read_model_settings('non_mandatory_tour_scheduling.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('tour_scheduling_nonmandatory.csv')) + model_spec = simulate.read_model_spec(file_name='tour_scheduling_nonmandatory.csv') tours = tours.to_frame() persons_merged = persons_merged.to_frame() diff --git a/activitysim/abm/models/non_mandatory_tour_frequency.py b/activitysim/abm/models/non_mandatory_tour_frequency.py index 400768167..06e4f2f3d 100644 --- a/activitysim/abm/models/non_mandatory_tour_frequency.py +++ b/activitysim/abm/models/non_mandatory_tour_frequency.py @@ -6,7 +6,6 @@ import pandas as pd -from activitysim.core.simulate import read_model_spec from activitysim.core.interaction_simulate import interaction_simulate from activitysim.core import tracing @@ -39,8 +38,7 @@ def non_mandatory_tour_frequency(persons, persons_merged, trace_label = 'non_mandatory_tour_frequency' model_settings = config.read_model_settings('non_mandatory_tour_frequency.yaml') - model_spec = simulate.read_model_spec( - config.config_file_path('non_mandatory_tour_frequency.csv')) + model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_frequency.csv') alternatives = simulate.read_model_alts( config.config_file_path('non_mandatory_tour_frequency_alternatives.csv'), diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 96dc07987..dab93f7af 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -63,7 +63,7 @@ def school_location_sample( trace_label = 'school_location_sample' model_settings = config.read_model_settings('school_location.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('school_location_sample.csv')) + model_spec = simulate.read_model_spec(file_name='school_location_sample.csv') choosers = persons_merged.to_frame() # FIXME - MEMORY HACK - only include columns actually used in spec @@ -234,7 +234,7 @@ def school_location_simulate(persons_merged, persons, """ trace_label = 'school_location_simulate' model_settings = config.read_model_settings('school_location.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('school_location.csv')) + model_spec = simulate.read_model_spec(file_name='school_location.csv') NO_SCHOOL_TAZ = -1 diff --git a/activitysim/abm/models/stop_frequency.py b/activitysim/abm/models/stop_frequency.py index 9c75de605..cf44eb207 100644 --- a/activitysim/abm/models/stop_frequency.py +++ b/activitysim/abm/models/stop_frequency.py @@ -20,13 +20,6 @@ logger = logging.getLogger(__name__) -def get_stop_frequency_spec(tour_type): - - file_name = 'stop_frequency_%s.csv' % tour_type - file_path = config.config_file_path(file_name) - return simulate.read_model_spec(file_path) - - @inject.injectable() def stop_frequency_alts(): # alt file for building trips even though simulation is simple_simulate not interaction_simulate @@ -199,7 +192,7 @@ def stop_frequency( logging.info("%s running segment %s with %s chooser rows" % (trace_label, segment_type, choosers.shape[0])) - spec = get_stop_frequency_spec(segment_type) + spec = simulate.read_model_spec(file_name='stop_frequency_%s.csv' % segment_type) assert spec is not None, "spec for segment_type %s not found" % segment_type diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 6963c3840..7d29119ae 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -40,7 +40,7 @@ def get_spec_for_purpose(model_settings, spec_name, purpose): - omnibus_spec = simulate.read_model_spec(config.config_file_path(model_settings[spec_name])) + omnibus_spec = simulate.read_model_spec(file_name=model_settings[spec_name]) spec = omnibus_spec[[purpose]] @@ -126,7 +126,7 @@ def compute_ood_logsums( trace_label) nest_spec = config.get_logit_model_settings(logsum_settings) - logsum_spec = simulate.read_model_spec(config.config_file_path(logsum_settings['SPEC'])) + logsum_spec = simulate.read_model_spec(file_name=logsum_settings['SPEC']) logsums = simulate.simple_simulate_logsums( choosers, diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 938e54fe8..247f42b69 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -42,7 +42,7 @@ def trip_mode_choice( model_settings = config.read_model_settings('trip_mode_choice.yaml') model_spec = \ - simulate.read_model_spec(config.config_file_path(model_settings['SPEC'])) + simulate.read_model_spec(file_name=model_settings['SPEC']) omnibus_coefficients = \ assign.read_constant_spec(config.config_file_path(model_settings['COEFFS'])) diff --git a/activitysim/abm/models/util/mode.py b/activitysim/abm/models/util/mode.py index fa68cd6ff..782fcbba9 100644 --- a/activitysim/abm/models/util/mode.py +++ b/activitysim/abm/models/util/mode.py @@ -29,7 +29,7 @@ def tour_mode_choice_spec(model_settings): assert 'SPEC' in model_settings - return simulate.read_model_spec(config.config_file_path(model_settings['SPEC'])) + return simulate.read_model_spec(file_name=model_settings['SPEC']) def tour_mode_choice_coeffecients_spec(model_settings): diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 4015a0e4d..8d1267d86 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -33,8 +33,7 @@ def people(data_dir): @pytest.fixture(scope='module') def cdap_indiv_and_hhsize1(configs_dir): - f = os.path.join(configs_dir, 'cdap_indiv_and_hhsize1.csv') - return read_model_spec(f) + return read_model_spec(file_name='cdap_indiv_and_hhsize1.csv', spec_dir=configs_dir) @pytest.fixture(scope='module') diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index a6668932e..3b2b7f298 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -57,7 +57,7 @@ def workplace_location_sample(persons_merged, trace_label = 'workplace_location_sample' model_settings = config.read_model_settings('workplace_location.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('workplace_location_sample.csv')) + model_spec = simulate.read_model_spec(file_name='workplace_location_sample.csv') # FIXME - only choose workplace_location of workers? is this the right criteria? choosers = persons_merged.to_frame() @@ -182,7 +182,7 @@ def workplace_location_simulate(persons_merged, persons, trace_label = 'workplace_location_simulate' model_settings = config.read_model_settings('workplace_location.yaml') - model_spec = simulate.read_model_spec(config.config_file_path('workplace_location.csv')) + model_spec = simulate.read_model_spec(file_name='workplace_location.csv') NO_WORKPLACE_TAZ = -1 diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 90bba6eb7..f31d2b770 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -14,6 +14,7 @@ from . import logit from . import tracing from . import pipeline +from . import config from . import assign @@ -57,12 +58,14 @@ def read_model_alts(file_path, set_index=None): return df -def read_model_spec(file_path, +def read_model_spec(model_settings=None, file_name=None, spec_dir=None, description_name="Description", expression_name="Expression"): """ Read a CSV model specification into a Pandas DataFrame or Series. + file_path : str absolute or relative path to file + The CSV is expected to have columns for component descriptions and expressions, plus one or more alternatives. @@ -72,8 +75,13 @@ def read_model_spec(file_path, Parameters ---------- - file_path : str - path to CSV spec file + model_settings : dict + name of spec_file is in model_settings['SPEC'] and file is relative to configs + file_name : str + file_name id spec file in configs folder (or in spec_dir is specified) + spec_dir : str + directory in which to fine spec file if not in configs + description_name : str, optional Name of the column in `fname` that contains the component description. expression_name : str, optional @@ -86,6 +94,18 @@ def read_model_spec(file_path, expression values are set as the table index. """ + assert (model_settings or file_name) and not (model_settings and file_name), \ + "expect either model_spec or file_name argument" + + if model_settings is not None: + assert isinstance(model_settings, dict) + file_name = model_settings['SPEC'] + + if spec_dir is not None: + file_path = os.path.join(spec_dir, file_name) + else: + file_path = config.config_file_path(file_name) + spec = pd.read_csv(file_path, comment='#') spec = spec.dropna(subset=[expression_name]) diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 8765d4c3e..d07315be3 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -26,7 +26,8 @@ def spec_name(data_dir): @pytest.fixture(scope='module') def spec(data_dir, spec_name): return simulate.read_model_spec( - os.path.join(data_dir, spec_name), + file_name=spec_name, + spec_dir=data_dir, description_name='description', expression_name='expression') @@ -39,7 +40,8 @@ def data(data_dir): def test_read_model_spec(data_dir, spec_name): spec = simulate.read_model_spec( - os.path.join(data_dir, spec_name), + file_name=spec_name, + spec_dir=data_dir, description_name='description', expression_name='expression') assert len(spec) == 4 From 77ac9063cae64e5a0c44aab37eecd543f7cae91c Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Sat, 29 Sep 2018 16:26:58 -0400 Subject: [PATCH 013/122] example_mp multiprocessing prototype works on mac - no tests or documentation --- .gitignore | 1 - activitysim/abm/models/school_location.py | 37 +- .../abm/test/configs/school_location.yaml | 10 +- activitysim/core/config.py | 160 ++++- activitysim/core/inject_defaults.py | 59 -- activitysim/core/simulate.py | 4 + activitysim/core/test/test_inject_defaults.py | 4 +- activitysim/core/test/test_tracing.py | 62 -- activitysim/core/tracing.py | 45 +- example/configs/logging.yaml | 6 +- example/configs/school_location.yaml | 10 +- example/configs/settings.yaml | 6 +- ..._frequency_annotate_tours_preprocessor.csv | 6 +- example_mp/configs/logging.yaml | 55 ++ example_mp/configs/settings.yaml | 114 ++++ .../configs/trip_purpose_and_destination.yaml | 8 + example_mp/configs/trip_scheduling.yaml | 8 + example_mp/output/.gitignore | 5 + example_mp/simulation.py | 65 ++ example_mp/tasks.py | 634 ++++++++++++++++++ 20 files changed, 1090 insertions(+), 209 deletions(-) delete mode 100644 activitysim/core/inject_defaults.py create mode 100644 example_mp/configs/logging.yaml create mode 100644 example_mp/configs/settings.yaml create mode 100644 example_mp/configs/trip_purpose_and_destination.yaml create mode 100644 example_mp/configs/trip_scheduling.yaml create mode 100644 example_mp/output/.gitignore create mode 100644 example_mp/simulation.py create mode 100644 example_mp/tasks.py diff --git a/.gitignore b/.gitignore index fbafd8c5b..229e6dcac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ sandbox/ -example_mp/ example/data/* .idea .ipynb_checkpoints diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index dab93f7af..3d0ec1dd6 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -60,10 +60,9 @@ def school_location_sample( +-----------+--------------+----------------+------------+ """ - trace_label = 'school_location_sample' + model_name = 'school_location_sample' model_settings = config.read_model_settings('school_location.yaml') - - model_spec = simulate.read_model_spec(file_name='school_location_sample.csv') + model_spec = simulate.read_model_spec(file_name=model_settings['SAMPLE_SPEC']) choosers = persons_merged.to_frame() # FIXME - MEMORY HACK - only include columns actually used in spec @@ -97,7 +96,7 @@ def school_location_sample( choosers_segment = choosers[choosers["is_" + school_type]] if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (trace_label, school_type)) + logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) continue # alts indexed by taz with one column containing size_term for this tour_type @@ -118,7 +117,7 @@ def school_location_sample( skims=skims, locals_d=locals_d, chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(trace_label, school_type)) + trace_label=tracing.extend_trace_label(model_name, school_type)) choices['school_type'] = school_type_id choices_list.append(choices) @@ -128,7 +127,7 @@ def school_location_sample( # - # NARROW choices['school_type'] = choices['school_type'].astype(np.uint8) else: - logger.info("Skipping %s: add_null_results" % trace_label) + logger.info("Skipping %s: add_null_results" % model_name) choices = pd.DataFrame() inject.add_table('school_location_sample', choices) @@ -162,16 +161,15 @@ def school_location_logsums( | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | +-----------+--------------+----------------+------------+----------------+ """ - - trace_label = 'school_location_logsums' - + model_name = 'school_location_logsums' model_settings = config.read_model_settings('school_location.yaml') + logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) location_sample = school_location_sample.to_frame() if location_sample.shape[0] == 0: - tracing.no_results(trace_label) + tracing.no_results(model_name) return logger.info("Running school_location_logsums with %s rows" % location_sample.shape[0]) @@ -188,7 +186,7 @@ def school_location_logsums( choosers = location_sample[location_sample['school_type'] == school_type_id] if choosers.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (trace_label, school_type)) + logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) continue choosers = pd.merge( @@ -204,7 +202,7 @@ def school_location_logsums( logsum_settings, model_settings, skim_dict, skim_stack, chunk_size, trace_hh_id, - tracing.extend_trace_label(trace_label, school_type)) + tracing.extend_trace_label(model_name, school_type)) logsums_list.append(logsums) @@ -232,9 +230,10 @@ def school_location_simulate(persons_merged, persons, School location model on school_location_sample annotated with mode_choice logsum to select a school_taz from sample alternatives """ - trace_label = 'school_location_simulate' + model_name = 'school_location' model_settings = config.read_model_settings('school_location.yaml') - model_spec = simulate.read_model_spec(file_name='school_location.csv') + + model_spec = simulate.read_model_spec(file_name=model_settings['SPEC']) NO_SCHOOL_TAZ = -1 @@ -273,7 +272,7 @@ def school_location_simulate(persons_merged, persons, choosers_segment = choosers[choosers["is_" + school_type]] if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (trace_label, school_type)) + logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) continue alts_segment = \ @@ -292,8 +291,8 @@ def school_location_simulate(persons_merged, persons, skims=skims, locals_d=locals_d, chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(trace_label, school_type), - trace_choice_name='school_location') + trace_label=tracing.extend_trace_label(model_name, school_type), + trace_choice_name=model_name) choices_list.append(choices) @@ -310,12 +309,12 @@ def school_location_simulate(persons_merged, persons, # no school-goers (but we still want to annotate persons) persons['school_taz'] = NO_SCHOOL_TAZ - logger.info("%s no school-goers" % trace_label) + logger.info("%s no school-goers" % model_name) expressions.assign_columns( df=persons, model_settings=model_settings.get('annotate_persons'), - trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) + trace_label=tracing.extend_trace_label(model_name, 'annotate_persons')) pipeline.replace_table("persons", persons) diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index 83813ad50..80a7bc66b 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -6,17 +6,17 @@ SIMULATE_CHOOSER_COLUMNS: - is_highschool - is_gradeschool - -LOGSUM_SETTINGS: tour_mode_choice.yaml -LOGSUM_PREPROCESSOR: nontour_preprocessor - - # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +SAMPLE_SPEC: school_location_sample.csv +SPEC: school_location.csv + +LOGSUM_SETTINGS: tour_mode_choice.yaml +LOGSUM_PREPROCESSOR: nontour_preprocessor annotate_persons: SPEC: annotate_persons_school diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 9be5ae627..a98e0f8aa 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -10,6 +10,65 @@ logger = logging.getLogger(__name__) +""" + default injectables +""" + + +@inject.injectable() +def configs_dir(): + if not os.path.exists('configs'): + raise RuntimeError("configs_dir: directory does not exist") + return 'configs' + + +@inject.injectable() +def data_dir(): + if not os.path.exists('data'): + raise RuntimeError("data_dir: directory does not exist") + return 'data' + + +@inject.injectable() +def output_dir(): + if not os.path.exists('output'): + raise RuntimeError("output_dir: directory does not exist") + return 'output' + + +@inject.injectable() +def output_file_prefix(): + return '' + + +@inject.injectable(cache=True) +def settings(): + return read_settings_file('settings.yaml', mandatory=True) + + +@inject.injectable(cache=True) +def pipeline_file_name(settings): + """ + Orca injectable to return the path to the pipeline hdf5 file based on output_dir and settings + """ + pipeline_file_name = settings.get('pipeline', 'pipeline.h5') + + return pipeline_file_name + + +@inject.injectable() +def rng_base_seed(): + return 0 + + +def str2bool(v): + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + def handle_standard_args(parser=None): """ @@ -32,16 +91,19 @@ def handle_standard_args(parser=None): if parser is None: parser = argparse.ArgumentParser() - parser.add_argument("-c", "--config", help="path to config dir") + parser.add_argument("-c", "--config", help="path to config dir", action='append') parser.add_argument("-o", "--output", help="path to output dir") parser.add_argument("-d", "--data", help="path to data dir") parser.add_argument("-r", "--resume", help="resume after") - parser.add_argument("-m", "--models", help="models run_list_name in settings") + parser.add_argument("-m", "--multiprocess", type=str2bool, nargs='?', const=True, + help="run multiprocess (boolean flag, no arg defaults to true)") + args = parser.parse_args() if args.config: - if not os.path.exists(args.config): - raise IOError("Could not find configs dir '%s'." % args.config) + for dir in args.config: + if not os.path.exists(dir): + raise IOError("Could not find configs dir '%s'" % dir) inject.add_injectable("configs_dir", args.config) if args.output: if not os.path.exists(args.output): @@ -49,12 +111,12 @@ def handle_standard_args(parser=None): inject.add_injectable("output_dir", args.output) if args.data: if not os.path.exists(args.data): - raise IOError("Could not find data dir '%s'." % args.data) + raise IOError("Could not find data dir '%s'" % args.data) inject.add_injectable("data_dir", args.data) if args.resume: inject.add_injectable("resume_after", args.resume) - if args.models: - inject.add_injectable("run_list_name", args.models) + if args.multiprocess: + inject.add_injectable("multiprocess", args.multiprocess) return args @@ -70,6 +132,11 @@ def setting(key, default=None): if s is None: s = inject.get_injectable(key, None) + if s: + # fixme - when does this happen? + logger.info("read setting %s from injectable" % key) + bug + # otherwise fall back to supplied default if s is None: s = default @@ -78,20 +145,23 @@ def setting(key, default=None): def read_model_settings(file_name, mandatory=False): + """ - model_settings = None - - file_path = config_file_path(file_name, mandatory=False) + Parameters + ---------- + file_name : str + yaml file name + mandatory : bool + throw error if file empty or not found + Returns + ------- - if file_path is not None and os.path.isfile(file_path): - with open(file_path) as f: - model_settings = yaml.load(f) + """ - if model_settings is None: - model_settings = {} + if not file_name.lower().endswith('.yaml'): + file_name = '%s.yaml' % (file_name, ) - if mandatory and not model_settings: - raise RuntimeError("Could not read settings from %s" % file_name) + model_settings = read_settings_file(file_name, mandatory=mandatory) return model_settings @@ -181,12 +251,56 @@ def config_file_path(file_name, mandatory=True): configs_dir = inject.get_injectable('configs_dir') - file_path = os.path.join(configs_dir, file_name) + if isinstance(configs_dir, str): + configs_dir = [configs_dir] + + assert isinstance(configs_dir, list) - if not os.path.exists(os.path.join(configs_dir, file_name)): - if mandatory: - raise RuntimeError("config_file_path: file does not exist: %s" % file_path) - else: - return None + file_path = None + for dir in configs_dir: + p = os.path.join(dir, file_name) + if os.path.exists(p): + file_path = p + break + + if mandatory and not file_path: + raise RuntimeError("config_file_path: file '%s' not in %s" % (file_path, configs_dir)) return file_path + + +def read_settings_file(file_name, mandatory=True): + + def backfill_settings(settings, backfill): + new_settings = backfill.copy() + new_settings.update(settings) + return new_settings + + configs_dir = inject.get_injectable('configs_dir') + + if isinstance(configs_dir, str): + configs_dir = [configs_dir] + assert isinstance(configs_dir, list) + + settings = {} + for dir in configs_dir: + file_path = os.path.join(dir, file_name) + if os.path.exists(file_path): + if settings: + logger.debug("inherit settings for %s from %s" % (file_name, file_path)) + + with open(file_path) as f: + s = yaml.load(f) + settings = backfill_settings(settings, s) + + if s.get('inherit_settings', False): + logger.debug("inherit_settings flag set for %s in %s" % (file_name, file_path)) + continue + else: + break + + if mandatory and not settings: + raise RuntimeError("read_settings_file: no settings for '%s' in %s" % + (file_name, configs_dir)) + + return settings diff --git a/activitysim/core/inject_defaults.py b/activitysim/core/inject_defaults.py deleted file mode 100644 index b3b39209b..000000000 --- a/activitysim/core/inject_defaults.py +++ /dev/null @@ -1,59 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -import os -import logging - -import pandas as pd -import yaml - -from activitysim.core import inject - -logger = logging.getLogger(__name__) - - -@inject.injectable() -def configs_dir(): - if not os.path.exists('configs'): - raise RuntimeError("configs_dir: directory does not exist") - return 'configs' - - -@inject.injectable() -def data_dir(): - if not os.path.exists('data'): - raise RuntimeError("data_dir: directory does not exist") - return 'data' - - -@inject.injectable() -def output_dir(): - if not os.path.exists('output'): - raise RuntimeError("output_dir: directory does not exist") - return 'output' - - -@inject.injectable() -def output_file_prefix(): - return '' - - -@inject.injectable(cache=True) -def settings(configs_dir): - with open(os.path.join(configs_dir, 'settings.yaml')) as f: - return yaml.load(f) - - -@inject.injectable(cache=True) -def pipeline_file_name(settings): - """ - Orca injectable to return the path to the pipeline hdf5 file based on output_dir and settings - """ - pipeline_file_name = settings.get('pipeline', 'pipeline.h5') - - return pipeline_file_name - - -@inject.injectable() -def rng_base_seed(): - return 0 diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index f31d2b770..c46f897ac 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -100,6 +100,10 @@ def read_model_spec(model_settings=None, file_name=None, spec_dir=None, if model_settings is not None: assert isinstance(model_settings, dict) file_name = model_settings['SPEC'] + else: + assert isinstance(file_name, str) + if not file_name.lower().endswith('.csv'): + file_name = '%s.csv' % (file_name,) if spec_dir is not None: file_path = os.path.join(spec_dir, file_name) diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index 917cdb355..df873203e 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -11,8 +11,8 @@ from .. import inject -# Also note that the following import statement has the side-effect of registering injectables: -from .. import inject_defaults +# Note that the following import statement has the side-effect of registering injectables: +from .. import config def teardown_function(func): diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index dc29f1bff..2fffca60a 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -33,43 +33,6 @@ def add_canonical_dirs(): orca.add_injectable("output_dir", output_dir) -def test_bad_custom_config_file(capsys): - - add_canonical_dirs() - - custom_config_file = os.path.join(os.path.dirname(__file__), 'configs', 'xlogging.yaml') - tracing.config_logger(custom_config_file=custom_config_file) - - logger = logging.getLogger('activitysim') - - file_handlers = [h for h in logger.handlers if type(h) is logging.FileHandler] - assert len(file_handlers) == 1 - asim_logger_baseFilename = file_handlers[0].baseFilename - - logger = logging.getLogger(__name__) - logger.info('test_bad_custom_config_file') - logger.info('log_info') - logger.warn('log_warn1') - - out, err = capsys.readouterr() - - # don't consume output - print out - - assert "could not find conf file" in out - assert 'log_warn1' in out - assert 'log_info' not in out - - close_handlers() - - logger.warn('log_warn2') - - with open(asim_logger_baseFilename, 'r') as content_file: - content = content_file.read() - assert 'log_warn1' in content - assert 'log_warn2' not in content - - def test_config_logger(capsys): add_canonical_dirs() @@ -109,31 +72,6 @@ def test_config_logger(capsys): assert 'log_warn2' not in content -def test_custom_config_logger(capsys): - - add_canonical_dirs() - - custom_config_file = os.path.join(os.path.dirname(__file__), 'configs', 'custom_logging.yaml') - tracing.config_logger(custom_config_file) - - logger = logging.getLogger('activitysim') - - logger.warn('custom_log_warn') - - asim_logger_filename = os.path.join(os.path.dirname(__file__), 'output', 'xasim.log') - - with open(asim_logger_filename, 'r') as content_file: - content = content_file.read() - assert 'custom_log_warn' in content - - out, err = capsys.readouterr() - - # don't consume output - print out - - assert 'custom_log_warn' in out - - def test_print_summary(capsys): add_canonical_dirs() diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index cd0da1cb6..415cdef43 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -15,7 +15,6 @@ from activitysim.core import inject -import inject_defaults import config @@ -55,7 +54,7 @@ def print_elapsed_time(msg=None, t0=None, debug=False): return t1 -def delete_output_files(file_type, output_dir=None): +def delete_output_files(file_type, ignore=None): """ Delete files in output directory of specified type @@ -69,14 +68,22 @@ def delete_output_files(file_type, output_dir=None): Nothing """ - if output_dir is None: - output_dir = inject.get_injectable('output_dir') + output_dir = inject.get_injectable('output_dir') + + if ignore: + ignore = [os.path.realpath(p) for p in ignore] + print "delete_output_files ignoring", ignore logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) for the_file in os.listdir(output_dir): if the_file.endswith(file_type): file_path = os.path.join(output_dir, the_file) + + if ignore and os.path.realpath(file_path) in ignore: + logger.info("delete_output_files ignoring %s" % file_path) + continue + try: if os.path.isfile(file_path): os.unlink(file_path) @@ -84,7 +91,7 @@ def delete_output_files(file_type, output_dir=None): print(e) -def delete_csv_files(output_dir=None): +def delete_csv_files(): """ Delete CSV files in output_dir @@ -92,36 +99,27 @@ def delete_csv_files(output_dir=None): ------- Nothing """ - delete_output_files(CSV_FILE_TYPE, output_dir) + delete_output_files(CSV_FILE_TYPE) -def config_logger(custom_config_file=None, basic=False): +def config_logger(basic=False): """ Configure logger - if log_config_file is not supplied then look for conf file in configs_dir - - if not found use basicConfig - - Parameters - ---------- - custom_config_file: str - custom config filename - basic: boolean - basic setup + look for conf file in configs_dir, if not found use basicConfig Returns ------- Nothing """ - log_config_file = None - if custom_config_file and os.path.isfile(custom_config_file): - log_config_file = custom_config_file - elif not basic: - # look for conf file in configs_dir + # look for conf file in configs_dir + log_config_file = None + if not basic: log_config_file = config.config_file_path(LOGGING_CONF_FILE_NAME, mandatory=False) + print "log_config_file", log_config_file + if log_config_file: with open(log_config_file) as f: config_dict = yaml.load(f) @@ -133,9 +131,6 @@ def config_logger(custom_config_file=None, basic=False): logger = logging.getLogger(ASIM_LOGGER) - if custom_config_file and not os.path.isfile(custom_config_file): - logger.error("#\n#\n#\nconfig_logger could not find conf file '%s'" % custom_config_file) - if log_config_file: logger.info("Read logging configuration from: %s" % log_config_file) else: diff --git a/example/configs/logging.yaml b/example/configs/logging.yaml index a7e8155dc..29a9f6fb8 100644 --- a/example/configs/logging.yaml +++ b/example/configs/logging.yaml @@ -15,8 +15,7 @@ logging: loggers: activitysim: - #level: !!python/name:logging.INFO - level: !!python/name:logging.DEBUG + level: !!python/name:logging.INFO handlers: [console, logfile] propagate: false @@ -38,8 +37,7 @@ logging: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - #level: !!python/name:logging.NOTSET - level: !!python/name:logging.DEBUG + level: !!python/name:logging.NOTSET formatters: diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 83813ad50..80a7bc66b 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -6,17 +6,17 @@ SIMULATE_CHOOSER_COLUMNS: - is_highschool - is_gradeschool - -LOGSUM_SETTINGS: tour_mode_choice.yaml -LOGSUM_PREPROCESSOR: nontour_preprocessor - - # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +SAMPLE_SPEC: school_location_sample.csv +SPEC: school_location.csv + +LOGSUM_SETTINGS: tour_mode_choice.yaml +LOGSUM_PREPROCESSOR: nontour_preprocessor annotate_persons: SPEC: annotate_persons_school diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 508e70d9a..755525ceb 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -67,8 +67,12 @@ output_tables: prefix: final_ tables: - checkpoints -# - tours +# - accessibility +# - land_use +# - households +# - persons # - trips +# - tours # area_types less than this are considered urban urban_threshold: 4 diff --git a/example/configs/stop_frequency_annotate_tours_preprocessor.csv b/example/configs/stop_frequency_annotate_tours_preprocessor.csv index 9e658848e..df486eede 100644 --- a/example/configs/stop_frequency_annotate_tours_preprocessor.csv +++ b/example/configs/stop_frequency_annotate_tours_preprocessor.csv @@ -36,13 +36,13 @@ Number of subtours in the tour,num_atwork_subtours,"df.atwork_subtour_frequency. Number of hh shop tours including joint,num_hh_shop_tours,"reindex_i(df[df.tour_type==SHOP_TOUR].groupby('household_id').size(), df.person_id)" Number of hh maint tours including joint,num_hh_maint_tours,"reindex_i(df[df.tour_type==MAINT_TOUR].groupby('household_id').size(), df.person_id)" # FIXME - need hhacc and pracc,, -,_outbound_is_peak,"(df.start>=setting('start_am_peak')) & (df.end<=setting('end_am_peak'))" +tourStartsInPeakPeriod,_tour_starts_in_peak,skim_time_period_label(df.start) == 'AM' AccesibilityAtOrigin fallback,hhacc,0 -AccesibilityAtOrigin if transit,hhacc,"hhacc.where(~tour_mode_is_transit, df.trPkRetail.where(_outbound_is_peak, df.trOpRetail))" +AccesibilityAtOrigin if transit,hhacc,"hhacc.where(~tour_mode_is_transit, df.trPkRetail.where(_tour_starts_in_peak, df.trOpRetail))" AccesibilityAtOrigin if non_motorized,hhacc,"hhacc.where(~tour_mode_is_non_motorized, df.nmRetail)" AccesibilityADestination fallback,pracc,0 AccesibilityADestination peak transit,_dest_trPkRetail,"reindex(accessibility.trPkRetail, df.destination)" AccesibilityADestination off-peak transit,_dest_trOpRetail,"reindex(accessibility.trOpRetail, df.destination)" -AccesibilityAtDestination if transit,pracc,"pracc.where(~tour_mode_is_transit, _dest_trPkRetail.where(_outbound_is_peak, _dest_trOpRetail))" +AccesibilityAtDestination if transit,pracc,"pracc.where(~tour_mode_is_transit, _dest_trPkRetail.where(_tour_starts_in_peak, _dest_trOpRetail))" AccesibilityAtDestination if non_motorized,pracc,"pracc.where(~tour_mode_is_non_motorized, reindex(accessibility.nmRetail, df.destination))" ,destination_area_type,"reindex(land_use.area_type, df.destination)" diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml new file mode 100644 index 000000000..171b51fa6 --- /dev/null +++ b/example_mp/configs/logging.yaml @@ -0,0 +1,55 @@ +# Config for logging +# ------------------ +# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema + +logging: + version: 1 + disable_existing_loggers: true + + + # Configuring the default (root) logger is highly recommended + root: + level: !!python/name:logging.DEBUG + handlers: [console, logfile] + + loggers: + + activitysim: + level: !!python/name:logging.DEBUG + handlers: [console, logfile] + propagate: false + + orca: + level: !!python/name:logging.WARN + handlers: [console, logfile] + propagate: false + + handlers: + + logfile: + class: logging.FileHandler + filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] + mode: w + formatter: fileFormatter + level: !!python/name:logging.NOTSET + + console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + #level: !!python/name:logging.NOTSET + level: !!python/name:logging.DEBUG + + formatters: + + simpleFormatter: + class: !!python/name:logging.Formatter + # format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' + format: !!python/object/apply:tasks.console_logger_format ['%(levelname)s - %(name)s - %(message)s'] + datefmt: '%d/%m/%Y %H:%M:%S' + + fileFormatter: + class: !!python/name:logging.Formatter + format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' + datefmt: '%d/%m/%Y %H:%M:%S' + diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml new file mode 100644 index 000000000..b5abac746 --- /dev/null +++ b/example_mp/configs/settings.yaml @@ -0,0 +1,114 @@ + +inherit_settings: True + +#number of households to simulate +households_sample_size: 0 + +#trace household id; comment out for no trace +# household with all tour categories +trace_hh_id: 1269102 + +# trace origin, destination in accessibility calculation +trace_od: [5, 11] + +#internal settings +chunk_size: 2000000000 + +# comment out or set false to disable variability check in simple_simulate and interaction_simulate +check_for_variability: False + +#models: +# - initialize_landuse +# - compute_accessibility +# - initialize_households +# - school_location_sample +# - school_location_logsums +# - school_location_simulate +# - workplace_location_sample +# - workplace_location_logsums +# - workplace_location_simulate +# - auto_ownership_simulate +# - cdap_simulate +# - mandatory_tour_frequency +# - mandatory_tour_scheduling +# - joint_tour_frequency +# - joint_tour_composition +# - joint_tour_participation +# - joint_tour_destination_sample +# - joint_tour_destination_logsums +# - joint_tour_destination_simulate +# - joint_tour_scheduling +# - non_mandatory_tour_frequency +# - non_mandatory_tour_destination +# - non_mandatory_tour_scheduling +# - tour_mode_choice_simulate +# - atwork_subtour_frequency +# - atwork_subtour_destination_sample +# - atwork_subtour_destination_logsums +# - atwork_subtour_destination_simulate +# - atwork_subtour_scheduling +# - atwork_subtour_mode_choice +# - stop_frequency +# - trip_purpose +# - trip_destination +# - trip_purpose_and_destination +# - trip_scheduling +# - trip_mode_choice +# - write_data_dictionary +# - write_tables + +#resume_after: + + +# comment out to run single-threaded +multiprocess: False + +multiprocess_steps: + - label: mp_initialize + begin: initialize_landuse + - label: mp_households + begin: school_location_sample + num_processes: 2 + slice: + tables: + - households + - persons + - label: mp_summarize + begin: write_data_dictionary + +#multiprocess_steps: +# - label: mp_initialize_landuse +# begin: initialize_landuse +# - label: mp_accessibility +# begin: compute_accessibility +# num_processes: 2 +# slice: +# tables: +# - accessibility +# except: +# - land_use +# - label: mp_initialize_households +# begin: initialize_households +# - label: mp_households +# begin: school_location_sample +# # num_processes = 0 means use all available cpus +# num_processes: 0 +# slice: +# tables: +# - households +# - persons +# - label: mp_summarize +# begin: write_data_dictionary + + +output_tables: + action: include + prefix: final_ + tables: + - checkpoints + - accessibility + - land_use + - households + - persons + - trips + - tours diff --git a/example_mp/configs/trip_purpose_and_destination.yaml b/example_mp/configs/trip_purpose_and_destination.yaml new file mode 100644 index 000000000..283f924cb --- /dev/null +++ b/example_mp/configs/trip_purpose_and_destination.yaml @@ -0,0 +1,8 @@ + +inherit_settings: True + +MAX_ITERATIONS: 5 + +# drop failed trips and cleanup failed trip leg_mates for consistency +# (i.e. adjust trip_count, trip_num, first for missing failed trips) +CLEANUP: True diff --git a/example_mp/configs/trip_scheduling.yaml b/example_mp/configs/trip_scheduling.yaml new file mode 100644 index 000000000..91959a745 --- /dev/null +++ b/example_mp/configs/trip_scheduling.yaml @@ -0,0 +1,8 @@ + +inherit_settings: True + +MAX_ITERATIONS: 100 + +#FAILFIX: drop_and_cleanup +FAILFIX: choose_most_initial + diff --git a/example_mp/output/.gitignore b/example_mp/output/.gitignore new file mode 100644 index 000000000..4a2323ec0 --- /dev/null +++ b/example_mp/output/.gitignore @@ -0,0 +1,5 @@ +*.csv +*.log +*.prof +*.h5 +*.txt diff --git a/example_mp/simulation.py b/example_mp/simulation.py new file mode 100644 index 000000000..5462eafd3 --- /dev/null +++ b/example_mp/simulation.py @@ -0,0 +1,65 @@ +import os +import sys +import logging +import multiprocessing + +from activitysim.core import inject +from activitysim.core import tracing +from activitysim.core import config +from activitysim.core import pipeline +# from activitysim import abm + +import tasks + +logger = logging.getLogger('activitysim') + + +def cleanup_output_files(): + + active_log_files = \ + [h.baseFilename for h in logger.root.handlers if isinstance(h, logging.FileHandler)] + tracing.delete_output_files('log', ignore=active_log_files) + + tracing.delete_output_files('h5') + tracing.delete_output_files('csv') + tracing.delete_output_files('txt') + + +if __name__ == '__main__': + + # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') + inject.add_injectable('data_dir', '../example/data') + inject.add_injectable('configs_dir', ['configs', '../example/configs']) + # inject.add_injectable('configs_dir', '../example/configs') + + config.handle_standard_args() + tracing.config_logger() + + # cleanup if not resuming + if not config.setting('resume_after', False): + cleanup_output_files() + + run_list = tasks.get_run_list() + + tasks.print_run_list(run_list) + + t0 = tracing.print_elapsed_time() + + if run_list['multiprocess']: + logger.info("run multiprocess simulation") + logger.info("main process pid : %s" % os.getpid()) + logger.info("sys.executable : %s" % sys.executable) + logger.info("cpu count : %s" % multiprocessing.cpu_count()) + + tasks.run_multiprocess(run_list) + + else: + logger.info("run single process simulation") + + # tasks.run_simulation(run_list['models'], run_list['resume_after']) + pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) + + # tables will no longer be available after pipeline is closed + pipeline.close_pipeline() + + t0 = tracing.print_elapsed_time("mp_simulation", t0) diff --git a/example_mp/tasks.py b/example_mp/tasks.py new file mode 100644 index 000000000..08afb582c --- /dev/null +++ b/example_mp/tasks.py @@ -0,0 +1,634 @@ +import os +import time +import logging + +from collections import OrderedDict + +import numpy as np +import pandas as pd +import multiprocessing as mp + +from activitysim.core import inject +from activitysim.core import tracing +from activitysim.core import pipeline +from activitysim.core import config + +from activitysim.core.config import setting +from activitysim.core.config import handle_standard_args + +from activitysim import abm +from activitysim.abm.tables.skims import skims_to_load +from activitysim.abm.tables.skims import shared_buffer_for_skims +from activitysim.abm.tables.skims import load_skims + + +logger = logging.getLogger('activitysim') + + +def load_skim_data(skim_buffer): + + logger.info("load_skim_data") + + data_dir = inject.get_injectable('data_dir') + omx_file_path = os.path.join(data_dir, setting('skims_file')) + tags_to_load = setting('skim_time_periods')['labels'] + + skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + + skim_data = np.frombuffer(skim_buffer, dtype=skim_dtype).reshape(skims_shape) + + load_skims(omx_file_path, skim_keys, skim_data) + + return skim_data + + +def pipeline_table_keys(pipeline_store, checkpoint_name=None): + + checkpoints = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] + + if checkpoint_name: + # specified checkpoint row as series + i = checkpoints[checkpoints[pipeline.CHECKPOINT_NAME] == checkpoint_name].index[0] + checkpoint = checkpoints.loc[i] + else: + # last checkpoint row as series + checkpoint = checkpoints.iloc[-1] + checkpoint_name = checkpoint.loc[pipeline.CHECKPOINT_NAME] + + # series with table name as index and checkpoint_name as value + checkpoint_tables = checkpoint[~checkpoint.index.isin(pipeline.NON_TABLE_COLUMNS)] + + # omit dropped tables with empty checkpoint name + checkpoint_tables = checkpoint_tables[checkpoint_tables != ''] + + # hdf5 key is / + # FIXME - pathologically knows the format used by pipeline.pipeline_table_key() + checkpoint_tables = {table_name: table_name + '/' + checkpoint_name + for table_name, checkpoint_name in checkpoint_tables.iteritems()} + + # checkpoint name and series mapping table name to hdf5 key for tables in that checkpoint + return checkpoint_name, checkpoint_tables + + +def build_slice_rules(slice_info, tables): + + slicer_table_names = slice_info['tables'] + slicer_table_exceptions = slice_info.get('except', []) + primary_slicer = slicer_table_names[0] + + if primary_slicer not in tables: + raise RuntimeError("primary slice table '%s' not in pipeline" % primary_slicer) + + logger.debug("build_slice_rules tables %s" % tables.keys()) + logger.debug("build_slice_rules primary_slicer %s" % primary_slicer) + logger.debug("build_slice_rules slicer_table_names %s" % slicer_table_names) + logger.debug("build_slice_rules slicer_table_exceptions %s" % slicer_table_exceptions) + + # dict mapping slicer table_name to index name + # (also presumed to be name of ref col name in referencing table) + slicer_ref_cols = OrderedDict() + + # build slice rules for loaded tables + slice_rules = {} + for table_name, df in tables.iteritems(): + + rule = {} + if table_name == primary_slicer: + # slice primary apportion table + rule = {'slice_by': 'primary'} + elif table_name in slicer_table_exceptions: + rule['slice_by'] = None + else: + for slicer_table_name in slicer_ref_cols: + if df.index.name == tables[slicer_table_name].index.name: + # slice df with same index name as a known slicer + rule = {'slice_by': 'index', 'source': slicer_table_name} + else: + # if df has a column with same name as the ref_col (index) of a slicer? + try: + source, ref_col = next((t, c) + for t, c in slicer_ref_cols.iteritems() + if c in df.columns) + # then we can use that table to slice this df + rule = {'slice_by': 'column', + 'column': ref_col, + 'source': source} + except StopIteration: + rule['slice_by'] = None + + if rule['slice_by']: + # cascade sliceability + slicer_ref_cols[table_name] = df.index.name + + slice_rules[table_name] = rule + + print "## rule %s: %s" % (table_name, rule) + + for table_name in slice_rules: + logger.debug("%s: %s" % (table_name, slice_rules[table_name])) + + return slice_rules + + +def apportion_pipeline(sub_job_names, slice_info): + + pipeline_file_name = inject.get_injectable('pipeline_file_name') + + tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) + + # get last checkpoint from first job pipeline + pipeline_path = config.build_output_file_path(pipeline_file_name) + + logger.debug("apportion_pipeline pipeline_path: %s" % pipeline_path) + + # load all tables from pipeline + with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: + + checkpoints_df = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] + + # hdf5_keys is a dict mapping table_name to pipeline hdf5_key + checkpoint_name, hdf5_keys = pipeline_table_keys(pipeline_store) + + # ensure presence of slicer tables in pipeline + for table_name in tables: + if table_name not in hdf5_keys: + raise RuntimeError("slicer table %s not found in pipeline" % table_name) + + # load all tables from pipeline + for table_name, hdf5_key in hdf5_keys.iteritems(): + # new checkpoint for all tables the same + checkpoints_df[table_name] = checkpoint_name + # load the dataframe + tables[table_name] = pipeline_store[hdf5_key] + + logger.debug("loaded table %s %s" % (table_name, tables[table_name].shape)) + + # keep only the last row of checkpoints and patch the last checkpoint name + checkpoints_df = checkpoints_df.tail(1).copy() + checkpoints_df[tables.keys()] = checkpoint_name + + # build slice rules for loaded tables + slice_rules = build_slice_rules(slice_info, tables) + + # allocate sliced tables + num_sub_jobs = len(sub_job_names) + for i in range(num_sub_jobs): + + process_name = sub_job_names[i] + pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) + + # remove existing file + try: + os.unlink(pipeline_path) + except OSError as e: + pass + + with pd.HDFStore(pipeline_path, mode='a') as pipeline_store: + + # remember sliced_tables so we can cascade slicing to other tables + sliced_tables = {} + for table_name, rule in slice_rules.iteritems(): + + df = tables[table_name] + + if rule['slice_by'] == 'primary': + # slice primary apportion table by num_sub_jobs strides + # this hopefully yields a more random distribution + # (e.g.) households are ordered by size in input store + primary_df = df[np.asanyarray(range(df.shape[0])) % num_sub_jobs == i] + sliced_tables[table_name] = primary_df + elif rule['slice_by'] == 'index': + # slice a table with same index name as a known slicer + source_df = sliced_tables[rule['source']] + sliced_tables[table_name] = df.loc[source_df.index] + elif rule['slice_by'] == 'column': + # slice a table with a recognized slicer_column + source_df = sliced_tables[rule['source']] + sliced_tables[table_name] = df[df[rule['column']].isin(source_df.index)] + elif rule['slice_by'] is None: + # not all tables should be sliced (e.g. land_use) + sliced_tables[table_name] = df + else: + raise RuntimeError("Unrecognized slice rule '%s' for table %s" % + (rule['slice_by'], table_name)) + + hdf5_key = pipeline.pipeline_table_key(table_name, checkpoint_name) + + logger.debug("writing %s (%s) to %s in %s" % + (table_name, sliced_tables[table_name].shape, hdf5_key, pipeline_path)) + pipeline_store[hdf5_key] = sliced_tables[table_name] + + logger.debug("writing checkpoints (%s) to %s in %s" % + (checkpoints_df.shape, pipeline.CHECKPOINT_TABLE_NAME, pipeline_path)) + pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] = checkpoints_df + + +def coalesce_pipelines(sub_process_names, slice_info): + + pipeline_file_name = inject.get_injectable('pipeline_file_name') + + logger.debug("coalesce_pipelines to: %s" % pipeline_file_name) + + # tables that are identical in every pipeline and so don't need to be concatenated + + tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) + + # read all tables from first process pipeline + pipeline_path = \ + config.build_output_file_path(pipeline_file_name, use_prefix=sub_process_names[0]) + with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: + + # hdf5_keys is a dict mapping table_name to pipeline hdf5_key + checkpoint_name, hdf5_keys = pipeline_table_keys(pipeline_store) + + for table_name, hdf5_key in hdf5_keys.iteritems(): + print "loading table", table_name, hdf5_key + tables[table_name] = pipeline_store[hdf5_key] + + # use slice rules followed by apportion_pipeline to identify singleton tables + slice_rules = build_slice_rules(slice_info, tables) + singleton_table_names = [t for t, rule in slice_rules.iteritems() if rule['slice_by'] is None] + singleton_tables = {t: tables[t] for t in singleton_table_names} + omnibus_keys = {t: k for t, k in hdf5_keys.iteritems() if t not in singleton_table_names} + + logger.debug("coalesce_pipelines to: %s" % pipeline_file_name) + logger.debug("singleton_table_names: %s" % singleton_table_names) + logger.debug("omnibus_keys: %s" % omnibus_keys) + + # concat omnibus tables from all sub_processes + omnibus_tables = {table_name: [] for table_name in omnibus_keys} + for process_name in sub_process_names: + pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) + logger.info("coalesce pipeline %s" % pipeline_path) + + with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: + for table_name, hdf5_key in omnibus_keys.iteritems(): + omnibus_tables[table_name].append(pipeline_store[hdf5_key]) + + pipeline.open_pipeline() + + for table_name in singleton_tables: + df = singleton_tables[table_name] + logger.info("adding singleton table %s %s" % (table_name, df.shape)) + pipeline.replace_table(table_name, df) + for table_name in omnibus_tables: + df = pd.concat(omnibus_tables[table_name], sort=False) + logger.info("adding omnibus table %s %s" % (table_name, df.shape)) + pipeline.replace_table(table_name, df) + + pipeline.add_checkpoint(checkpoint_name) + + pipeline.close_pipeline() + + # pipeline_path = config.build_output_file_path(pipeline_file_name) + # with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: + # checkpoint_name, checkpoint_tables = pipeline_table_keys(pipeline_store) + # print "checkpoint_tables\n", checkpoint_tables + + +def run_simulation(models, resume_after=None): + + pipeline.run(models=models, resume_after=resume_after) + + # tables will no longer be available after pipeline is closed + pipeline.close_pipeline() + + +def allocate_shared_data(): + logger.info("allocate_shared_data") + + data_dir = inject.get_injectable('data_dir') + omx_file_path = os.path.join(data_dir, setting('skims_file')) + tags_to_load = setting('skim_time_periods')['labels'] + + # select the skims to load + skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + + skim_buffer = shared_buffer_for_skims(skims_shape, skim_dtype) + + return skim_buffer + + +def run_mp_simulation(skim_buffer, models, resume_after, num_processes, pipeline_prefix=False): + + handle_standard_args() + + # do this before config_logger so log file is named appropriately + process_name = mp.current_process().name + + logger.info("run_mp_simulation %s num_processes %s" % (process_name, num_processes)) + + inject.add_injectable("log_file_prefix", process_name) + if pipeline_prefix: + pipeline_prefix = process_name if pipeline_prefix is True else pipeline_prefix + logger.info("injecting pipeline_file_prefix '%s'" % pipeline_prefix) + inject.add_injectable("pipeline_file_prefix", pipeline_prefix) + + tracing.config_logger() + + inject.add_injectable('skim_buffer', skim_buffer) + + if num_processes > 1: + chunk_size = inject.get_injectable('chunk_size') + + if chunk_size: + new_chunk_size = int(round(chunk_size / float(num_processes))) + new_chunk_size = max(new_chunk_size, 1) + logger.info("run_mp_simulation adjusting chunk_size from %s to %s" % + (chunk_size, new_chunk_size)) + inject.add_injectable("chunk_size", new_chunk_size) + + run_simulation(models, resume_after) + + # try: + # run_simulation(models, resume_after) + # except Exception as e: + # print(e) + # logger.error("Error running simulation: %s" % (e,)) + # raise e + + +def mp_apportion_pipeline(sub_job_proc_names, slice_info): + process_name = mp.current_process().name + inject.add_injectable("log_file_prefix", process_name) + tracing.config_logger() + + apportion_pipeline(sub_job_proc_names, slice_info) + + +def mp_setup_skims(skim_buffer): + process_name = mp.current_process().name + inject.add_injectable("log_file_prefix", process_name) + tracing.config_logger() + + skim_data = load_skim_data(skim_buffer) + + +def mp_coalesce_pipelines(sub_job_proc_names, slice_info): + process_name = mp.current_process().name + inject.add_injectable("log_file_prefix", process_name) + tracing.config_logger() + + coalesce_pipelines(sub_job_proc_names, slice_info) + + +def mp_debug(injectables): + + for k,v in injectables.iteritems(): + inject.add_injectable(k, v) + + process_name = mp.current_process().name + inject.add_injectable("log_file_prefix", process_name) + tracing.config_logger() + + print "configs_dir", inject.get_injectable('configs_dir') + print "households_sample_size", setting('households_sample_size') + +def run_sub_process(p): + logger.info("running sub_process %s" % p.name) + p.start() + p.join() + # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) + + if p.exitcode: + logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) + raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) + + +def run_sub_procs(procs): + for p in procs: + logger.info("start process %s" % p.name) + p.start() + + while mp.active_children(): + logger.info("%s active processes" % len(mp.active_children())) + time.sleep(15) + + for p in procs: + p.join() + + error_procs = [p for p in procs if p.exitcode] + + return error_procs + + +def run_multiprocess(run_list): + + #fixme + # logger.info('running mp_debug') + # run_sub_process( + # mp.Process(target=mp_debug, name='mp_debug', + # args=({},)) + # ) + # bug + + logger.info('setup shared skim data') + shared_skim_data = allocate_shared_data() + run_sub_process( + mp.Process(target=mp_setup_skims, name='mp_setup_skims', args=(shared_skim_data,)) + ) + + resume_after = None + + for step_info in run_list['multiprocess_steps']: + + label = step_info['label'] + step_models = step_info['models'] + slice_info = step_info.get('slice', None) + + if not slice_info: + + num_processes = step_info['num_processes'] + assert num_processes == 1 + + logger.info('running step %s single process with %s models' % (label, len(step_models))) + + # unsliced steps run single-threaded + sub_proc_name = label + + run_sub_process( + mp.Process(target=run_mp_simulation, name=sub_proc_name, + args=(shared_skim_data, step_models, resume_after, num_processes)) + ) + + else: + + num_processes = step_info['num_processes'] + + logger.info('running step %s multiprocess with %s processes and %s models' % + (label, num_processes, len(step_models))) + + sub_proc_names = ["%s_sub-%s" % (label, i) for i in range(num_processes)] + + logger.info('apportioning households to sub_processes') + run_sub_process( + mp.Process(target=mp_apportion_pipeline, name='%s_apportion' % label, + args=(sub_proc_names, slice_info)) + ) + + logger.info('starting sub_processes') + error_procs = run_sub_procs([ + mp.Process(target=run_mp_simulation, name=process_name, + args=(shared_skim_data, step_models, resume_after, num_processes), + kwargs={'pipeline_prefix': True}) + for process_name in sub_proc_names + ]) + + if error_procs: + for p in error_procs: + logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) + raise RuntimeError("%s processes failed in %s" % (len(error_procs), label)) + + logger.info('coalescing sub_process pipelines') + run_sub_process( + mp.Process(target=mp_coalesce_pipelines, name='%s_coalesce' % label, + args=(sub_proc_names, slice_info)) + ) + + resume_after = '_' + + +def get_run_list(): + + models = setting('models', []) + resume_after = inject.get_injectable('resume_after', None) or setting('resume_after', None) + multiprocess = inject.get_injectable('multiprocess', False) or setting('multiprocess', False) + multiprocess_steps = setting('multiprocess_steps', []) + + if multiprocess and mp.cpu_count() == 1: + logger.warn("Can't multiprocess because there is only 1 cpu") + multiprocess = False + + run_list = { + 'models': models, + 'resume_after': resume_after, + 'multiprocess': multiprocess, + # 'multiprocess_steps': multiprocess_steps # add this later if multiprocess + } + + if not models or not isinstance(models, list): + raise RuntimeError('No models list in settings file') + + if resume_after not in models + ['_', None]: + raise RuntimeError("resume_after '%s' not in models list" % resume_after) + + if multiprocess: + + if resume_after: + raise RuntimeError("resume_after not implemented for multiprocessing") + + if not multiprocess_steps: + raise RuntimeError("multiprocess setting is %s but no multiprocess_steps setting" % + multiprocess) + + # check label, num_processes value and presence of slice info + labels = set() + for istep in range(len(multiprocess_steps)): + step = multiprocess_steps[istep] + + # check label + label = step.get('label', None) + if not label: + raise RuntimeError("missing label for step %s" + " in multiprocess_steps" % istep) + if label in labels: + raise RuntimeError("duplicate step label %s" + " in multiprocess_steps" % label) + labels.add(label) + + # validate num_processes and assign default + num_processes = step.get('num_processes', 0) + + if not isinstance(num_processes, int) or num_processes < 0: + raise RuntimeError("bad value (%s) for num_processes for step %s" + " in multiprocess_steps" % (num_processes, label)) + + if 'slice' in step: + if num_processes == 0: + logger.info("Setting num_processes = %s for step %s" % + (num_processes, label)) + num_processes = mp.cpu_count() + if num_processes == 1: + raise RuntimeError("num_processes = 1 but found slice info for step %s" + " in multiprocess_steps" % label) + if num_processes > mp.cpu_count(): + logger.warn("num_processes setting (%s) greater than cpu count (%s" % + (num_processes, mp.cpu_count())) + else: + if num_processes == 0: + num_processes = 1 + if num_processes > 1: + raise RuntimeError("num_processes > 1 but no slice info for step %s" + " in multiprocess_steps" % label) + + multiprocess_steps[istep]['num_processes'] = num_processes + + # determine index in models list of step starts + START = 'begin' + starts = [0] * len(multiprocess_steps) + for istep in range(len(multiprocess_steps)): + step = multiprocess_steps[istep] + + label = step['label'] + + slice = step.get('slice', None) + if slice: + if 'tables' not in slice: + raise RuntimeError("missing tables list for step %s" + " in multiprocess_steps" % istep) + + start = step.get(START, None) + if not label: + raise RuntimeError("missing %s tag for step '%s' (%s)" + " in multiprocess_steps" % + (START, label, istep)) + if start not in models: + raise RuntimeError("%s tag '%s' for step '%s' (%s) not in models list" % + (START, start, label, istep)) + + starts[istep] = models.index(start) + + if istep == 0 and starts[istep] != 0: + raise RuntimeError("%s tag '%s' for first is not first model in models list" % + (START, start, label, istep)) + + if istep > 0 and starts[istep] <= starts[istep - 1]: + raise RuntimeError("%s tag '%s' for step '%s' (%s)" + " falls before that of prior step in models list" % + (START, start, label, istep)) + + # build step model lists + starts.append(len(models)) # so last step gets remaining models in list + for istep in range(len(multiprocess_steps)): + multiprocess_steps[istep]['models'] = models[starts[istep]: starts[istep + 1]] + + run_list['multiprocess_steps'] = multiprocess_steps + + return run_list + + +def print_run_list(run_list): + + print "resume_after:", run_list['resume_after'] + print "multiprocess:", run_list['multiprocess'] + + if run_list['multiprocess']: + for step in run_list['multiprocess_steps']: + print "step:", step['label'] + print " num_processes:", step.get('num_processes', None) + print " models" + for m in step['models']: + print " - ", m + else: + print "models" + for m in run_list['models']: + print " - ", m +# 'multiprocess_steps': multiprocess_steps, + + +def console_logger_format(format): + + if inject.get_injectable('log_file_prefix', None): + format = "%(processName)-10s " + format + + return format From 82093a2338303cd74b4cb20f499ccbb2fb7eaebf Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 1 Oct 2018 20:32:04 -0400 Subject: [PATCH 014/122] eval_variables coerce result columns to numeric to avoid memory intensive df astype call --- activitysim/core/assign.py | 16 ++- activitysim/core/simulate.py | 161 +++++++++++++++++-------- activitysim/core/test/test_simulate.py | 24 ++-- activitysim/core/util.py | 15 +++ example_mp/configs/settings.yaml | 93 +++++++------- example_mp/tasks.py | 5 +- 6 files changed, 189 insertions(+), 125 deletions(-) diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 8cfd3e352..f8eb79072 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -197,7 +197,7 @@ def is_temp_scalar(target): def is_temp(target): return target.startswith('_') - def to_series(x, target=None): + def to_series(x): if x is None or np.isscalar(x): return pd.Series([x] * len(df.index), index=df.index) return x @@ -255,7 +255,7 @@ def to_series(x, target=None): # FIXME should whitelist globals for security? globals_dict = {} - values = to_series(eval(expression, globals_dict, _locals_dict), target=target) + expr_values = to_series(eval(expression, globals_dict, _locals_dict)) np.seterr(**save_err) np.seterrcall(saved_handler) @@ -266,17 +266,17 @@ def to_series(x, target=None): logger.error("assign_variables expression: %s = %s" % (str(target), str(expression))) - # values = to_series(None, target=target) + # expr_values = to_series(None, target=target) raise err if not is_temp(target): - variables[target] = values + variables[target] = expr_values if trace_results is not None: - trace_results[uniquify_key(trace_results, target)] = values[trace_rows] + trace_results[uniquify_key(trace_results, target)] = expr_values[trace_rows] # update locals to allows us to ref previously assigned targets - _locals_dict[target] = values + _locals_dict[target] = expr_values if trace_results is not None: @@ -288,8 +288,6 @@ def to_series(x, target=None): trace_results = pd.concat([df[trace_rows], trace_results], axis=1) # we stored result in dict - convert to df - variables = pd.DataFrame.from_dict(variables) - # in case items were numpy arrays not pandas series, fix index - variables.index = df.index + variables = util.df_from_dict(variables, index=df.index) return variables, trace_results, trace_assigned_locals diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index c46f897ac..1fea43fa1 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -3,8 +3,10 @@ from __future__ import print_function +import sys import os import logging +import time from collections import OrderedDict import numpy as np @@ -15,6 +17,7 @@ from . import tracing from . import pipeline from . import config +from . import util from . import assign @@ -133,7 +136,7 @@ def read_model_spec(model_settings=None, file_name=None, spec_dir=None, return spec -def eval_variables(exprs, df, locals_d=None, target_type=np.float64): +def eval_variables(exprs, df, locals_d=None): """ Evaluate a set of variable expressions from a spec in the context of a given data table. @@ -149,6 +152,10 @@ def eval_variables(exprs, df, locals_d=None, target_type=np.float64): Users should take care that these expressions must result in a Pandas Series. + # FIXME - for performance, it is essential that spec and expression_values + # FIXME - not contain booleans when dotted with spec values + # FIXME - or the arrays will be converted to dtype=object within dot() + Parameters ---------- exprs : sequence of str @@ -156,8 +163,6 @@ def eval_variables(exprs, df, locals_d=None, target_type=np.float64): locals_d : Dict This is a dictionary of local variables that will be the environment for an evaluation of an expression that begins with @ - target_type: dtype or None - type to coerce results or None if no coercion desired Returns ------- @@ -173,22 +178,36 @@ def eval_variables(exprs, df, locals_d=None, target_type=np.float64): locals_dict['df'] = df - def to_series(x): - if np.isscalar(x): - return pd.Series([x] * len(df), index=df.index) - return x + def to_array(x): + + if x is None or np.isscalar(x): + a = np.asanyarray([x] * len(df.index)) + elif isinstance(x, pd.Series): + # fixme + # assert x.index.equals(df.index) + # save a little RAM + a = x.values + + # FIXME - for performance, it is essential that spec and expression_values + # FIXME - not contain booleans when dotted with spec values + # FIXME - or the arrays will be converted to dtype=object within dot() + if not np.issubdtype(a.dtype, np.number): + a = a.astype(np.int8) + + return a values = OrderedDict() print('eval_variables', end='') # print ... for each expression for expr in exprs: - print('.', end='') # print ... + print('.', end='') + sys.stdout.flush() # logger.debug("eval_variables: %s" % expr) # logger.debug("eval_variables %s" % util.memory_info()) try: if expr.startswith('@'): - expr_values = to_series(eval(expr[1:], globals_dict, locals_dict)) + expr_values = to_array(eval(expr[1:], globals_dict, locals_dict)) else: - expr_values = df.eval(expr) + expr_values = to_array(df.eval(expr)) # read model spec should ensure uniqueness, otherwise we should uniquify assert expr not in values values[expr] = expr_values @@ -199,13 +218,7 @@ def to_series(x): raise err print() # print ... - values = pd.DataFrame.from_dict(values) - - # FIXME - for performance, it is essential that spec and expression_values - # FIXME - not contain booleans when dotted with spec values - # FIXME - or the arrays will be converted to dtype=object within dot() - if target_type is not None: - values = values.astype(target_type) + values = util.df_from_dict(values, index=df.index) return values @@ -779,49 +792,73 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): Index will be that of `choosers`, values will be logsum across spec column values """ + # FIXME - untested and not currently used by any models... + trace_label = tracing.extend_trace_label(trace_label, 'mnl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) check_for_variability = tracing.check_for_variability() logger.debug("running eval_mnl_logsums") - # t0 = tracing.print_elapsed_time() + t00 = t0 = print_elapsed_time() + + # trace choosers + if have_trace_targets: + tracing.trace_df(choosers, '%s.choosers' % trace_label) + cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) expression_values = eval_variables(spec.index, choosers, locals_d) - # t0 = tracing.print_elapsed_time("eval_variables", t0, debug=True) + t0 = print_elapsed_time("eval_variables", t0, debug=True) if check_for_variability: _check_for_variability(expression_values, trace_label) # utility values utilities = compute_utilities(expression_values, spec) - # t0 = tracing.print_elapsed_time("compute_utilities", t0, debug=True) + t0 = print_elapsed_time("compute_utilities", t0, debug=True) + + # trace expression_values + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) + cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) + del expression_values # done with expression_values + # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row - utils_arr = utilities.values.astype('float') - logsums = np.log(np.exp(utils_arr).sum(axis=1)) + logsums = np.log(np.exp(utilities.values).sum(axis=1)) logsums = pd.Series(logsums, index=choosers.index) + t0 = print_elapsed_time("logsums", t0, debug=True) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) - cum_size = chunk.log_df_size(trace_label, "utilities", utilities, cum_size) - chunk.log_chunk_size(trace_label, cum_size) - + # trace utilities if have_trace_targets: # add logsum to utilities for tracing utilities['logsum'] = logsums - - tracing.trace_df(choosers, '%s.choosers' % trace_label) tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) + cum_size = chunk.log_df_size(trace_label, "utilities", utilities, cum_size) + + # trace logsums + if have_trace_targets: tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) + cum_size = chunk.log_df_size(trace_label, "logsums", logsums, cum_size) + chunk.log_chunk_size(trace_label, cum_size) + + t0 = print_elapsed_time("end eval_mnl_logsums", t00, debug=True) return logsums +def print_elapsed_time(msg=None, t0=None, debug=False): + + msg = "%s %s" % (msg, util.memory_info()) + # print(msg) + # sys.stdout.write('\a') + # sys.stdout.flush() + return tracing.print_elapsed_time(msg, t0, debug=debug) + + def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): """ like eval_nl except return logsums instead of making choices @@ -834,51 +871,69 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): trace_label = tracing.extend_trace_label(trace_label, 'nl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - check_for_variability = tracing.check_for_variability() - logger.debug("running eval_nl_logsums") - # t0 = tracing.print_elapsed_time() + t00 = t0 = print_elapsed_time("begin eval_nl_logsums") + # trace choosers + if have_trace_targets: + tracing.trace_df(choosers, '%s.choosers' % trace_label) + cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) + + # - eval spec expressions # column names of expression_values match spec index values expression_values = eval_variables(spec.index, choosers, locals_d) - # t0 = tracing.print_elapsed_time("eval_variables", t0, debug=True) + t0 = print_elapsed_time("eval_variables", t0, debug=True) if check_for_variability: _check_for_variability(expression_values, trace_label) - # t0 = tracing.print_elapsed_time("_check_for_variability", t0, debug=True) + t0 = print_elapsed_time("_check_for_variability", t0, debug=True) - # raw utilities of all the leaves + # - raw utilities of all the leaves raw_utilities = compute_utilities(expression_values, spec) - # t0 = tracing.print_elapsed_time("expression_values.dot", t0, debug=True) + t0 = print_elapsed_time("compute_utilities", t0, debug=True) - # exponentiated utilities of leaves and nests + # trace expression_values + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) + cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) + del expression_values # done with expression_values + + # - exponentiated utilities of leaves and nests nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) - # t0 = tracing.print_elapsed_time("compute_nested_exp_utilities", t0, debug=True) + t0 = print_elapsed_time("compute_nested_exp_utilities", t0, debug=True) + # trace raw_utilities + if have_trace_targets: + tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, + column_labels=['alternative', 'utility']) + cum_size = chunk.log_df_size(trace_label, "raw_utilities", raw_utilities, cum_size) + del raw_utilities # done with raw_utilities + + # - logsums logsums = np.log(nested_exp_utilities.root) logsums = pd.Series(logsums, index=choosers.index) - # t0 = tracing.print_elapsed_time("logsums", t0, debug=True) - - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) - cum_size = chunk.log_df_size(trace_label, "raw_utilities", raw_utilities, cum_size) - cum_size = chunk.log_df_size(trace_label, "nested_exp_utils", nested_exp_utilities, cum_size) - chunk.log_chunk_size(trace_label, cum_size) + t0 = print_elapsed_time("logsums", t0, debug=True) + # trace nested_exp_utilities if have_trace_targets: # add logsum to nested_exp_utilities for tracing nested_exp_utilities['logsum'] = logsums - - tracing.trace_df(choosers, '%s.choosers' % trace_label) - tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, - column_labels=['alternative', 'utility']) tracing.trace_df(nested_exp_utilities, '%s.nested_exp_utilities' % trace_label, column_labels=['alternative', 'utility']) + cum_size = \ + chunk.log_df_size(trace_label, "nested_exp_utilities", nested_exp_utilities, cum_size) + del nested_exp_utilities # done with nested_exp_utilities + + # trace logsums + if have_trace_targets: tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) + cum_size = chunk.log_df_size(trace_label, "logsums", logsums, cum_size) + chunk.log_chunk_size(trace_label, cum_size) + + t0 = print_elapsed_time("end eval_nl_logsums", t00, debug=True) return logsums diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index d07315be3..835edaca1 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -4,6 +4,7 @@ import os.path import numpy.testing as npt +import numpy as np import pandas as pd import pandas.util.testing as pdt import pytest @@ -54,25 +55,18 @@ def test_read_model_spec(data_dir, spec_name): def test_eval_variables(spec, data): - result = simulate.eval_variables(spec.index, data, target_type=None) + result = simulate.eval_variables(spec.index, data) - expected_result = pd.DataFrame([ - [True, False, 4, 1], - [False, True, 4, 1], - [False, True, 5, 1]], + expected = pd.DataFrame([ + [1, 0, 4, 1], + [0, 1, 4, 1], + [0, 1, 5, 1]], index=data.index, columns=spec.index) - pdt.assert_frame_equal(result, expected_result, check_names=False) + for i in [0, 1]: + expected[expected.columns[i]] = expected[expected.columns[i]].astype(np.int8) - result = simulate.eval_variables(spec.index, data, target_type=float) - - expected_result = pd.DataFrame([ - [1.0, 0.0, 4.0, 1.0], - [0.0, 1.0, 4.0, 1.0], - [0.0, 1.0, 5.0, 1.0]], - index=data.index, columns=spec.index) - - pdt.assert_frame_equal(result, expected_result, check_names=False) + pdt.assert_frame_equal(result, expected, check_names=False) def test_simple_simulate(data, spec): diff --git a/activitysim/core/util.py b/activitysim/core/util.py index da590a464..697c6d362 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -301,3 +301,18 @@ def assign_in_place(df, df2): new_columns = [c for c in df2.columns if c not in df.columns] df[new_columns] = df2[new_columns] + + +def df_from_dict(values, index=None): + + df = pd.DataFrame.from_dict(values) + if index is not None: + df.index = index + + # 2x slower but users less peak RAM + # df = pd.DataFrame(index = index) + # for c in values.keys(): + # df[c] = values[c] + # del values[c] + + return df diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index b5abac746..a1d62e5b1 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -13,51 +13,52 @@ trace_od: [5, 11] #internal settings chunk_size: 2000000000 +# # comment out or set false to disable variability check in simple_simulate and interaction_simulate check_for_variability: False -#models: -# - initialize_landuse -# - compute_accessibility -# - initialize_households -# - school_location_sample -# - school_location_logsums -# - school_location_simulate -# - workplace_location_sample -# - workplace_location_logsums -# - workplace_location_simulate -# - auto_ownership_simulate -# - cdap_simulate -# - mandatory_tour_frequency -# - mandatory_tour_scheduling -# - joint_tour_frequency -# - joint_tour_composition -# - joint_tour_participation -# - joint_tour_destination_sample -# - joint_tour_destination_logsums -# - joint_tour_destination_simulate -# - joint_tour_scheduling -# - non_mandatory_tour_frequency -# - non_mandatory_tour_destination -# - non_mandatory_tour_scheduling -# - tour_mode_choice_simulate -# - atwork_subtour_frequency -# - atwork_subtour_destination_sample -# - atwork_subtour_destination_logsums -# - atwork_subtour_destination_simulate -# - atwork_subtour_scheduling -# - atwork_subtour_mode_choice -# - stop_frequency -# - trip_purpose -# - trip_destination -# - trip_purpose_and_destination -# - trip_scheduling -# - trip_mode_choice -# - write_data_dictionary -# - write_tables +models: + - initialize_landuse + - compute_accessibility + - initialize_households + - school_location_sample + - school_location_logsums + - school_location_simulate + - workplace_location_sample + - workplace_location_logsums + - workplace_location_simulate + - auto_ownership_simulate + - cdap_simulate + - mandatory_tour_frequency + - mandatory_tour_scheduling + - joint_tour_frequency + - joint_tour_composition + - joint_tour_participation + - joint_tour_destination_sample + - joint_tour_destination_logsums + - joint_tour_destination_simulate + - joint_tour_scheduling + - non_mandatory_tour_frequency + - non_mandatory_tour_destination + - non_mandatory_tour_scheduling + - tour_mode_choice_simulate + - atwork_subtour_frequency + - atwork_subtour_destination_sample + - atwork_subtour_destination_logsums + - atwork_subtour_destination_simulate + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + - write_data_dictionary + - write_tables -#resume_after: +resume_after: school_location_simulate # comment out to run single-threaded @@ -106,9 +107,9 @@ output_tables: prefix: final_ tables: - checkpoints - - accessibility - - land_use - - households - - persons - - trips - - tours +# - accessibility +# - land_use +# - households +# - persons +# - trips +# - tours diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 08afb582c..9b2eb768e 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -374,7 +374,7 @@ def mp_coalesce_pipelines(sub_job_proc_names, slice_info): def mp_debug(injectables): - for k,v in injectables.iteritems(): + for k, v in injectables.iteritems(): inject.add_injectable(k, v) process_name = mp.current_process().name @@ -384,6 +384,7 @@ def mp_debug(injectables): print "configs_dir", inject.get_injectable('configs_dir') print "households_sample_size", setting('households_sample_size') + def run_sub_process(p): logger.info("running sub_process %s" % p.name) p.start() @@ -414,7 +415,7 @@ def run_sub_procs(procs): def run_multiprocess(run_list): - #fixme + # fixme # logger.info('running mp_debug') # run_sub_process( # mp.Process(target=mp_debug, name='mp_debug', From 1ee96eee6ba440df3ffe7fdbe9aed2b579a5cdd2 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 2 Oct 2018 09:13:10 -0400 Subject: [PATCH 015/122] mp chunk_size in step_info --- activitysim/core/simulate.py | 2 +- example_mp/configs/settings.yaml | 10 ++--- example_mp/simulation.py | 7 +++- example_mp/tasks.py | 72 +++++++++++++++++--------------- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 1fea43fa1..1160b7dd5 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -852,7 +852,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): def print_elapsed_time(msg=None, t0=None, debug=False): - msg = "%s %s" % (msg, util.memory_info()) + # msg = "%s %s" % (msg, util.memory_info()) # print(msg) # sys.stdout.write('\a') # sys.stdout.flush() diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index a1d62e5b1..6ee474475 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -11,10 +11,6 @@ trace_hh_id: 1269102 # trace origin, destination in accessibility calculation trace_od: [5, 11] -#internal settings -chunk_size: 2000000000 -# - # comment out or set false to disable variability check in simple_simulate and interaction_simulate check_for_variability: False @@ -58,18 +54,20 @@ models: - write_data_dictionary - write_tables -resume_after: school_location_simulate +#resume_after: school_location_simulate # comment out to run single-threaded multiprocess: False +chunk_size: 2000000000 multiprocess_steps: - label: mp_initialize begin: initialize_landuse - label: mp_households begin: school_location_sample - num_processes: 2 + num_processes: 3 + #chunk_size: 1000000000 slice: tables: - households diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 5462eafd3..1f2ce4f36 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -40,8 +40,8 @@ def cleanup_output_files(): cleanup_output_files() run_list = tasks.get_run_list() - - tasks.print_run_list(run_list) + with open(config.output_file_path('run_list.txt'), 'w') as file: + tasks.print_run_list(run_list, file) t0 = tracing.print_elapsed_time() @@ -51,6 +51,9 @@ def cleanup_output_files(): logger.info("sys.executable : %s" % sys.executable) logger.info("cpu count : %s" % multiprocessing.cpu_count()) + tasks.print_run_list(run_list, sys.stdout) + bug + tasks.run_multiprocess(run_list) else: diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 9b2eb768e..9f463d143 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -309,7 +309,11 @@ def allocate_shared_data(): return skim_buffer -def run_mp_simulation(skim_buffer, models, resume_after, num_processes, pipeline_prefix=False): +def run_mp_simulation(skim_buffer, step_info, resume_after, pipeline_prefix=False): + + models = step_info['models'] + num_processes = step_info['num_processes'] + chunk_size = step_info['chunk_size'] handle_standard_args() @@ -327,16 +331,7 @@ def run_mp_simulation(skim_buffer, models, resume_after, num_processes, pipeline tracing.config_logger() inject.add_injectable('skim_buffer', skim_buffer) - - if num_processes > 1: - chunk_size = inject.get_injectable('chunk_size') - - if chunk_size: - new_chunk_size = int(round(chunk_size / float(num_processes))) - new_chunk_size = max(new_chunk_size, 1) - logger.info("run_mp_simulation adjusting chunk_size from %s to %s" % - (chunk_size, new_chunk_size)) - inject.add_injectable("chunk_size", new_chunk_size) + inject.add_injectable("chunk_size", chunk_size) run_simulation(models, resume_after) @@ -434,30 +429,27 @@ def run_multiprocess(run_list): for step_info in run_list['multiprocess_steps']: label = step_info['label'] - step_models = step_info['models'] slice_info = step_info.get('slice', None) if not slice_info: - num_processes = step_info['num_processes'] - assert num_processes == 1 + assert step_info['num_processes'] == 1 - logger.info('running step %s single process with %s models' % (label, len(step_models))) + logger.info('running step %s single process' % (label,)) # unsliced steps run single-threaded sub_proc_name = label run_sub_process( mp.Process(target=run_mp_simulation, name=sub_proc_name, - args=(shared_skim_data, step_models, resume_after, num_processes)) + args=(shared_skim_data, step_info, resume_after)) ) else: num_processes = step_info['num_processes'] - logger.info('running step %s multiprocess with %s processes and %s models' % - (label, num_processes, len(step_models))) + logger.info('running step %s multiprocess with %s processes' % (label, num_processes,)) sub_proc_names = ["%s_sub-%s" % (label, i) for i in range(num_processes)] @@ -470,7 +462,7 @@ def run_multiprocess(run_list): logger.info('starting sub_processes') error_procs = run_sub_procs([ mp.Process(target=run_mp_simulation, name=process_name, - args=(shared_skim_data, step_models, resume_after, num_processes), + args=(shared_skim_data, step_info, resume_after), kwargs={'pipeline_prefix': True}) for process_name in sub_proc_names ]) @@ -494,6 +486,7 @@ def get_run_list(): models = setting('models', []) resume_after = inject.get_injectable('resume_after', None) or setting('resume_after', None) multiprocess = inject.get_injectable('multiprocess', False) or setting('multiprocess', False) + global_chunk_size = setting('chunk_size', 0) multiprocess_steps = setting('multiprocess_steps', []) if multiprocess and mp.cpu_count() == 1: @@ -522,12 +515,12 @@ def get_run_list(): raise RuntimeError("multiprocess setting is %s but no multiprocess_steps setting" % multiprocess) - # check label, num_processes value and presence of slice info + # check label, num_processes, chunk_size and presence of slice info labels = set() for istep in range(len(multiprocess_steps)): step = multiprocess_steps[istep] - # check label + # - validate label label = step.get('label', None) if not label: raise RuntimeError("missing label for step %s" @@ -537,7 +530,7 @@ def get_run_list(): " in multiprocess_steps" % label) labels.add(label) - # validate num_processes and assign default + # - validate num_processes and assign default num_processes = step.get('num_processes', 0) if not isinstance(num_processes, int) or num_processes < 0: @@ -564,7 +557,18 @@ def get_run_list(): multiprocess_steps[istep]['num_processes'] = num_processes - # determine index in models list of step starts + # - validate chunk_size and assign default + chunk_size = step.get('chunk_size', None) + if chunk_size is None: + if global_chunk_size > 0 and num_processes > 1: + chunk_size = int(round(global_chunk_size / float(num_processes))) + chunk_size = max(chunk_size, 1) + else: + chunk_size = global_chunk_size + + multiprocess_steps[istep]['chunk_size'] = chunk_size + + # - determine index in models list of step starts START = 'begin' starts = [0] * len(multiprocess_steps) for istep in range(len(multiprocess_steps)): @@ -598,7 +602,7 @@ def get_run_list(): " falls before that of prior step in models list" % (START, start, label, istep)) - # build step model lists + # - build step model lists starts.append(len(models)) # so last step gets remaining models in list for istep in range(len(multiprocess_steps)): multiprocess_steps[istep]['models'] = models[starts[istep]: starts[istep + 1]] @@ -608,23 +612,23 @@ def get_run_list(): return run_list -def print_run_list(run_list): +def print_run_list(run_list, file): - print "resume_after:", run_list['resume_after'] - print "multiprocess:", run_list['multiprocess'] + print >> file, "resume_after:", run_list['resume_after'] + print >> file, "multiprocess:", run_list['multiprocess'] if run_list['multiprocess']: for step in run_list['multiprocess_steps']: - print "step:", step['label'] - print " num_processes:", step.get('num_processes', None) - print " models" + print >> file, "step:", step['label'] + print >> file, " num_processes:", step['num_processes'] + print >> file, " chunk_size:", step['chunk_size'] + print >> file, " models" for m in step['models']: - print " - ", m + print >> file, " - ", m else: - print "models" + print >> file, "models" for m in run_list['models']: - print " - ", m -# 'multiprocess_steps': multiprocess_steps, + print >> file, " - ", m def console_logger_format(format): From 129cfb33ff8b58d9dc3affa270c2336e34e1f7e1 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 2 Oct 2018 10:21:57 -0400 Subject: [PATCH 016/122] remove needless df astype in utils_to_probs --- activitysim/core/logit.py | 4 +++- example_mp/configs/settings.yaml | 4 ++-- example_mp/simulation.py | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 5284fd5ac..10b49b160 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -98,7 +98,9 @@ def utils_to_probs(utils, trace_label=None, exponentiated=False, allow_zero_prob """ trace_label = tracing.extend_trace_label(trace_label, 'utils_to_probs') - utils_arr = utils.values.astype('float') + # fixme - conversion to float not needed in either case? + # utils_arr = utils.values.astype('float') + utils_arr = utils.values if not exponentiated: utils_arr = np.exp(utils_arr) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 6ee474475..fbcc593d6 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -59,7 +59,7 @@ models: # comment out to run single-threaded multiprocess: False -chunk_size: 2000000000 +chunk_size: 4000000000 multiprocess_steps: - label: mp_initialize @@ -67,7 +67,7 @@ multiprocess_steps: - label: mp_households begin: school_location_sample num_processes: 3 - #chunk_size: 1000000000 + chunk_size: 1000000000 slice: tables: - households diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 1f2ce4f36..b412202c8 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -52,7 +52,6 @@ def cleanup_output_files(): logger.info("cpu count : %s" % multiprocessing.cpu_count()) tasks.print_run_list(run_list, sys.stdout) - bug tasks.run_multiprocess(run_list) From 91fb4f5cb899f37b8c5efc379e26ee48c0e35a85 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 2 Oct 2018 21:14:24 -0400 Subject: [PATCH 017/122] mp pass injectables to sub procs --- activitysim/abm/tables/skims.py | 4 +- example_mp/configs/settings.yaml | 2 +- example_mp/simulation.py | 5 ++- example_mp/tasks.py | 68 +++++++++++++++----------------- 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 81569c691..82c04db57 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -27,7 +27,7 @@ def skims_to_load(omx_file_path, tags_to_load=None): # select the skims to load with omx.open_file(omx_file_path) as omx_file: - omx_shape = omx_file.shape() + omx_shape = tuple(map(int, omx_file.shape())) # sometimes omx shape are floats! skim_keys = OrderedDict() for skim_name in omx_file.listMatrices(): @@ -53,7 +53,7 @@ def skims_to_load(omx_file_path, tags_to_load=None): def shared_buffer_for_skims(skims_shape, skim_dtype, shared=False): - buffer_size = np.prod(skims_shape) + buffer_size = int(np.prod(skims_shape)) if np.issubdtype(skim_dtype, np.float64): typecode = 'd' diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index fbcc593d6..2e9b792e0 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -2,7 +2,7 @@ inherit_settings: True #number of households to simulate -households_sample_size: 0 +households_sample_size: 1000 #trace household id; comment out for no trace # household with all tour categories diff --git a/example_mp/simulation.py b/example_mp/simulation.py index b412202c8..bba26461d 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -35,6 +35,9 @@ def cleanup_output_files(): config.handle_standard_args() tracing.config_logger() + injectables = ['data_dir', 'configs_dir', 'output_dir'] + injectables = {k: inject.get_injectable(k) for k in injectables} + # cleanup if not resuming if not config.setting('resume_after', False): cleanup_output_files() @@ -53,7 +56,7 @@ def cleanup_output_files(): tasks.print_run_list(run_list, sys.stdout) - tasks.run_multiprocess(run_list) + tasks.run_multiprocess(run_list, injectables) else: logger.info("run single process simulation") diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 9f463d143..0d5cd4d02 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -309,7 +309,17 @@ def allocate_shared_data(): return skim_buffer -def run_mp_simulation(skim_buffer, step_info, resume_after, pipeline_prefix=False): +def setup_injectables_and_logging(injectables): + + for k, v in injectables.iteritems(): + inject.add_injectable(k, v) + + process_name = mp.current_process().name + inject.add_injectable("log_file_prefix", process_name) + tracing.config_logger() + + +def run_mp_simulation(injectables, skim_buffer, step_info, resume_after, pipeline_prefix=False): models = step_info['models'] num_processes = step_info['num_processes'] @@ -319,16 +329,14 @@ def run_mp_simulation(skim_buffer, step_info, resume_after, pipeline_prefix=Fals # do this before config_logger so log file is named appropriately process_name = mp.current_process().name - - logger.info("run_mp_simulation %s num_processes %s" % (process_name, num_processes)) - - inject.add_injectable("log_file_prefix", process_name) if pipeline_prefix: pipeline_prefix = process_name if pipeline_prefix is True else pipeline_prefix logger.info("injecting pipeline_file_prefix '%s'" % pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) - tracing.config_logger() + setup_injectables_and_logging(injectables) + + logger.info("run_mp_simulation %s num_processes %s" % (process_name, num_processes)) inject.add_injectable('skim_buffer', skim_buffer) inject.add_injectable("chunk_size", chunk_size) @@ -343,41 +351,27 @@ def run_mp_simulation(skim_buffer, step_info, resume_after, pipeline_prefix=Fals # raise e -def mp_apportion_pipeline(sub_job_proc_names, slice_info): - process_name = mp.current_process().name - inject.add_injectable("log_file_prefix", process_name) - tracing.config_logger() - +def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): + setup_injectables_and_logging(injectables) apportion_pipeline(sub_job_proc_names, slice_info) -def mp_setup_skims(skim_buffer): - process_name = mp.current_process().name - inject.add_injectable("log_file_prefix", process_name) - tracing.config_logger() - +def mp_setup_skims(injectables, skim_buffer): + setup_injectables_and_logging(injectables) skim_data = load_skim_data(skim_buffer) -def mp_coalesce_pipelines(sub_job_proc_names, slice_info): - process_name = mp.current_process().name - inject.add_injectable("log_file_prefix", process_name) - tracing.config_logger() - +def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): + setup_injectables_and_logging(injectables) coalesce_pipelines(sub_job_proc_names, slice_info) def mp_debug(injectables): - for k, v in injectables.iteritems(): - inject.add_injectable(k, v) - - process_name = mp.current_process().name - inject.add_injectable("log_file_prefix", process_name) - tracing.config_logger() + setup_injectables_and_logging(injectables) - print "configs_dir", inject.get_injectable('configs_dir') - print "households_sample_size", setting('households_sample_size') + for k in injectables: + print k, inject.get_injectable(k) def run_sub_process(p): @@ -408,20 +402,20 @@ def run_sub_procs(procs): return error_procs -def run_multiprocess(run_list): +def run_multiprocess(run_list, injectables): # fixme # logger.info('running mp_debug') # run_sub_process( - # mp.Process(target=mp_debug, name='mp_debug', - # args=({},)) + # mp.Process(target=mp_debug, name='mp_debug', args=(injectables,)) # ) # bug logger.info('setup shared skim data') shared_skim_data = allocate_shared_data() run_sub_process( - mp.Process(target=mp_setup_skims, name='mp_setup_skims', args=(shared_skim_data,)) + mp.Process(target=mp_setup_skims, name='mp_setup_skims', + args=(injectables, shared_skim_data,)) ) resume_after = None @@ -442,7 +436,7 @@ def run_multiprocess(run_list): run_sub_process( mp.Process(target=run_mp_simulation, name=sub_proc_name, - args=(shared_skim_data, step_info, resume_after)) + args=(injectables, shared_skim_data, step_info, resume_after)) ) else: @@ -456,13 +450,13 @@ def run_multiprocess(run_list): logger.info('apportioning households to sub_processes') run_sub_process( mp.Process(target=mp_apportion_pipeline, name='%s_apportion' % label, - args=(sub_proc_names, slice_info)) + args=(injectables, sub_proc_names, slice_info)) ) logger.info('starting sub_processes') error_procs = run_sub_procs([ mp.Process(target=run_mp_simulation, name=process_name, - args=(shared_skim_data, step_info, resume_after), + args=(injectables, shared_skim_data, step_info, resume_after), kwargs={'pipeline_prefix': True}) for process_name in sub_proc_names ]) @@ -475,7 +469,7 @@ def run_multiprocess(run_list): logger.info('coalescing sub_process pipelines') run_sub_process( mp.Process(target=mp_coalesce_pipelines, name='%s_coalesce' % label, - args=(sub_proc_names, slice_info)) + args=(injectables, sub_proc_names, slice_info)) ) resume_after = '_' From cb7e01e317d7194d8275132bdf611ac6b93c7f59 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 2 Oct 2018 22:54:13 -0400 Subject: [PATCH 018/122] suppress_intermediate_checkpoints for mp --- example_mp/configs/settings.yaml | 20 ++++++++++---------- example_mp/tasks.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 2e9b792e0..4383ba277 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -18,11 +18,11 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - school_location_sample - - school_location_logsums + - _school_location_sample + - _school_location_logsums - school_location_simulate - - workplace_location_sample - - workplace_location_logsums + - _workplace_location_sample + - _workplace_location_logsums - workplace_location_simulate - auto_ownership_simulate - cdap_simulate @@ -31,8 +31,8 @@ models: - joint_tour_frequency - joint_tour_composition - joint_tour_participation - - joint_tour_destination_sample - - joint_tour_destination_logsums + - _joint_tour_destination_sample + - _joint_tour_destination_logsums - joint_tour_destination_simulate - joint_tour_scheduling - non_mandatory_tour_frequency @@ -40,8 +40,8 @@ models: - non_mandatory_tour_scheduling - tour_mode_choice_simulate - atwork_subtour_frequency - - atwork_subtour_destination_sample - - atwork_subtour_destination_logsums + - _atwork_subtour_destination_sample + - _atwork_subtour_destination_logsums - atwork_subtour_destination_simulate - atwork_subtour_scheduling - atwork_subtour_mode_choice @@ -65,9 +65,9 @@ multiprocess_steps: - label: mp_initialize begin: initialize_landuse - label: mp_households - begin: school_location_sample + begin: _school_location_sample num_processes: 3 - chunk_size: 1000000000 + #chunk_size: 1000000000 slice: tables: - households diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 0d5cd4d02..982e20113 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -286,14 +286,6 @@ def coalesce_pipelines(sub_process_names, slice_info): # print "checkpoint_tables\n", checkpoint_tables -def run_simulation(models, resume_after=None): - - pipeline.run(models=models, resume_after=resume_after) - - # tables will no longer be available after pipeline is closed - pipeline.close_pipeline() - - def allocate_shared_data(): logger.info("allocate_shared_data") @@ -341,7 +333,8 @@ def run_mp_simulation(injectables, skim_buffer, step_info, resume_after, pipelin inject.add_injectable('skim_buffer', skim_buffer) inject.add_injectable("chunk_size", chunk_size) - run_simulation(models, resume_after) + pipeline.run(models=models, resume_after=resume_after) + pipeline.close_pipeline() # try: # run_simulation(models, resume_after) @@ -599,7 +592,14 @@ def get_run_list(): # - build step model lists starts.append(len(models)) # so last step gets remaining models in list for istep in range(len(multiprocess_steps)): - multiprocess_steps[istep]['models'] = models[starts[istep]: starts[istep + 1]] + step_models = models[starts[istep]: starts[istep + 1]] + + suppress_intermediate_checkpoints = True + if suppress_intermediate_checkpoints: + step_models = ['_' + m if m[0] != '_' and m != step_models[-1] else m for m in step_models] + + multiprocess_steps[istep]['models'] = step_models + run_list['multiprocess_steps'] = multiprocess_steps From 218735d1e4a66b69a16dc9ccee82f5c4e1895a37 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 5 Oct 2018 16:03:16 -0400 Subject: [PATCH 019/122] segment skim_data into blocks based on MAX_BLOCK_BYTES --- activitysim/abm/models/accessibility.py | 9 +- activitysim/abm/tables/skims.py | 216 +++++++++++++++++++----- activitysim/core/skim.py | 55 +++--- activitysim/core/test/test_skim.py | 12 +- example_mp/configs/settings.yaml | 12 +- example_mp/tasks.py | 87 ++++------ 6 files changed, 259 insertions(+), 132 deletions(-) diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 5249054f5..cbacd1176 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -36,8 +36,9 @@ class AccessibilitySkims(object): def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): - logger.info("init AccessibilitySkims with %d dest zones %d orig zones skim_data.shape %s" % - (len(dest_zones), len(orig_zones), skim_dict.skim_data.shape, )) + omx_shape = skim_dict.skim_info['omx_shape'] + logger.info("init AccessibilitySkims with %d dest zones %d orig zones omx_shape %s" % + (len(dest_zones), len(orig_zones), omx_shape, )) assert len(orig_zones) <= len(dest_zones) assert np.isin(orig_zones, dest_zones).all() @@ -47,7 +48,7 @@ def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): self.skim_dict = skim_dict self.transpose = transpose - if skim_dict.skim_data.shape[0] == len(orig_zones): + if omx_shape[0] == len(orig_zones): # no slicing required self.slice_map = None else: @@ -56,7 +57,7 @@ def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): # data = data[orig_map, :][:, dest_map] # <- RIGHT # data = data[np.ix_(orig_map, dest_map)] # <- ALSO RIGHT - skim_index = range(skim_dict.skim_data.shape[0]) + skim_index = range(omx_shape.shape[0]) orig_map = np.isin(skim_index, skim_dict.offset_mapper.map(orig_zones)) dest_map = np.isin(skim_index, skim_dict.offset_mapper.map(dest_zones)) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 82c04db57..f4d8df9f4 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -1,6 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +import sys import os import logging @@ -22,71 +25,191 @@ """ -def skims_to_load(omx_file_path, tags_to_load=None): +def get_skim_info(omx_file_path, tags_to_load=None): - # select the skims to load - with omx.open_file(omx_file_path) as omx_file: + # this is sys.maxint for p2.7 but no limit for p3 + # MAX_BLOCK_BYTES = 28880000 + MAX_BLOCK_BYTES = sys.maxint - omx_shape = tuple(map(int, omx_file.shape())) # sometimes omx shape are floats! - skim_keys = OrderedDict() + # Note: we load all skims except those with key2 not in tags_to_load + # Note: we require all skims to be of same dtype so they can share buffer - is that ok? + # fixme is it ok to require skims be all the same type? if so, is this the right choice? + skim_dtype = np.float32 + omx_name = os.path.splitext(os.path.basename(omx_file_path))[0] - for skim_name in omx_file.listMatrices(): - key1, sep, key2 = skim_name.partition('__') + with omx.open_file(omx_file_path) as omx_file: + omx_shape = tuple(map(int, omx_file.shape())) # sometimes omx shape are floats! + omx_skim_names = omx_file.listMatrices() - # ignore composite tags not in tags_to_load - if tags_to_load and sep and key2 not in tags_to_load: - continue + # - omx_keys dict maps skim key to omx_key + # DISTWALK: DISTWALK + # ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ... + omx_keys = OrderedDict() + for skim_name in omx_skim_names: + key1, sep, key2 = skim_name.partition('__') - key = (key1, key2) if sep else key1 - skim_keys[skim_name] = key + # - ignore composite tags not in tags_to_load + if tags_to_load and sep and key2 not in tags_to_load: + continue - num_skims = len(skim_keys.keys()) + skim_key = (key1, key2) if sep else key1 + omx_keys[skim_key] = skim_name + num_skims = len(omx_keys.keys()) skim_data_shape = omx_shape + (num_skims, ) - skim_dtype = np.float32 - logger.debug("skims_to_load from %s" % (omx_file_path, )) - logger.debug("skims_to_load skim_data_shape %s skim_dtype %s" % (skim_data_shape, skim_dtype)) + # - key1_subkeys dict maps key1 to dict of subkeys with that key1 + # DIST: {'DIST': 0} + # DRV_COM_WLK_BOARDS: {'MD': 1, 'AM': 0, 'PM': 2}, ... + key1_subkeys = OrderedDict() + for skim_key, omx_key in omx_keys.iteritems(): + if isinstance(skim_key, tuple): + key1, key2 = skim_key + else: + key1 = key2 = skim_key + key2_dict = key1_subkeys.setdefault(key1, {}) + key2_dict[key2] = len(key2_dict) + + # - blocks dict maps block name to blocksize (number of subkey skims in block) + # skims_0: 198, + # skims_1: 198, ... + # - key1_block_offsets dict maps key1 to (block, offset) of first skim with that key1 + # DISTWALK: (0, 2), + # DRV_COM_WLK_BOARDS: (0, 3), ... + + if MAX_BLOCK_BYTES: + max_block_items = MAX_BLOCK_BYTES // np.dtype(skim_dtype).itemsize + max_skims_per_block = max_block_items // np.prod(omx_shape) + else: + max_skims_per_block = num_skims - return skim_keys, skim_data_shape, skim_dtype + def block_name(block): + return "%s_%s" % (omx_name, block) + key1_block_offsets = OrderedDict() + blocks = OrderedDict() + block = offset = 0 + for key1, v in key1_subkeys.iteritems(): + num_subkeys = len(v) + if offset + num_subkeys > max_skims_per_block: # next block + blocks[block_name(block)] = offset + block += 1 + offset = 0 + key1_block_offsets[key1] = (block, offset) + offset += num_subkeys + blocks[block_name(block)] = offset # last block -def shared_buffer_for_skims(skims_shape, skim_dtype, shared=False): + # - block_offsets dict maps skim_key to (block, offset) of omx matrix + # DIST: (0, 0), + # ('DRV_COM_WLK_BOARDS', 'AM'): (0, 3), + # ('DRV_COM_WLK_BOARDS', 'MD') (0, 4), ... + block_offsets = OrderedDict() + for skim_key in omx_keys: - buffer_size = int(np.prod(skims_shape)) + if isinstance(skim_key, tuple): + key1, key2 = skim_key + else: + key1 = key2 = skim_key - if np.issubdtype(skim_dtype, np.float64): - typecode = 'd' - elif np.issubdtype(skim_dtype, np.float32): - typecode = 'f' - else: - raise RuntimeError("shared_buffer_for_skims unrecognized dtype %s" % skim_dtype) + block, key1_offset = key1_block_offsets[key1] + + key2_relative_offset = key1_subkeys.get(key1).get(key2) + + block_offsets[skim_key] = (block, key1_offset + key2_relative_offset) + + logger.debug("get_skim_info from %s" % (omx_file_path, )) + logger.debug("get_skim_info skim_dtype %s omx_shape %s num_skims %s num_blocks %s" % + (skim_dtype, omx_shape, num_skims, len(blocks))) + + skim_info = { + 'omx_name': omx_name, + 'omx_shape': omx_shape, + 'num_skims': num_skims, + 'dtype': skim_dtype, + 'omx_keys': omx_keys, + 'key1_block_offsets': key1_block_offsets, + 'block_offsets': block_offsets, + 'blocks': blocks, + } + + return skim_info + + +def buffer_for_skims(skim_info, shared=False): - logger.info("allocating shared buffer of size %s (%s)" % (buffer_size, skims_shape, )) + skim_dtype = skim_info['dtype'] + omx_shape = skim_info['omx_shape'] + blocks = skim_info['blocks'] - skim_buffer = mp.RawArray(typecode, buffer_size) + skim_buffer = {} + for block_name, block_size in blocks.iteritems(): + + buffer_size = np.prod(omx_shape) * block_size + + logger.info("allocating shared buffer %s for %s (%s) matrices" % + (block_name, buffer_size, omx_shape, )) + + if shared: + if np.issubdtype(skim_dtype, np.float64): + typecode = 'd' + elif np.issubdtype(skim_dtype, np.float32): + typecode = 'f' + else: + raise RuntimeError("buffer_for_skims unrecognized dtype %s" % skim_dtype) + + buffer = mp.RawArray(typecode, buffer_size) + else: + buffer = np.zeros(buffer_size, dtype=skim_dtype) + + skim_buffer[block_name] = buffer return skim_buffer -def load_skims(omx_file_path, skim_keys, skim_data): +def skim_data_from_buffer(skim_buffer, skim_info): + + assert type(skim_buffer) == dict + + omx_shape = skim_info['omx_shape'] + skim_dtype = skim_info['dtype'] + blocks = skim_info['blocks'] + + skim_data = [] + for block_name, block_size in blocks.iteritems(): + skims_shape = omx_shape + (block_size,) + block_buffer = skim_buffer[block_name] + assert len(block_buffer) == int(np.prod(skims_shape)) + block_data = np.frombuffer(block_buffer, dtype=skim_dtype).reshape(skims_shape) + skim_data.append(block_data) + + return skim_data + + +def load_skims(omx_file_path, skim_info, skim_buffer): + + skim_data = skim_data_from_buffer(skim_buffer, skim_info) + + block_offsets = skim_info['block_offsets'] + omx_keys = skim_info['omx_keys'] # read skims into skim_data with omx.open_file(omx_file_path) as omx_file: - n = 0 - for skim_name, key in skim_keys.iteritems(): + for skim_key, omx_key in omx_keys.iteritems(): - logger.debug("load_skims skim_name %s key %s" % (skim_name, key)) - - omx_data = omx_file[skim_name] + omx_data = omx_file[omx_key] assert np.issubdtype(omx_data.dtype, np.floating) + block, offset = block_offsets[skim_key] + block_data = skim_data[block] + + logger.debug("load_skims load omx_key %s skim_key %s to block %s offset %s" % + (omx_key, skim_key, block, offset)) + # this will trigger omx readslice to read and copy data to skim_data's buffer - a = skim_data[:, :, n] + a = block_data[:, :, offset] a[:] = omx_data[:] - n += 1 - logger.info("load_skims loaded %s skims from %s" % (n, omx_file_path)) + logger.info("load_skims loaded skims from %s" % (omx_file_path, )) @inject.injectable(cache=True) @@ -98,23 +221,28 @@ def skim_dict(data_dir, settings): logger.info("loading skim_dict from %s" % (omx_file_path, )) # select the skims to load - skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + skim_info = get_skim_info(omx_file_path, tags_to_load) - logger.debug("skim_data_shape %s skim_dtype %s" % (skims_shape, skim_dtype)) + logger.debug("omx_shape %s skim_dtype %s" % (skim_info['omx_shape'], skim_info['dtype'])) skim_buffer = inject.get_injectable('skim_buffer', None) if skim_buffer: logger.info('Using existing skim_buffer for skims') - skim_data = np.frombuffer(skim_buffer, dtype=skim_dtype).reshape(skims_shape) else: - skim_data = np.zeros(skims_shape, dtype=skim_dtype) - load_skims(omx_file_path, skim_keys, skim_data) + skim_buffer = buffer_for_skims(skim_info, shared=False) + load_skims(omx_file_path, skim_info, skim_buffer) + + skim_data = skim_data_from_buffer(skim_buffer, skim_info) - logger.info("skim_data dtype %s shape %s bytes %s (%s)" % - (skim_dtype, skims_shape, skim_data.nbytes, util.GB(skim_data.nbytes))) + block_names = skim_info['blocks'].keys() + for i in range(len(skim_data)): + block_name = block_names[i] + block_data = skim_data[i] + logger.info("block_name %s bytes %s (%s)" % + (block_name, block_data.nbytes, util.GB(block_data.nbytes))) # create skim dict - skim_dict = skim.SkimDict(skim_data, skim_keys.values()) + skim_dict = skim.SkimDict(skim_data, skim_info) skim_dict.offset_mapper.set_offset_int(-1) return skim_dict diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index b0da50332..b2b1276ce 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -140,15 +140,12 @@ class SkimDict(object): Note that keys are either strings or tuples of two strings (to support stacking of skims.) """ - def __init__(self, skim_data, skim_keys): - - skim_key_to_skim_num = OrderedDict(zip(skim_keys, range(len(skim_keys)))) + def __init__(self, skim_data, skim_info): + self.skim_info = skim_info self.skim_data = skim_data - self.skim_key_to_skim_num = skim_key_to_skim_num - self.num_skims = skim_data.shape[2] - self.offset_mapper = OffsetMapper() + self.offset_mapper = OffsetMapper() self.usage = set() def touch(self, key): @@ -169,13 +166,13 @@ def get(self, key): skim: Skim The skim object """ - assert key in self.skim_key_to_skim_num - n = self.skim_key_to_skim_num[key] - assert n < self.num_skims + + block, offset = self.skim_info['block_offsets'].get(key) + block_data = self.skim_data[block] self.touch(key) - data = self.skim_data[:, :, n] + data = block_data[:, :, offset] return SkimWrapper(data, self.offset_mapper) @@ -317,12 +314,28 @@ def __init__(self, skim_dict): self.offset_mapper = skim_dict.offset_mapper self.skim_dict = skim_dict - # build a dict mapping key1 to dict of key2 (dim3) indexes in skim_data + # - key1_blocks dict maps key1 to block number + # DISTWALK: 0, + # DRV_COM_WLK_BOARDS: 0, ... + key1_block_offsets = skim_dict.skim_info['key1_block_offsets'] + self.key1_blocks = {k: v[0] for k, v in key1_block_offsets.iteritems()} + + # - skim_dim3 dict maps key1 to dict of key2 absolute offsets into block + # DRV_COM_WLK_BOARDS: {'MD': 4, 'AM': 3, 'PM': 5}, ... + block_offsets = skim_dict.skim_info['block_offsets'] skim_dim3 = OrderedDict() - for key, n in skim_dict.skim_key_to_skim_num.iteritems(): - if isinstance(key, tuple): - key1, key2 = key - skim_dim3.setdefault(key1, OrderedDict())[key2] = n + for skim_key in block_offsets: + + if not isinstance(skim_key, tuple): + continue + + key1, key2 = skim_key + block, offset = block_offsets[skim_key] + + assert block == self.key1_blocks[key1] + + skim_dim3.setdefault(key1, OrderedDict())[key2] = offset + self.skim_dim3 = skim_dim3 logger.info("SkimStack.__init__ loaded %s keys with %s total skims" @@ -332,22 +345,18 @@ def __init__(self, skim_dict): self.usage = set() def touch(self, key): - self.usage.add(key) - # def __str__(self): - # - # return "\n".join( - # "%s %s" % (key1, sub_dict) - # for key1, sub_dict in self.skim_dim3.iteritems()) - def lookup(self, orig, dest, dim3, key): orig = self.offset_mapper.map(orig) dest = self.offset_mapper.map(dest) + assert key in self.key1_blocks, "SkimStack key %s missing" % key assert key in self.skim_dim3, "SkimStack key %s missing" % key - stacked_skim_data = self.skim_dict.skim_data + + block = self.key1_blocks[key] + stacked_skim_data = self.skim_dict.skim_data[block] skim_keys_to_indexes = self.skim_dim3[key] self.touch(key) diff --git a/activitysim/core/test/test_skim.py b/activitysim/core/test/test_skim.py index 4de21927b..ed01e8cb0 100644 --- a/activitysim/core/test/test_skim.py +++ b/activitysim/core/test/test_skim.py @@ -77,7 +77,11 @@ def test_skims(data): skim_data[:, :, 0] = data skim_data[:, :, 1] = data*10 - skim_dict = skim.SkimDict(skim_data, ['AM', 'PM']) + skim_info = { + 'block_offsets': {'AM': (0, 0), 'PM': (0, 1)} + } + + skim_dict = skim.SkimDict([skim_data], skim_info) skims = skim_dict.wrap("taz_l", "taz_r") @@ -113,7 +117,11 @@ def test_3dskims(data): skim_data[:, :, 0] = data skim_data[:, :, 1] = data*10 - skim_dict = skim.SkimDict(skim_data, [("SOV", "AM"), ("SOV", "PM")]) + skim_info = { + 'block_offsets': {('SOV', 'AM'): (0, 0), ('SOV', 'PM'): (0, 1)}, + 'key1_block_offsets': {'SOV': (0, 0)} + } + skim_dict = skim.SkimDict([skim_data], skim_info) stack = skim.SkimStack(skim_dict) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 4383ba277..e50a7d24b 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -105,9 +105,9 @@ output_tables: prefix: final_ tables: - checkpoints -# - accessibility -# - land_use -# - households -# - persons -# - trips -# - tours + - accessibility + - land_use + - households + - persons + - trips + - tours diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 982e20113..9fffe7e01 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -17,31 +17,14 @@ from activitysim.core.config import handle_standard_args from activitysim import abm -from activitysim.abm.tables.skims import skims_to_load -from activitysim.abm.tables.skims import shared_buffer_for_skims +from activitysim.abm.tables.skims import get_skim_info +from activitysim.abm.tables.skims import buffer_for_skims from activitysim.abm.tables.skims import load_skims logger = logging.getLogger('activitysim') -def load_skim_data(skim_buffer): - - logger.info("load_skim_data") - - data_dir = inject.get_injectable('data_dir') - omx_file_path = os.path.join(data_dir, setting('skims_file')) - tags_to_load = setting('skim_time_periods')['labels'] - - skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) - - skim_data = np.frombuffer(skim_buffer, dtype=skim_dtype).reshape(skims_shape) - - load_skims(omx_file_path, skim_keys, skim_data) - - return skim_data - - def pipeline_table_keys(pipeline_store, checkpoint_name=None): checkpoints = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] @@ -280,23 +263,29 @@ def coalesce_pipelines(sub_process_names, slice_info): pipeline.close_pipeline() - # pipeline_path = config.build_output_file_path(pipeline_file_name) - # with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: - # checkpoint_name, checkpoint_tables = pipeline_table_keys(pipeline_store) - # print "checkpoint_tables\n", checkpoint_tables +def load_skim_data(skim_buffer): -def allocate_shared_data(): - logger.info("allocate_shared_data") + logger.info("load_skim_data") data_dir = inject.get_injectable('data_dir') omx_file_path = os.path.join(data_dir, setting('skims_file')) tags_to_load = setting('skim_time_periods')['labels'] - # select the skims to load - skim_keys, skims_shape, skim_dtype = skims_to_load(omx_file_path, tags_to_load) + skim_info = get_skim_info(omx_file_path, tags_to_load) + load_skims(omx_file_path, skim_info, skim_buffer) - skim_buffer = shared_buffer_for_skims(skims_shape, skim_dtype) + +def allocate_shared_skim_buffer(): + logger.info("allocate_shared_skim_buffer") + + data_dir = inject.get_injectable('data_dir') + omx_file_path = os.path.join(data_dir, setting('skims_file')) + tags_to_load = setting('skim_time_periods')['labels'] + + # select the skims to load + skim_info = get_skim_info(omx_file_path, tags_to_load) + skim_buffer = buffer_for_skims(skim_info, shared=True) return skim_buffer @@ -311,7 +300,9 @@ def setup_injectables_and_logging(injectables): tracing.config_logger() -def run_mp_simulation(injectables, skim_buffer, step_info, resume_after, pipeline_prefix=False): +def run_mp_simulation(injectables, step_info, resume_after, pipeline_prefix, **kwargs): + + skim_buffer = kwargs models = step_info['models'] num_processes = step_info['num_processes'] @@ -349,9 +340,10 @@ def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): apportion_pipeline(sub_job_proc_names, slice_info) -def mp_setup_skims(injectables, skim_buffer): +def mp_setup_skims(injectables, **kwargs): + skim_buffer = kwargs setup_injectables_and_logging(injectables) - skim_data = load_skim_data(skim_buffer) + load_skim_data(skim_buffer) def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): @@ -359,14 +351,6 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): coalesce_pipelines(sub_job_proc_names, slice_info) -def mp_debug(injectables): - - setup_injectables_and_logging(injectables) - - for k in injectables: - print k, inject.get_injectable(k) - - def run_sub_process(p): logger.info("running sub_process %s" % p.name) p.start() @@ -397,18 +381,13 @@ def run_sub_procs(procs): def run_multiprocess(run_list, injectables): - # fixme - # logger.info('running mp_debug') - # run_sub_process( - # mp.Process(target=mp_debug, name='mp_debug', args=(injectables,)) - # ) - # bug - logger.info('setup shared skim data') - shared_skim_data = allocate_shared_data() + shared_skim_buffer = allocate_shared_skim_buffer() + run_sub_process( mp.Process(target=mp_setup_skims, name='mp_setup_skims', - args=(injectables, shared_skim_data,)) + args=(injectables,), + kwargs=shared_skim_buffer) ) resume_after = None @@ -429,7 +408,8 @@ def run_multiprocess(run_list, injectables): run_sub_process( mp.Process(target=run_mp_simulation, name=sub_proc_name, - args=(injectables, shared_skim_data, step_info, resume_after)) + args=(injectables, step_info, resume_after, False), + kwargs=shared_skim_buffer) ) else: @@ -449,8 +429,8 @@ def run_multiprocess(run_list, injectables): logger.info('starting sub_processes') error_procs = run_sub_procs([ mp.Process(target=run_mp_simulation, name=process_name, - args=(injectables, shared_skim_data, step_info, resume_after), - kwargs={'pipeline_prefix': True}) + args=(injectables, step_info, resume_after, True), + kwargs=shared_skim_buffer) for process_name in sub_proc_names ]) @@ -594,13 +574,14 @@ def get_run_list(): for istep in range(len(multiprocess_steps)): step_models = models[starts[istep]: starts[istep + 1]] + # suppress_intermediate_checkpoints until we support resume_after suppress_intermediate_checkpoints = True if suppress_intermediate_checkpoints: - step_models = ['_' + m if m[0] != '_' and m != step_models[-1] else m for m in step_models] + step_models = ['_' + m if m[0] != '_' and m != step_models[-1] else m + for m in step_models] multiprocess_steps[istep]['models'] = step_models - run_list['multiprocess_steps'] = multiprocess_steps return run_list From 4c7fdf89ed6044b04bee50fc09947dc4558295cd Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 11 Oct 2018 17:15:18 -0400 Subject: [PATCH 020/122] mp resume_after working with messy code --- activitysim/abm/models/accessibility.py | 2 +- activitysim/abm/models/cdap.py | 5 +- activitysim/abm/models/initialize.py | 2 + activitysim/abm/test/output/.gitignore | 1 + activitysim/core/config.py | 5 +- activitysim/core/inject.py | 2 +- activitysim/core/orca/__init__.py | 7 + activitysim/core/orca/orca.py | 2086 +++++++++++++++++ activitysim/core/orca/tests/__init__.py | 3 + .../core/orca/tests/test_mergetables.py | 257 ++ activitysim/core/orca/tests/test_orca.py | 1260 ++++++++++ activitysim/core/orca/utils/__init__.py | 5 + activitysim/core/orca/utils/logutil.py | 127 + activitysim/core/orca/utils/testing.py | 73 + activitysim/core/orca/utils/tests/__init__.py | 3 + .../core/orca/utils/tests/test_testing.py | 87 + .../core/orca/utils/tests/test_utils.py | 9 + activitysim/core/orca/utils/utils.py | 27 + activitysim/core/pipeline.py | 34 +- activitysim/core/simulate.py | 3 - activitysim/core/test/test_inject_defaults.py | 7 +- activitysim/core/test/test_simulate.py | 6 +- activitysim/core/timetable.py | 2 - activitysim/core/tracing.py | 22 +- example/configs/tour_mode_choice.yaml | 2 +- example/configs/tour_mode_choice_coeffs.csv | 3 +- example/output/.gitignore | 1 + example_mp/configs/logging.yaml | 20 +- example_mp/configs/settings.yaml | 30 +- example_mp/output/.gitignore | 1 + example_mp/simulation.py | 12 +- example_mp/tasks.py | 427 +++- example_multi/simulation.py | 2 +- setup.py | 2 +- 34 files changed, 4382 insertions(+), 153 deletions(-) create mode 100644 activitysim/core/orca/__init__.py create mode 100644 activitysim/core/orca/orca.py create mode 100644 activitysim/core/orca/tests/__init__.py create mode 100644 activitysim/core/orca/tests/test_mergetables.py create mode 100644 activitysim/core/orca/tests/test_orca.py create mode 100644 activitysim/core/orca/utils/__init__.py create mode 100644 activitysim/core/orca/utils/logutil.py create mode 100644 activitysim/core/orca/utils/testing.py create mode 100644 activitysim/core/orca/utils/tests/__init__.py create mode 100644 activitysim/core/orca/utils/tests/test_testing.py create mode 100644 activitysim/core/orca/utils/tests/test_utils.py create mode 100644 activitysim/core/orca/utils/utils.py diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index cbacd1176..22c4acd8a 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -57,7 +57,7 @@ def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): # data = data[orig_map, :][:, dest_map] # <- RIGHT # data = data[np.ix_(orig_map, dest_map)] # <- ALSO RIGHT - skim_index = range(omx_shape.shape[0]) + skim_index = range(omx_shape[0]) orig_map = np.isin(skim_index, skim_dict.offset_mapper.map(orig_zones)) dest_map = np.isin(skim_index, skim_dict.offset_mapper.map(dest_zones)) diff --git a/activitysim/abm/models/cdap.py b/activitysim/abm/models/cdap.py index de8ab01bc..eb8f761f7 100644 --- a/activitysim/abm/models/cdap.py +++ b/activitysim/abm/models/cdap.py @@ -1,8 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function import logging -import os import pandas as pd @@ -107,7 +107,8 @@ def cdap_simulate(persons_merged, persons, households, pipeline.replace_table("households", households) tracing.print_summary('cdap_activity', persons.cdap_activity, value_counts=True) - print pd.crosstab(persons.ptype, persons.cdap_activity, margins=True) + logger.info("cdap crosstabs:\n%s" % + pd.crosstab(persons.ptype, persons.cdap_activity, margins=True)) if trace_hh_id: diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index dce9f1a8d..ae33fa64b 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -107,3 +107,5 @@ def preload_injectables(): if inject.get_injectable('skim_stack', None) is not None: t0 = tracing.print_elapsed_time("preload skim_stack", t0, debug=True) + + return True diff --git a/activitysim/abm/test/output/.gitignore b/activitysim/abm/test/output/.gitignore index 5c191ce35..d98bf24ad 100644 --- a/activitysim/abm/test/output/.gitignore +++ b/activitysim/abm/test/output/.gitignore @@ -2,3 +2,4 @@ *.log *.h5 *.txt +*.yaml diff --git a/activitysim/core/config.py b/activitysim/core/config.py index a98e0f8aa..26f17e138 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -94,7 +94,7 @@ def handle_standard_args(parser=None): parser.add_argument("-c", "--config", help="path to config dir", action='append') parser.add_argument("-o", "--output", help="path to output dir") parser.add_argument("-d", "--data", help="path to data dir") - parser.add_argument("-r", "--resume", help="resume after") + parser.add_argument("-r", "--resume", nargs='?', const='_', type=str, help="resume after") parser.add_argument("-m", "--multiprocess", type=str2bool, nargs='?', const=True, help="run multiprocess (boolean flag, no arg defaults to true)") @@ -133,9 +133,8 @@ def setting(key, default=None): s = inject.get_injectable(key, None) if s: - # fixme - when does this happen? + # this happens when handle_standard_args overrides a setting with an injectable logger.info("read setting %s from injectable" % key) - bug # otherwise fall back to supplied default if s is None: diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index b5bcfeb42..3cfcbf56b 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -1,7 +1,7 @@ import logging import pandas as pd -import orca +from . import orca _DECORATED_STEPS = {} _DECORATED_TABLES = {} diff --git a/activitysim/core/orca/__init__.py b/activitysim/core/orca/__init__.py new file mode 100644 index 000000000..a4e55efe5 --- /dev/null +++ b/activitysim/core/orca/__init__.py @@ -0,0 +1,7 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +from .orca import * + +version = __version__ = '1.5.1' diff --git a/activitysim/core/orca/orca.py b/activitysim/core/orca/orca.py new file mode 100644 index 000000000..f5dc0c37b --- /dev/null +++ b/activitysim/core/orca/orca.py @@ -0,0 +1,2086 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +from __future__ import print_function + +try: + from inspect import getfullargspec as getargspec +except ImportError: + from inspect import getargspec +import logging +import time +import warnings +from collections import Callable, namedtuple +from contextlib import contextmanager +from functools import wraps + +import pandas as pd +import tables +import tlz as tz + +from . import utils +from .utils.logutil import log_start_finish +from collections import namedtuple + +warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) +logger = logging.getLogger('orca') + +_TABLES = {} +_COLUMNS = {} +_STEPS = {} +_BROADCASTS = {} +_INJECTABLES = {} + +_CACHING = True +_TABLE_CACHE = {} +_COLUMN_CACHE = {} +_INJECTABLE_CACHE = {} +_MEMOIZED = {} + +_CS_FOREVER = 'forever' +_CS_ITER = 'iteration' +_CS_STEP = 'step' + +CacheItem = namedtuple('CacheItem', ['name', 'value', 'scope']) + + +def clear_all(): + """ + Clear any and all stored state from Orca. + + """ + _TABLES.clear() + _COLUMNS.clear() + _STEPS.clear() + _BROADCASTS.clear() + _INJECTABLES.clear() + _TABLE_CACHE.clear() + _COLUMN_CACHE.clear() + _INJECTABLE_CACHE.clear() + for m in _MEMOIZED.values(): + m.value.clear_cached() + _MEMOIZED.clear() + logger.debug('pipeline state cleared') + + +def clear_cache(scope=None): + """ + Clear all cached data. + + Parameters + ---------- + scope : {None, 'step', 'iteration', 'forever'}, optional + Clear cached values with a given scope. + By default all cached values are removed. + + """ + if not scope: + _TABLE_CACHE.clear() + _COLUMN_CACHE.clear() + _INJECTABLE_CACHE.clear() + for m in _MEMOIZED.values(): + m.value.clear_cached() + logger.debug('pipeline cache cleared') + else: + for d in (_TABLE_CACHE, _COLUMN_CACHE, _INJECTABLE_CACHE): + items = tz.valfilter(lambda x: x.scope == scope, d) + for k in items: + del d[k] + for m in tz.filter(lambda x: x.scope == scope, _MEMOIZED.values()): + m.value.clear_cached() + logger.debug('cleared cached values with scope {!r}'.format(scope)) + + +def enable_cache(): + """ + Allow caching of registered variables that explicitly have + caching enabled. + + """ + global _CACHING + _CACHING = True + + +def disable_cache(): + """ + Turn off caching across Orca, even for registered variables + that have caching enabled. + + """ + global _CACHING + _CACHING = False + + +def cache_on(): + """ + Whether caching is currently enabled or disabled. + + Returns + ------- + on : bool + True if caching is enabled. + + """ + return _CACHING + + +@contextmanager +def cache_disabled(): + turn_back_on = True if cache_on() else False + disable_cache() + + yield + + if turn_back_on: + enable_cache() + + +# for errors that occur during Orca runs +class OrcaError(Exception): + pass + + +class DataFrameWrapper(object): + """ + Wraps a DataFrame so it can provide certain columns and handle + computed columns. + + Parameters + ---------- + name : str + Name for the table. + frame : pandas.DataFrame + copy_col : bool, optional + Whether to return copies when evaluating columns. + + Attributes + ---------- + name : str + Table name. + copy_col : bool + Whether to return copies when evaluating columns. + local : pandas.DataFrame + The wrapped DataFrame. + + """ + def __init__(self, name, frame, copy_col=True): + self.name = name + self.local = frame + self.copy_col = copy_col + + @property + def columns(self): + """ + Columns in this table. + + """ + return self.local_columns + list_columns_for_table(self.name) + + @property + def local_columns(self): + """ + Columns that are part of the wrapped DataFrame. + + """ + return list(self.local.columns) + + @property + def index(self): + """ + Table index. + + """ + return self.local.index + + def to_frame(self, columns=None): + """ + Make a DataFrame with the given columns. + + Will always return a copy of the underlying table. + + Parameters + ---------- + columns : sequence or string, optional + Sequence of the column names desired in the DataFrame. A string + can also be passed if only one column is desired. + If None all columns are returned, including registered columns. + + Returns + ------- + frame : pandas.DataFrame + + """ + extra_cols = _columns_for_table(self.name) + + if columns is not None: + columns = [columns] if isinstance(columns, str) else columns + columns = set(columns) + set_extra_cols = set(extra_cols) + local_cols = set(self.local.columns) & columns - set_extra_cols + df = self.local[list(local_cols)].copy() + extra_cols = {k: extra_cols[k] for k in (columns & set_extra_cols)} + else: + df = self.local.copy() + + with log_start_finish( + 'computing {!r} columns for table {!r}'.format( + len(extra_cols), self.name), + logger): + for name, col in extra_cols.items(): + with log_start_finish( + 'computing column {!r} for table {!r}'.format( + name, self.name), + logger): + df[name] = col() + + return df + + def update_col(self, column_name, series): + """ + Add or replace a column in the underlying DataFrame. + + Parameters + ---------- + column_name : str + Column to add or replace. + series : pandas.Series or sequence + Column data. + + """ + logger.debug('updating column {!r} in table {!r}'.format( + column_name, self.name)) + self.local[column_name] = series + + def __setitem__(self, key, value): + return self.update_col(key, value) + + def get_column(self, column_name): + """ + Returns a column as a Series. + + Parameters + ---------- + column_name : str + + Returns + ------- + column : pandas.Series + + """ + with log_start_finish( + 'getting single column {!r} from table {!r}'.format( + column_name, self.name), + logger): + extra_cols = _columns_for_table(self.name) + if column_name in extra_cols: + with log_start_finish( + 'computing column {!r} for table {!r}'.format( + column_name, self.name), + logger): + column = extra_cols[column_name]() + else: + column = self.local[column_name] + if self.copy_col: + return column.copy() + else: + return column + + def __getitem__(self, key): + return self.get_column(key) + + def __getattr__(self, key): + return self.get_column(key) + + def column_type(self, column_name): + """ + Report column type as one of 'local', 'series', or 'function'. + + Parameters + ---------- + column_name : str + + Returns + ------- + col_type : {'local', 'series', 'function'} + 'local' means that the column is part of the registered table, + 'series' means the column is a registered Pandas Series, + and 'function' means the column is a registered function providing + a Pandas Series. + + """ + extra_cols = list_columns_for_table(self.name) + + if column_name in extra_cols: + col = _COLUMNS[(self.name, column_name)] + + if isinstance(col, _SeriesWrapper): + return 'series' + elif isinstance(col, _ColumnFuncWrapper): + return 'function' + + elif column_name in self.local_columns: + return 'local' + + raise KeyError('column {!r} not found'.format(column_name)) + + def update_col_from_series(self, column_name, series, cast=False): + """ + Update existing values in a column from another series. + Index values must match in both column and series. Optionally + casts data type to match the existing column. + + Parameters + --------------- + column_name : str + series : panas.Series + cast: bool, optional, default False + """ + logger.debug('updating column {!r} in table {!r}'.format( + column_name, self.name)) + + col_dtype = self.local[column_name].dtype + if series.dtype != col_dtype: + if cast: + series = series.astype(col_dtype) + else: + err_msg = "Data type mismatch, existing:{}, update:{}" + err_msg = err_msg.format(col_dtype, series.dtype) + raise ValueError(err_msg) + + self.local.loc[series.index, column_name] = series + + def __len__(self): + return len(self.local) + + def clear_cached(self): + """ + Remove cached results from this table's computed columns. + + """ + _TABLE_CACHE.pop(self.name, None) + for col in _columns_for_table(self.name).values(): + col.clear_cached() + logger.debug('cleared cached columns for table {!r}'.format(self.name)) + + +class TableFuncWrapper(object): + """ + Wrap a function that provides a DataFrame. + + Parameters + ---------- + name : str + Name for the table. + func : callable + Callable that returns a DataFrame. + cache : bool, optional + Whether to cache the results of calling the wrapped function. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + copy_col : bool, optional + Whether to return copies when evaluating columns. + + Attributes + ---------- + name : str + Table name. + cache : bool + Whether caching is enabled for this table. + copy_col : bool + Whether to return copies when evaluating columns. + + """ + def __init__( + self, name, func, cache=False, cache_scope=_CS_FOREVER, + copy_col=True): + self.name = name + self._func = func + self._argspec = getargspec(func) + self.cache = cache + self.cache_scope = cache_scope + self.copy_col = copy_col + self._columns = [] + self._index = None + self._len = 0 + + @property + def columns(self): + """ + Columns in this table. (May contain only computed columns + if the wrapped function has not been called yet.) + + """ + return self._columns + list_columns_for_table(self.name) + + @property + def local_columns(self): + """ + Only the columns contained in the DataFrame returned by the + wrapped function. (No registered columns included.) + + """ + if self._columns: + return self._columns + else: + self._call_func() + return self._columns + + @property + def index(self): + """ + Index of the underlying table. Will be None if that index is + unknown. + + """ + return self._index + + def _call_func(self): + """ + Call the wrapped function and return the result wrapped by + DataFrameWrapper. + Also updates attributes like columns, index, and length. + + """ + if _CACHING and self.cache and self.name in _TABLE_CACHE: + logger.debug('returning table {!r} from cache'.format(self.name)) + return _TABLE_CACHE[self.name].value + + with log_start_finish( + 'call function to get frame for table {!r}'.format( + self.name), + logger): + kwargs = _collect_variables(names=self._argspec.args, + expressions=self._argspec.defaults) + frame = self._func(**kwargs) + + self._columns = list(frame.columns) + self._index = frame.index + self._len = len(frame) + + wrapped = DataFrameWrapper(self.name, frame, copy_col=self.copy_col) + + if self.cache: + _TABLE_CACHE[self.name] = CacheItem( + self.name, wrapped, self.cache_scope) + + return wrapped + + def __call__(self): + return self._call_func() + + def to_frame(self, columns=None): + """ + Make a DataFrame with the given columns. + + Will always return a copy of the underlying table. + + Parameters + ---------- + columns : sequence, optional + Sequence of the column names desired in the DataFrame. + If None all columns are returned. + + Returns + ------- + frame : pandas.DataFrame + + """ + return self._call_func().to_frame(columns) + + def get_column(self, column_name): + """ + Returns a column as a Series. + + Parameters + ---------- + column_name : str + + Returns + ------- + column : pandas.Series + + """ + frame = self._call_func() + return DataFrameWrapper(self.name, frame, + copy_col=self.copy_col).get_column(column_name) + + def __getitem__(self, key): + return self.get_column(key) + + def __getattr__(self, key): + return self.get_column(key) + + def __len__(self): + return self._len + + def column_type(self, column_name): + """ + Report column type as one of 'local', 'series', or 'function'. + + Parameters + ---------- + column_name : str + + Returns + ------- + col_type : {'local', 'series', 'function'} + 'local' means that the column is part of the registered table, + 'series' means the column is a registered Pandas Series, + and 'function' means the column is a registered function providing + a Pandas Series. + + """ + extra_cols = list_columns_for_table(self.name) + + if column_name in extra_cols: + col = _COLUMNS[(self.name, column_name)] + + if isinstance(col, _SeriesWrapper): + return 'series' + elif isinstance(col, _ColumnFuncWrapper): + return 'function' + + elif column_name in self.local_columns: + return 'local' + + raise KeyError('column {!r} not found'.format(column_name)) + + def clear_cached(self): + """ + Remove this table's cached result and that of associated columns. + + """ + _TABLE_CACHE.pop(self.name, None) + for col in _columns_for_table(self.name).values(): + col.clear_cached() + logger.debug( + 'cleared cached result and cached columns for table {!r}'.format( + self.name)) + + def func_source_data(self): + """ + Return data about the wrapped function source, including file name, + line number, and source code. + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + return utils.func_source_data(self._func) + + +class _ColumnFuncWrapper(object): + """ + Wrap a function that returns a Series. + + Parameters + ---------- + table_name : str + Table with which the column will be associated. + column_name : str + Name for the column. + func : callable + Should return a Series that has an + index matching the table to which it is being added. + cache : bool, optional + Whether to cache the result of calling the wrapped function. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + + Attributes + ---------- + name : str + Column name. + table_name : str + Name of table this column is associated with. + cache : bool + Whether caching is enabled for this column. + + """ + def __init__( + self, table_name, column_name, func, cache=False, + cache_scope=_CS_FOREVER): + self.table_name = table_name + self.name = column_name + self._func = func + self._argspec = getargspec(func) + self.cache = cache + self.cache_scope = cache_scope + + def __call__(self): + """ + Evaluate the wrapped function and return the result. + + """ + if (_CACHING and + self.cache and + (self.table_name, self.name) in _COLUMN_CACHE): + logger.debug( + 'returning column {!r} for table {!r} from cache'.format( + self.name, self.table_name)) + return _COLUMN_CACHE[(self.table_name, self.name)].value + + with log_start_finish( + ('call function to provide column {!r} for table {!r}' + ).format(self.name, self.table_name), logger): + kwargs = _collect_variables(names=self._argspec.args, + expressions=self._argspec.defaults) + col = self._func(**kwargs) + + if self.cache: + _COLUMN_CACHE[(self.table_name, self.name)] = CacheItem( + (self.table_name, self.name), col, self.cache_scope) + + return col + + def clear_cached(self): + """ + Remove any cached result of this column. + + """ + x = _COLUMN_CACHE.pop((self.table_name, self.name), None) + if x is not None: + logger.debug( + 'cleared cached value for column {!r} in table {!r}'.format( + self.name, self.table_name)) + + def func_source_data(self): + """ + Return data about the wrapped function source, including file name, + line number, and source code. + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + return utils.func_source_data(self._func) + + +class _SeriesWrapper(object): + """ + Wrap a Series for the purpose of giving it the same interface as a + `_ColumnFuncWrapper`. + + Parameters + ---------- + table_name : str + Table with which the column will be associated. + column_name : str + Name for the column. + series : pandas.Series + Series with index matching the table to which it is being added. + + Attributes + ---------- + name : str + Column name. + table_name : str + Name of table this column is associated with. + + """ + def __init__(self, table_name, column_name, series): + self.table_name = table_name + self.name = column_name + self._column = series + + def __call__(self): + return self._column + + def clear_cached(self): + """ + Here for compatibility with `_ColumnFuncWrapper`. + + """ + pass + + +class _InjectableFuncWrapper(object): + """ + Wraps a function that will provide an injectable value elsewhere. + + Parameters + ---------- + name : str + func : callable + cache : bool, optional + Whether to cache the result of calling the wrapped function. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + + Attributes + ---------- + name : str + Name of this injectable. + cache : bool + Whether caching is enabled for this injectable function. + + """ + def __init__(self, name, func, cache=False, cache_scope=_CS_FOREVER): + self.name = name + self._func = func + self._argspec = getargspec(func) + self.cache = cache + self.cache_scope = cache_scope + + def __call__(self): + if _CACHING and self.cache and self.name in _INJECTABLE_CACHE: + logger.debug( + 'returning injectable {!r} from cache'.format(self.name)) + return _INJECTABLE_CACHE[self.name].value + + with log_start_finish( + 'call function to provide injectable {!r}'.format(self.name), + logger): + kwargs = _collect_variables(names=self._argspec.args, + expressions=self._argspec.defaults) + result = self._func(**kwargs) + + if self.cache: + _INJECTABLE_CACHE[self.name] = CacheItem( + self.name, result, self.cache_scope) + + return result + + def clear_cached(self): + """ + Clear a cached result for this injectable. + + """ + x = _INJECTABLE_CACHE.pop(self.name, None) + if x: + logger.debug( + 'injectable {!r} removed from cache'.format(self.name)) + + +class _StepFuncWrapper(object): + """ + Wrap a step function for argument matching. + + Parameters + ---------- + step_name : str + func : callable + + Attributes + ---------- + name : str + Name of step. + + """ + def __init__(self, step_name, func): + self.name = step_name + self._func = func + self._argspec = getargspec(func) + + def __call__(self): + with log_start_finish('calling step {!r}'.format(self.name), logger): + kwargs = _collect_variables(names=self._argspec.args, + expressions=self._argspec.defaults) + return self._func(**kwargs) + + def _tables_used(self): + """ + Tables injected into the step. + + Returns + ------- + tables : set of str + + """ + args = list(self._argspec.args) + if self._argspec.defaults: + default_args = list(self._argspec.defaults) + else: + default_args = [] + # Combine names from argument names and argument default values. + names = args[:len(args) - len(default_args)] + default_args + tables = set() + for name in names: + parent_name = name.split('.')[0] + if is_table(parent_name): + tables.add(parent_name) + return tables + + def func_source_data(self): + """ + Return data about a step function's source, including file name, + line number, and source code. + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + return utils.func_source_data(self._func) + + +def is_table(name): + """ + Returns whether a given name refers to a registered table. + + """ + return name in _TABLES + + +def list_tables(): + """ + List of table names. + + """ + return list(_TABLES.keys()) + + +def list_columns(): + """ + List of (table name, registered column name) pairs. + + """ + return list(_COLUMNS.keys()) + + +def list_steps(): + """ + List of registered step names. + + """ + return list(_STEPS.keys()) + + +def list_injectables(): + """ + List of registered injectables. + + """ + return list(_INJECTABLES.keys()) + + +def list_broadcasts(): + """ + List of registered broadcasts as (cast table name, onto table name). + + """ + return list(_BROADCASTS.keys()) + + +def is_expression(name): + """ + Checks whether a given name is a simple variable name or a compound + variable expression. + + Parameters + ---------- + name : str + + Returns + ------- + is_expr : bool + + """ + return '.' in name + + +def _collect_variables(names, expressions=None): + """ + Map labels and expressions to registered variables. + + Handles argument matching. + + Example: + + _collect_variables(names=['zones', 'zone_id'], + expressions=['parcels.zone_id']) + + Would return a dict representing: + + {'parcels': , + 'zone_id': } + + Parameters + ---------- + names : list of str + List of registered variable names and/or labels. + If mixing names and labels, labels must come at the end. + expressions : list of str, optional + List of registered variable expressions for labels defined + at end of `names`. Length must match the number of labels. + + Returns + ------- + variables : dict + Keys match `names`. Values correspond to registered variables, + which may be wrappers or evaluated functions if appropriate. + + """ + # Map registered variable labels to expressions. + if not expressions: + expressions = [] + offset = len(names) - len(expressions) + labels_map = dict(tz.concatv( + tz.compatibility.zip(names[:offset], names[:offset]), + tz.compatibility.zip(names[offset:], expressions))) + + all_variables = tz.merge(_INJECTABLES, _TABLES) + variables = {} + for label, expression in labels_map.items(): + # In the future, more registered variable expressions could be + # supported. Currently supports names of registered variables + # and references to table columns. + if '.' in expression: + # Registered variable expression refers to column. + table_name, column_name = expression.split('.') + table = get_table(table_name) + variables[label] = table.get_column(column_name) + else: + thing = all_variables[expression] + if isinstance(thing, (_InjectableFuncWrapper, TableFuncWrapper)): + # Registered variable object is function. + variables[label] = thing() + else: + variables[label] = thing + + return variables + + +def add_table( + table_name, table, cache=False, cache_scope=_CS_FOREVER, + copy_col=True): + """ + Register a table with Orca. + + Parameters + ---------- + table_name : str + Should be globally unique to this table. + table : pandas.DataFrame or function + If a function, the function should return a DataFrame. + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + cache : bool, optional + Whether to cache the results of a provided callable. Does not + apply if `table` is a DataFrame. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + copy_col : bool, optional + Whether to return copies when evaluating columns. + + Returns + ------- + wrapped : `DataFrameWrapper` or `TableFuncWrapper` + + """ + if isinstance(table, Callable): + table = TableFuncWrapper(table_name, table, cache=cache, + cache_scope=cache_scope, copy_col=copy_col) + else: + table = DataFrameWrapper(table_name, table, copy_col=copy_col) + + # clear any cached data from a previously registered table + table.clear_cached() + + logger.debug('registering table {!r}'.format(table_name)) + _TABLES[table_name] = table + + return table + + +def table( + table_name=None, cache=False, cache_scope=_CS_FOREVER, copy_col=True): + """ + Decorates functions that return DataFrames. + + Decorator version of `add_table`. Table name defaults to + name of function. + + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + The argument name "iter_var" may be used to have the current + iteration variable injected. + + """ + def decorator(func): + if table_name: + name = table_name + else: + name = func.__name__ + add_table( + name, func, cache=cache, cache_scope=cache_scope, + copy_col=copy_col) + return func + return decorator + + +def get_raw_table(table_name): + """ + Get a wrapped table by name and don't do anything to it. + + Parameters + ---------- + table_name : str + + Returns + ------- + table : DataFrameWrapper or TableFuncWrapper + + """ + if is_table(table_name): + return _TABLES[table_name] + else: + raise KeyError('table not found: {}'.format(table_name)) + + +def get_table(table_name): + """ + Get a registered table. + + Decorated functions will be converted to `DataFrameWrapper`. + + Parameters + ---------- + table_name : str + + Returns + ------- + table : `DataFrameWrapper` + + """ + table = get_raw_table(table_name) + if isinstance(table, TableFuncWrapper): + table = table() + return table + + +def table_type(table_name): + """ + Returns the type of a registered table. + + The type can be either "dataframe" or "function". + + Parameters + ---------- + table_name : str + + Returns + ------- + table_type : {'dataframe', 'function'} + + """ + table = get_raw_table(table_name) + + if isinstance(table, DataFrameWrapper): + return 'dataframe' + elif isinstance(table, TableFuncWrapper): + return 'function' + + +def add_column( + table_name, column_name, column, cache=False, cache_scope=_CS_FOREVER): + """ + Add a new column to a table from a Series or callable. + + Parameters + ---------- + table_name : str + Table with which the column will be associated. + column_name : str + Name for the column. + column : pandas.Series or callable + Series should have an index matching the table to which it + is being added. If a callable, the function's argument + names and keyword argument values will be matched to + registered variables when the function needs to be + evaluated by Orca. The function should return a Series. + cache : bool, optional + Whether to cache the results of a provided callable. Does not + apply if `column` is a Series. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + + """ + if isinstance(column, Callable): + column = \ + _ColumnFuncWrapper( + table_name, column_name, column, + cache=cache, cache_scope=cache_scope) + else: + column = _SeriesWrapper(table_name, column_name, column) + + # clear any cached data from a previously registered column + column.clear_cached() + + logger.debug('registering column {!r} on table {!r}'.format( + column_name, table_name)) + _COLUMNS[(table_name, column_name)] = column + + return column + + +def column(table_name, column_name=None, cache=False, cache_scope=_CS_FOREVER): + """ + Decorates functions that return a Series. + + Decorator version of `add_column`. Series index must match + the named table. Column name defaults to name of function. + + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + The argument name "iter_var" may be used to have the current + iteration variable injected. + The index of the returned Series must match the named table. + + """ + def decorator(func): + if column_name: + name = column_name + else: + name = func.__name__ + add_column( + table_name, name, func, cache=cache, cache_scope=cache_scope) + return func + return decorator + + +def list_columns_for_table(table_name): + """ + Return a list of all the extra columns registered for a given table. + + Parameters + ---------- + table_name : str + + Returns + ------- + columns : list of str + + """ + return [cname for tname, cname in _COLUMNS.keys() if tname == table_name] + + +def _columns_for_table(table_name): + """ + Return all of the columns registered for a given table. + + Parameters + ---------- + table_name : str + + Returns + ------- + columns : dict of column wrappers + Keys will be column names. + + """ + return {cname: col + for (tname, cname), col in _COLUMNS.items() + if tname == table_name} + + +def column_map(tables, columns): + """ + Take a list of tables and a list of column names and resolve which + columns come from which table. + + Parameters + ---------- + tables : sequence of _DataFrameWrapper or _TableFuncWrapper + Could also be sequence of modified pandas.DataFrames, the important + thing is that they have ``.name`` and ``.columns`` attributes. + columns : sequence of str + The column names of interest. + + Returns + ------- + col_map : dict + Maps table names to lists of column names. + """ + if not columns: + return {t.name: None for t in tables} + + columns = set(columns) + colmap = { + t.name: list(set(t.columns).intersection(columns)) for t in tables} + foundcols = tz.reduce( + lambda x, y: x.union(y), (set(v) for v in colmap.values())) + if foundcols != columns: + raise RuntimeError('Not all required columns were found. ' + 'Missing: {}'.format(list(columns - foundcols))) + return colmap + + +def get_raw_column(table_name, column_name): + """ + Get a wrapped, registered column. + + This function cannot return columns that are part of wrapped + DataFrames, it's only for columns registered directly through Orca. + + Parameters + ---------- + table_name : str + column_name : str + + Returns + ------- + wrapped : _SeriesWrapper or _ColumnFuncWrapper + + """ + try: + return _COLUMNS[(table_name, column_name)] + except KeyError: + raise KeyError('column {!r} not found for table {!r}'.format( + column_name, table_name)) + + +def _memoize_function(f, name, cache_scope=_CS_FOREVER): + """ + Wraps a function for memoization and ties it's cache into the + Orca cacheing system. + + Parameters + ---------- + f : function + name : str + Name of injectable. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + + """ + cache = {} + + @wraps(f) + def wrapper(*args, **kwargs): + try: + cache_key = ( + args or None, frozenset(kwargs.items()) if kwargs else None) + in_cache = cache_key in cache + except TypeError: + raise TypeError( + 'function arguments must be hashable for memoization') + + if _CACHING and in_cache: + return cache[cache_key] + else: + result = f(*args, **kwargs) + cache[cache_key] = result + return result + + wrapper.__wrapped__ = f + wrapper.cache = cache + wrapper.clear_cached = lambda: cache.clear() + _MEMOIZED[name] = CacheItem(name, wrapper, cache_scope) + + return wrapper + + +def add_injectable( + name, value, autocall=True, cache=False, cache_scope=_CS_FOREVER, + memoize=False): + """ + Add a value that will be injected into other functions. + + Parameters + ---------- + name : str + value + If a callable and `autocall` is True then the function's + argument names and keyword argument values will be matched + to registered variables when the function needs to be + evaluated by Orca. The return value will + be passed to any functions using this injectable. In all other + cases, `value` will be passed through untouched. + autocall : bool, optional + Set to True to have injectable functions automatically called + (with argument matching) and the result injected instead of + the function itself. + cache : bool, optional + Whether to cache the return value of an injectable function. + Only applies when `value` is a callable and `autocall` is True. + cache_scope : {'step', 'iteration', 'forever'}, optional + Scope for which to cache data. Default is to cache forever + (or until manually cleared). 'iteration' caches data for each + complete iteration of the pipeline, 'step' caches data for + a single step of the pipeline. + memoize : bool, optional + If autocall is False it is still possible to cache function results + by setting this flag to True. Cached values are stored in a dictionary + keyed by argument values, so the argument values must be hashable. + Memoized functions have their caches cleared according to the same + rules as universal caching. + + """ + if isinstance(value, Callable): + if autocall: + value = _InjectableFuncWrapper( + name, value, cache=cache, cache_scope=cache_scope) + # clear any cached data from a previously registered value + value.clear_cached() + elif not autocall and memoize: + value = _memoize_function(value, name, cache_scope=cache_scope) + + logger.debug('registering injectable {!r}'.format(name)) + _INJECTABLES[name] = value + + +def injectable( + name=None, autocall=True, cache=False, cache_scope=_CS_FOREVER, + memoize=False): + """ + Decorates functions that will be injected into other functions. + + Decorator version of `add_injectable`. Name defaults to + name of function. + + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + The argument name "iter_var" may be used to have the current + iteration variable injected. + + """ + def decorator(func): + if name: + n = name + else: + n = func.__name__ + add_injectable( + n, func, autocall=autocall, cache=cache, cache_scope=cache_scope, + memoize=memoize) + return func + return decorator + + +def is_injectable(name): + """ + Checks whether a given name can be mapped to an injectable. + + """ + return name in _INJECTABLES + + +def get_raw_injectable(name): + """ + Return a raw, possibly wrapped injectable. + + Parameters + ---------- + name : str + + Returns + ------- + inj : _InjectableFuncWrapper or object + + """ + if is_injectable(name): + return _INJECTABLES[name] + else: + raise KeyError('injectable not found: {!r}'.format(name)) + + +def injectable_type(name): + """ + Classify an injectable as either 'variable' or 'function'. + + Parameters + ---------- + name : str + + Returns + ------- + inj_type : {'variable', 'function'} + If the injectable is an automatically called function or any other + type of callable the type will be 'function', all other injectables + will be have type 'variable'. + + """ + inj = get_raw_injectable(name) + if isinstance(inj, (_InjectableFuncWrapper, Callable)): + return 'function' + else: + return 'variable' + + +def get_injectable(name): + """ + Get an injectable by name. *Does not* evaluate wrapped functions. + + Parameters + ---------- + name : str + + Returns + ------- + injectable + Original value or evaluated value of an _InjectableFuncWrapper. + + """ + i = get_raw_injectable(name) + return i() if isinstance(i, _InjectableFuncWrapper) else i + + +def get_injectable_func_source_data(name): + """ + Return data about an injectable function's source, including file name, + line number, and source code. + + Parameters + ---------- + name : str + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + if injectable_type(name) != 'function': + raise ValueError('injectable {!r} is not a function'.format(name)) + + inj = get_raw_injectable(name) + + if isinstance(inj, _InjectableFuncWrapper): + return utils.func_source_data(inj._func) + elif hasattr(inj, '__wrapped__'): + return utils.func_source_data(inj.__wrapped__) + else: + return utils.func_source_data(inj) + + +def add_step(step_name, func): + """ + Add a step function to Orca. + + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + The argument name "iter_var" may be used to have the current + iteration variable injected. + + Parameters + ---------- + step_name : str + func : callable + + """ + if isinstance(func, Callable): + logger.debug('registering step {!r}'.format(step_name)) + _STEPS[step_name] = _StepFuncWrapper(step_name, func) + else: + raise TypeError('func must be a callable') + + +def step(step_name=None): + """ + Decorates functions that will be called by the `run` function. + + Decorator version of `add_step`. step name defaults to + name of function. + + The function's argument names and keyword argument values + will be matched to registered variables when the function + needs to be evaluated by Orca. + The argument name "iter_var" may be used to have the current + iteration variable injected. + + """ + def decorator(func): + if step_name: + name = step_name + else: + name = func.__name__ + add_step(name, func) + return func + return decorator + + +def is_step(step_name): + """ + Check whether a given name refers to a registered step. + + """ + return step_name in _STEPS + + +def get_step(step_name): + """ + Get a wrapped step by name. + + Parameters + ---------- + + """ + if is_step(step_name): + return _STEPS[step_name] + else: + raise KeyError('no step named {}'.format(step_name)) + + +Broadcast = namedtuple( + 'Broadcast', + ['cast', 'onto', 'cast_on', 'onto_on', 'cast_index', 'onto_index']) + + +def broadcast(cast, onto, cast_on=None, onto_on=None, + cast_index=False, onto_index=False): + """ + Register a rule for merging two tables by broadcasting one onto + the other. + + Parameters + ---------- + cast, onto : str + Names of registered tables. + cast_on, onto_on : str, optional + Column names used for merge, equivalent of ``left_on``/``right_on`` + parameters of pandas.merge. + cast_index, onto_index : bool, optional + Whether to use table indexes for merge. Equivalent of + ``left_index``/``right_index`` parameters of pandas.merge. + + """ + logger.debug( + 'registering broadcast of table {!r} onto {!r}'.format(cast, onto)) + _BROADCASTS[(cast, onto)] = \ + Broadcast(cast, onto, cast_on, onto_on, cast_index, onto_index) + + +def _get_broadcasts(tables): + """ + Get the broadcasts associated with a set of tables. + + Parameters + ---------- + tables : sequence of str + Table names for which broadcasts have been registered. + + Returns + ------- + casts : dict of `Broadcast` + Keys are tuples of strings like (cast_name, onto_name). + + """ + tables = set(tables) + casts = tz.keyfilter( + lambda x: x[0] in tables and x[1] in tables, _BROADCASTS) + if tables - set(tz.concat(casts.keys())): + raise ValueError('Not enough links to merge all tables.') + return casts + + +def is_broadcast(cast_name, onto_name): + """ + Checks whether a relationship exists for broadcast `cast_name` + onto `onto_name`. + + """ + return (cast_name, onto_name) in _BROADCASTS + + +def get_broadcast(cast_name, onto_name): + """ + Get a single broadcast. + + Broadcasts are stored data about how to do a Pandas join. + A Broadcast object is a namedtuple with these attributes: + + - cast: the name of the table being broadcast + - onto: the name of the table onto which "cast" is broadcast + - cast_on: The optional name of a column on which to join. + None if the table index will be used instead. + - onto_on: The optional name of a column on which to join. + None if the table index will be used instead. + - cast_index: True if the table index should be used for the join. + - onto_index: True if the table index should be used for the join. + + Parameters + ---------- + cast_name : str + The name of the table being braodcast. + onto_name : str + The name of the table onto which `cast_name` is broadcast. + + Returns + ------- + broadcast : Broadcast + + """ + if is_broadcast(cast_name, onto_name): + return _BROADCASTS[(cast_name, onto_name)] + else: + raise KeyError( + 'no rule found for broadcasting {!r} onto {!r}'.format( + cast_name, onto_name)) + + +# utilities for merge_tables +def _all_reachable_tables(t): + """ + A generator that provides all the names of tables that can be + reached via merges starting at the given target table. + + """ + for k, v in t.items(): + for tname in _all_reachable_tables(v): + yield tname + yield k + + +def _recursive_getitem(d, key): + """ + Descend into a dict of dicts to return the one that contains + a given key. Every value in the dict must be another dict. + + """ + if key in d: + return d + else: + for v in d.values(): + return _recursive_getitem(v, key) + else: + raise KeyError('Key not found: {}'.format(key)) + + +def _dict_value_to_pairs(d): + """ + Takes the first value of a dictionary (which it self should be + a dictionary) and turns it into a series of {key: value} dicts. + + For example, _dict_value_to_pairs({'c': {'a': 1, 'b': 2}}) will yield + {'a': 1} and {'b': 2}. + + """ + d = d[tz.first(d)] + + for k, v in d.items(): + yield {k: v} + + +def _is_leaf_node(merge_node): + """ + Returns True for dicts like {'a': {}}. + + """ + return len(merge_node) == 1 and not next(iter(merge_node.values())) + + +def _next_merge(merge_node): + """ + Gets a node that has only leaf nodes below it. This table and + the ones below are ready to be merged to make a new leaf node. + + """ + if all(_is_leaf_node(d) for d in _dict_value_to_pairs(merge_node)): + return merge_node + else: + for d in tz.remove(_is_leaf_node, _dict_value_to_pairs(merge_node)): + return _next_merge(d) + else: + raise OrcaError('No node found for next merge.') + + +def merge_tables(target, tables, columns=None, drop_intersection=True): + """ + Merge a number of tables onto a target table. Tables must have + registered merge rules via the `broadcast` function. + + Parameters + ---------- + target : str, DataFrameWrapper, or TableFuncWrapper + Name of the table (or wrapped table) onto which tables will be merged. + tables : list of `DataFrameWrapper`, `TableFuncWrapper`, or str + All of the tables to merge. Should include the target table. + columns : list of str, optional + If given, columns will be mapped to `tables` and only those columns + will be requested from each table. The final merged table will have + only these columns. By default all columns are used from every + table. + drop_intersection : bool + If True, keep the left most occurence of any column name if it occurs + on more than one table. This prevents getting back the same column + with suffixes applied by pd.merge. If false, columns names will be + suffixed with the table names - e.g. zone_id_buildings and + zone_id_parcels. + + Returns + ------- + merged : pandas.DataFrame + + """ + # allow target to be string or table wrapper + if isinstance(target, (DataFrameWrapper, TableFuncWrapper)): + target = target.name + + # allow tables to be strings or table wrappers + tables = [get_table(t) + if not isinstance(t, (DataFrameWrapper, TableFuncWrapper)) else t + for t in tables] + + merges = {t.name: {} for t in tables} + tables = {t.name: t for t in tables} + casts = _get_broadcasts(tables.keys()) + logger.debug( + 'attempting to merge tables {} to target table {}'.format( + tables.keys(), target)) + + # relate all the tables by registered broadcasts + for table, onto in casts: + merges[onto][table] = merges[table] + merges = {target: merges[target]} + + # verify that all the tables can be merged to the target + all_tables = set(_all_reachable_tables(merges)) + + if all_tables != set(tables.keys()): + raise RuntimeError( + ('Not all tables can be merged to target "{}". Unlinked tables: {}' + ).format(target, list(set(tables.keys()) - all_tables))) + + # add any columns necessary for indexing into other tables + # during merges + if columns: + columns = list(columns) + for c in casts.values(): + if c.onto_on: + columns.append(c.onto_on) + if c.cast_on: + columns.append(c.cast_on) + + # get column map for which columns go with which table + colmap = column_map(tables.values(), columns) + + # get frames + frames = {name: t.to_frame(columns=colmap[name]) + for name, t in tables.items()} + + past_intersections = set() + + # perform merges until there's only one table left + while merges[target]: + nm = _next_merge(merges) + onto = tz.first(nm) + onto_table = frames[onto] + + # loop over all the tables that can be broadcast onto + # the onto_table and merge them all in. + for cast in nm[onto]: + cast_table = frames[cast] + bc = casts[(cast, onto)] + + with log_start_finish( + 'merge tables {} and {}'.format(onto, cast), logger): + + intersection = set(onto_table.columns).\ + intersection(cast_table.columns) + # intersection is ok if it's the join key + intersection.discard(bc.onto_on) + intersection.discard(bc.cast_on) + # otherwise drop so as not to create conflicts + if drop_intersection: + cast_table = cast_table.drop(intersection, axis=1) + else: + # add suffix to past intersections which wouldn't get + # picked up by the merge - these we have to rename by hand + renames = dict(zip( + past_intersections, + [c+'_'+onto for c in past_intersections] + )) + onto_table = onto_table.rename(columns=renames) + + # keep track of past intersections in case there's an odd + # number of intersections + past_intersections = past_intersections.union(intersection) + + onto_table = pd.merge( + onto_table, cast_table, + suffixes=['_'+onto, '_'+cast], + left_on=bc.onto_on, right_on=bc.cast_on, + left_index=bc.onto_index, right_index=bc.cast_index) + + # replace the existing table with the merged one + frames[onto] = onto_table + + # free up space by dropping the cast table + del frames[cast] + + # mark the onto table as having no more things to broadcast + # onto it. + _recursive_getitem(merges, onto)[onto] = {} + + logger.debug('finished merge') + return frames[target] + + +def get_step_table_names(steps): + """ + Returns a list of table names injected into the provided steps. + + Parameters + ---------- + steps: list of str + Steps to gather table inputs from. + + Returns + ------- + list of str + + """ + table_names = set() + for s in steps: + table_names |= get_step(s)._tables_used() + return list(table_names) + + +def write_tables(fname, table_names=None, prefix=None, compress=False, local=False): + """ + Writes tables to a pandas.HDFStore file. + + Parameters + ---------- + fname : str + File name for HDFStore. Will be opened in append mode and closed + at the end of this function. + table_names: list of str, optional, default None + List of tables to write. If None, all registered tables will + be written. + prefix: str + If not None, used to prefix the output table names so that + multiple iterations can go in the same file. + compress: boolean + Whether to compress output file using standard HDF5-readable + zlib compression, default False. + + """ + if table_names is None: + table_names = list_tables() + + tables = (get_table(t) for t in table_names) + key_template = '{}/{{}}'.format(prefix) if prefix is not None else '{}' + + # set compression options to zlib level-1 if compress arg is True + complib = compress and 'zlib' or None + complevel = compress and 1 or 0 + + with pd.HDFStore(fname, mode='a', complib=complib, complevel=complevel) as store: + for t in tables: + # if local arg is True, store only local columns + columns = None + if local is True: + columns = t.local_columns + store[key_template.format(t.name)] = t.to_frame(columns=columns) + + +iter_step = namedtuple('iter_step', 'step_num,step_name') + + +def run(steps, iter_vars=None, data_out=None, out_interval=1, + out_base_tables=None, out_run_tables=None, compress=False, + out_base_local=True, out_run_local=True): + """ + Run steps in series, optionally repeatedly over some sequence. + The current iteration variable is set as a global injectable + called ``iter_var``. + + Parameters + ---------- + steps : list of str + List of steps to run identified by their name. + iter_vars : iterable, optional + The values of `iter_vars` will be made available as an injectable + called ``iter_var`` when repeatedly running `steps`. + data_out : str, optional + An optional filename to which all tables injected into any step + in `steps` will be saved every `out_interval` iterations. + File will be a pandas HDF data store. + out_interval : int, optional + Iteration interval on which to save data to `data_out`. For example, + 2 will save out every 2 iterations, 5 every 5 iterations. + Default is every iteration. + The results of the first and last iterations are always included. + The input (base) tables are also included and prefixed with `base/`, + these represent the state of the system before any steps have been + executed. + The interval is defined relative to the first iteration. For example, + a run begining in 2015 with an out_interval of 2, will write out + results for 2015, 2017, etc. + out_base_tables: list of str, optional, default None + List of base tables to write. If not provided, tables injected + into 'steps' will be written. + out_run_tables: list of str, optional, default None + List of run tables to write. If not provided, tables injected + into 'steps' will be written. + compress: boolean, optional, default False + Whether to compress output file using standard HDF5 zlib compression. + Compression yields much smaller files using slightly more CPU. + out_base_local: boolean, optional, default True + For tables in out_base_tables, whether to store only local columns (True) + or both, local and computed columns (False). + out_run_local: boolean, optional, default True + For tables in out_run_tables, whether to store only local columns (True) + or both, local and computed columns (False). + """ + iter_vars = iter_vars or [None] + max_i = len(iter_vars) + + # get the tables to write out + if out_base_tables is None or out_run_tables is None: + step_tables = get_step_table_names(steps) + + if out_base_tables is None: + out_base_tables = step_tables + + if out_run_tables is None: + out_run_tables = step_tables + + # write out the base (inputs) + if data_out: + add_injectable('iter_var', iter_vars[0]) + write_tables(data_out, out_base_tables, 'base', compress=compress, local=out_base_local) + + # run the steps + for i, var in enumerate(iter_vars, start=1): + add_injectable('iter_var', var) + + if var is not None: + logger.debug( + 'running iteration {} with iteration value {!r}'.format( + i, var)) + + t1 = time.time() + for j, step_name in enumerate(steps): + add_injectable('iter_step', iter_step(j, step_name)) + with log_start_finish( + 'run step {!r}'.format(step_name), logger, + logging.INFO): + step = get_step(step_name) + step() + clear_cache(scope=_CS_STEP) + + # write out the results for the current iteration + if data_out: + if (i - 1) % out_interval == 0 or i == max_i: + write_tables(data_out, out_run_tables, var, compress=compress, local=out_run_local) + + clear_cache(scope=_CS_ITER) + + +@contextmanager +def injectables(**kwargs): + """ + Temporarily add injectables to the pipeline environment. + Takes only keyword arguments. + + Injectables will be returned to their original state when the context + manager exits. + + """ + global _INJECTABLES + + original = _INJECTABLES.copy() + _INJECTABLES.update(kwargs) + yield + _INJECTABLES = original + + +@contextmanager +def temporary_tables(**kwargs): + """ + Temporarily set DataFrames as registered tables. + + Tables will be returned to their original state when the context + manager exits. Caching is not enabled for tables registered via + this function. + + """ + global _TABLES + + original = _TABLES.copy() + + for k, v in kwargs.items(): + if not isinstance(v, pd.DataFrame): + raise ValueError('tables only accepts DataFrames') + add_table(k, v) + + yield + + _TABLES = original + + +def eval_variable(name, **kwargs): + """ + Execute a single variable function registered with Orca + and return the result. Any keyword arguments are temporarily set + as injectables. This gives the value as would be injected into a function. + + Parameters + ---------- + name : str + Name of variable to evaluate. + Use variable expressions to specify columns. + + Returns + ------- + object + For injectables and columns this directly returns whatever + object is returned by the registered function. + For tables this returns a DataFrameWrapper as if the table + had been injected into a function. + + """ + with injectables(**kwargs): + vars = _collect_variables([name], [name]) + return vars[name] + + +def eval_step(name, **kwargs): + """ + Evaluate a step as would be done within the pipeline environment + and return the result. Any keyword arguments are temporarily set + as injectables. + + Parameters + ---------- + name : str + Name of step to run. + + Returns + ------- + object + Anything returned by a step. (Though note that in Orca runs + return values from steps are ignored.) + + """ + with injectables(**kwargs): + return get_step(name)() diff --git a/activitysim/core/orca/tests/__init__.py b/activitysim/core/orca/tests/__init__.py new file mode 100644 index 000000000..0d4e355c7 --- /dev/null +++ b/activitysim/core/orca/tests/__init__.py @@ -0,0 +1,3 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. diff --git a/activitysim/core/orca/tests/test_mergetables.py b/activitysim/core/orca/tests/test_mergetables.py new file mode 100644 index 000000000..8c924bc6e --- /dev/null +++ b/activitysim/core/orca/tests/test_mergetables.py @@ -0,0 +1,257 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +import pandas as pd +import pytest + +from .. import orca +from ..utils.testing import assert_frames_equal + + +def setup_function(func): + orca.clear_all() + + +def teardown_function(func): + orca.clear_all() + + +@pytest.fixture +def dfa(): + return orca.DataFrameWrapper('a', pd.DataFrame( + {'a1': [1, 2, 3], + 'a2': [4, 5, 6], + 'a3': [7, 8, 9]}, + index=['aa', 'ab', 'ac'])) + + +@pytest.fixture +def dfz(): + return orca.DataFrameWrapper('z', pd.DataFrame( + {'z1': [90, 91], + 'z2': [92, 93], + 'z3': [94, 95], + 'z4': [96, 97], + 'z5': [98, 99]}, + index=['za', 'zb'])) + + +@pytest.fixture +def dfb(): + return orca.DataFrameWrapper('b', pd.DataFrame( + {'b1': range(10, 15), + 'b2': range(15, 20), + 'a_id': ['ac', 'ac', 'ab', 'aa', 'ab'], + 'z_id': ['zb', 'zb', 'za', 'za', 'zb']}, + index=['ba', 'bb', 'bc', 'bd', 'be'])) + + +@pytest.fixture +def dfc(): + return orca.DataFrameWrapper('c', pd.DataFrame( + {'c1': range(20, 30), + 'c2': range(30, 40), + 'b_id': ['ba', 'bd', 'bb', 'bc', 'bb', 'ba', 'bb', 'bc', 'bd', 'bb']}, + index=['ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'cg', 'ch', 'ci', 'cj'])) + + +@pytest.fixture +def dfg(): + return orca.DataFrameWrapper('g', pd.DataFrame( + {'g1': [1, 2, 3]}, + index=['ga', 'gb', 'gc'])) + + +@pytest.fixture +def dfh(): + return orca.DataFrameWrapper('h', pd.DataFrame( + {'h1': range(10, 15), + 'g_id': ['ga', 'gb', 'gc', 'ga', 'gb']}, + index=['ha', 'hb', 'hc', 'hd', 'he'])) + + +def all_broadcasts(): + orca.broadcast('a', 'b', cast_index=True, onto_on='a_id') + orca.broadcast('z', 'b', cast_index=True, onto_on='z_id') + orca.broadcast('b', 'c', cast_index=True, onto_on='b_id') + orca.broadcast('g', 'h', cast_index=True, onto_on='g_id') + + +def test_recursive_getitem(): + assert orca._recursive_getitem({'a': {}}, 'a') == {'a': {}} + assert orca._recursive_getitem( + {'a': {'b': {'c': {'d': {}, 'e': {}}}}}, 'e') == {'d': {}, 'e': {}} + + with pytest.raises(KeyError): + orca._recursive_getitem({'a': {'b': {'c': {'d': {}, 'e': {}}}}}, 'f') + + +def test_dict_value_to_pairs(): + assert sorted(orca._dict_value_to_pairs({'c': {'a': 1, 'b': 2}}), + key=lambda d: next(iter(d))) == \ + [{'a': 1}, {'b': 2}] + + +def test_is_leaf_node(): + assert orca._is_leaf_node({'b': {'a': {}}}) is False + assert orca._is_leaf_node({'a': {}}) is True + + +def test_next_merge(): + assert orca._next_merge({'d': {'c': {}, 'b': {'a': {}}}}) == \ + {'b': {'a': {}}} + assert orca._next_merge({'b': {'a': {}, 'z': {}}}) == \ + {'b': {'a': {}, 'z': {}}} + + +def test_merge_tables_raises(dfa, dfz, dfb, dfg, dfh): + all_broadcasts() + + with pytest.raises(RuntimeError): + orca.merge_tables('b', [dfa, dfb, dfz, dfg, dfh]) + + +def test_merge_tables1(dfa, dfz, dfb): + all_broadcasts() + + merged = orca.merge_tables('b', [dfa, dfz, dfb]) + + expected = pd.merge( + dfa.to_frame(), dfb.to_frame(), left_index=True, right_on='a_id') + expected = pd.merge( + expected, dfz.to_frame(), left_on='z_id', right_index=True) + + assert_frames_equal(merged, expected) + + +def test_merge_tables2(dfa, dfz, dfb, dfc): + all_broadcasts() + + merged = orca.merge_tables(dfc, [dfa, dfz, dfb, dfc]) + + expected = pd.merge( + dfa.to_frame(), dfb.to_frame(), left_index=True, right_on='a_id') + expected = pd.merge( + expected, dfz.to_frame(), left_on='z_id', right_index=True) + expected = pd.merge( + expected, dfc.to_frame(), left_index=True, right_on='b_id') + + assert_frames_equal(merged, expected) + + +def test_merge_tables_cols(dfa, dfz, dfb, dfc): + all_broadcasts() + + merged = orca.merge_tables( + 'c', [dfa, dfz, dfb, dfc], columns=['a1', 'b1', 'z1', 'c1']) + + expected = pd.DataFrame( + {'c1': range(20, 30), + 'b1': [10, 13, 11, 12, 11, 10, 11, 12, 13, 11], + 'a1': [3, 1, 3, 2, 3, 3, 3, 2, 1, 3], + 'z1': [91, 90, 91, 90, 91, 91, 91, 90, 90, 91]}, + index=['ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'cg', 'ch', 'ci', 'cj']) + + assert_frames_equal(merged, expected) + + +def test_merge_tables3(): + df_a = pd.DataFrame( + {'a': [0, 1]}, + index=['a0', 'a1']) + df_b = pd.DataFrame( + {'b': [2, 3, 4, 5, 6], + 'a_id': ['a0', 'a1', 'a1', 'a0', 'a1']}, + index=['b0', 'b1', 'b2', 'b3', 'b4']) + df_c = pd.DataFrame( + {'c': [7, 8, 9]}, + index=['c0', 'c1', 'c2']) + df_d = pd.DataFrame( + {'d': [10, 11, 12, 13, 15, 16, 16, 17, 18, 19], + 'b_id': ['b2', 'b0', 'b3', 'b3', 'b1', 'b4', 'b1', 'b4', 'b3', 'b3'], + 'c_id': ['c0', 'c1', 'c1', 'c0', 'c0', 'c2', 'c1', 'c2', 'c1', 'c2']}, + index=['d0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9']) + + orca.add_table('a', df_a) + orca.add_table('b', df_b) + orca.add_table('c', df_c) + orca.add_table('d', df_d) + + orca.broadcast(cast='a', onto='b', cast_index=True, onto_on='a_id') + orca.broadcast(cast='b', onto='d', cast_index=True, onto_on='b_id') + orca.broadcast(cast='c', onto='d', cast_index=True, onto_on='c_id') + + df = orca.merge_tables(target='d', tables=['a', 'b', 'c', 'd']) + + expected = pd.merge(df_a, df_b, left_index=True, right_on='a_id') + expected = pd.merge(expected, df_d, left_index=True, right_on='b_id') + expected = pd.merge(df_c, expected, left_index=True, right_on='c_id') + + assert_frames_equal(df, expected) + + +def test_merge_tables_dup_columns(): + # I'm intentionally setting the zone-ids to something different when joined + # in a real case they'd likely be the same but the whole point of this + # test is to see if we can get them back with different names tied to each + # table and they need to be different to test if that's working + hh_df = pd.DataFrame({'zone_id': [1, 1, 2], 'building_id': [5, 5, 6]}) + orca.add_table('households', hh_df) + + bldg_df = pd.DataFrame( + {'zone_id': [2, 3], 'parcel_id': [0, 1]}, index=[5, 6]) + orca.add_table('buildings', bldg_df) + + parcels_df = pd.DataFrame({'zone_id': [4, 5]}, index=[0, 1]) + orca.add_table('parcels', parcels_df) + + orca.broadcast( + 'buildings', 'households', cast_index=True, onto_on='building_id') + orca.broadcast('parcels', 'buildings', cast_index=True, onto_on='parcel_id') + + df = orca.merge_tables( + target='households', tables=['households', 'buildings', 'parcels']) + + expected = pd.DataFrame( + {'building_id': [5, 5, 6], 'parcel_id': [0, 0, 1], 'zone_id': [1, 1, 2]}) + assert_frames_equal(df, expected) + + df = orca.merge_tables( + target='households', + tables=['households', 'buildings', 'parcels'], + drop_intersection=False) + + expected = pd.DataFrame({ + 'building_id': [5, 5, 6], + 'parcel_id': [0, 0, 1], + 'zone_id_households': [1, 1, 2], + 'zone_id_buildings': [2, 2, 3], + 'zone_id_parcels': [4, 4, 5] + }) + assert_frames_equal(df, expected) + + df = orca.merge_tables( + target='households', + tables=['households', 'buildings'], + drop_intersection=False) + + expected = pd.DataFrame({ + 'building_id': [5, 5, 6], + 'parcel_id': [0, 0, 1], + 'zone_id_households': [1, 1, 2], + 'zone_id_buildings': [2, 2, 3] + }) + assert_frames_equal(df, expected) + + df = orca.merge_tables( + target='households', + tables=['households', 'buildings'] + ) + + expected = pd.DataFrame({ + 'building_id': [5, 5, 6], + 'parcel_id': [0, 0, 1], + 'zone_id': [1, 1, 2] + }) + assert_frames_equal(df, expected) diff --git a/activitysim/core/orca/tests/test_orca.py b/activitysim/core/orca/tests/test_orca.py new file mode 100644 index 000000000..aae3c3093 --- /dev/null +++ b/activitysim/core/orca/tests/test_orca.py @@ -0,0 +1,1260 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +import os +import tempfile + +import pandas as pd +import pytest +from pandas.util import testing as pdt + +from .. import orca +from ..utils.testing import assert_frames_equal + + +def setup_function(func): + orca.clear_all() + orca.enable_cache() + + +def teardown_function(func): + orca.clear_all() + orca.enable_cache() + + +@pytest.fixture +def df(): + return pd.DataFrame( + [[1, 4], + [2, 5], + [3, 6]], + columns=['a', 'b'], + index=['x', 'y', 'z']) + + +def test_tables(df): + wrapped_df = orca.add_table('test_frame', df) + + @orca.table() + def test_func(test_frame): + return test_frame.to_frame() / 2 + + assert set(orca.list_tables()) == {'test_frame', 'test_func'} + + table = orca.get_table('test_frame') + assert table is wrapped_df + assert table.columns == ['a', 'b'] + assert table.local_columns == ['a', 'b'] + assert len(table) == 3 + pdt.assert_index_equal(table.index, df.index) + pdt.assert_series_equal(table.get_column('a'), df.a) + pdt.assert_series_equal(table.a, df.a) + pdt.assert_series_equal(table['b'], df['b']) + + table = orca._TABLES['test_func'] + assert table.index is None + assert table.columns == [] + assert len(table) is 0 + pdt.assert_frame_equal(table.to_frame(), df / 2) + pdt.assert_frame_equal(table.to_frame([]), df[[]]) + pdt.assert_frame_equal(table.to_frame(columns=['a']), df[['a']] / 2) + pdt.assert_frame_equal(table.to_frame(columns='a'), df[['a']] / 2) + pdt.assert_index_equal(table.index, df.index) + pdt.assert_series_equal(table.get_column('a'), df.a / 2) + pdt.assert_series_equal(table.a, df.a / 2) + pdt.assert_series_equal(table['b'], df['b'] / 2) + assert len(table) == 3 + assert table.columns == ['a', 'b'] + + +def test_table_func_cache(df): + orca.add_injectable('x', 2) + + @orca.table(cache=True) + def table(variable='x'): + return df * variable + + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 2) + orca.add_injectable('x', 3) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 2) + orca.get_table('table').clear_cached() + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 3) + orca.add_injectable('x', 4) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 3) + orca.clear_cache() + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 4) + orca.add_injectable('x', 5) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 4) + orca.add_table('table', table) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 5) + + +def test_table_func_cache_disabled(df): + orca.add_injectable('x', 2) + + @orca.table('table', cache=True) + def asdf(x): + return df * x + + orca.disable_cache() + + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 2) + orca.add_injectable('x', 3) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 3) + + orca.enable_cache() + + orca.add_injectable('x', 4) + pdt.assert_frame_equal(orca.get_table('table').to_frame(), df * 3) + + +def test_table_copy(df): + orca.add_table('test_frame_copied', df, copy_col=True) + orca.add_table('test_frame_uncopied', df, copy_col=False) + orca.add_table('test_func_copied', lambda: df, copy_col=True) + orca.add_table('test_func_uncopied', lambda: df, copy_col=False) + + @orca.table(copy_col=True) + def test_funcd_copied(): + return df + + @orca.table(copy_col=False) + def test_funcd_uncopied(): + return df + + @orca.table(copy_col=True) + def test_funcd_copied2(test_frame_copied): + # local returns original, but it is copied by copy_col. + return test_frame_copied.local + + @orca.table(copy_col=True) + def test_funcd_copied3(test_frame_uncopied): + # local returns original, but it is copied by copy_col. + return test_frame_uncopied.local + + @orca.table(copy_col=False) + def test_funcd_uncopied2(test_frame_copied): + # local returns original. + return test_frame_copied.local + + @orca.table(copy_col=False) + def test_funcd_uncopied3(test_frame_uncopied): + # local returns original. + return test_frame_uncopied.local + + orca.add_table('test_cache_copied', lambda: df, cache=True, copy_col=True) + orca.add_table( + 'test_cache_uncopied', lambda: df, cache=True, copy_col=False) + + @orca.table(cache=True, copy_col=True) + def test_cached_copied(): + return df + + @orca.table(cache=True, copy_col=False) + def test_cached_uncopied(): + return df + + # Create tables with computed columns. + orca.add_table( + 'test_copied_columns', pd.DataFrame(index=df.index), copy_col=True) + orca.add_table( + 'test_uncopied_columns', pd.DataFrame(index=df.index), copy_col=False) + for column_name in ['a', 'b']: + label = "test_frame_uncopied.{}".format(column_name) + + def func(col=label): + return col + for table_name in ['test_copied_columns', 'test_uncopied_columns']: + orca.add_column(table_name, column_name, func) + + for name in ['test_frame_uncopied', 'test_func_uncopied', + 'test_funcd_uncopied', 'test_funcd_uncopied2', + 'test_funcd_uncopied3', 'test_cache_uncopied', + 'test_cached_uncopied', 'test_uncopied_columns', + 'test_frame_copied', 'test_func_copied', + 'test_funcd_copied', 'test_funcd_copied2', + 'test_funcd_copied3', 'test_cache_copied', + 'test_cached_copied', 'test_copied_columns']: + table = orca.get_table(name) + table2 = orca.get_table(name) + + # to_frame will always return a copy. + if 'columns' in name: + assert_frames_equal(table.to_frame(), df) + else: + pdt.assert_frame_equal(table.to_frame(), df) + assert table.to_frame() is not df + pdt.assert_frame_equal(table.to_frame(), table.to_frame()) + assert table.to_frame() is not table.to_frame() + pdt.assert_series_equal(table.to_frame()['a'], df['a']) + assert table.to_frame()['a'] is not df['a'] + pdt.assert_series_equal(table.to_frame()['a'], + table.to_frame()['a']) + assert table.to_frame()['a'] is not table.to_frame()['a'] + + if 'uncopied' in name: + pdt.assert_series_equal(table['a'], df['a']) + assert table['a'] is df['a'] + pdt.assert_series_equal(table['a'], table2['a']) + assert table['a'] is table2['a'] + else: + pdt.assert_series_equal(table['a'], df['a']) + assert table['a'] is not df['a'] + pdt.assert_series_equal(table['a'], table2['a']) + assert table['a'] is not table2['a'] + + +def test_columns_for_table(): + orca.add_column( + 'table1', 'col10', pd.Series([1, 2, 3], index=['a', 'b', 'c'])) + orca.add_column( + 'table2', 'col20', pd.Series([10, 11, 12], index=['x', 'y', 'z'])) + + @orca.column('table1') + def col11(): + return pd.Series([4, 5, 6], index=['a', 'b', 'c']) + + @orca.column('table2', 'col21') + def asdf(): + return pd.Series([13, 14, 15], index=['x', 'y', 'z']) + + t1_col_names = orca.list_columns_for_table('table1') + assert set(t1_col_names) == {'col10', 'col11'} + + t2_col_names = orca.list_columns_for_table('table2') + assert set(t2_col_names) == {'col20', 'col21'} + + t1_cols = orca._columns_for_table('table1') + assert 'col10' in t1_cols and 'col11' in t1_cols + + t2_cols = orca._columns_for_table('table2') + assert 'col20' in t2_cols and 'col21' in t2_cols + + +def test_columns_and_tables(df): + orca.add_table('test_frame', df) + + @orca.table() + def test_func(test_frame): + return test_frame.to_frame() / 2 + + orca.add_column('test_frame', 'c', pd.Series([7, 8, 9], index=df.index)) + + @orca.column('test_func', 'd') + def asdf(test_func): + return test_func.to_frame(columns=['b'])['b'] * 2 + + @orca.column('test_func') + def e(column='test_func.d'): + return column + 1 + + test_frame = orca.get_table('test_frame') + assert set(test_frame.columns) == set(['a', 'b', 'c']) + assert_frames_equal( + test_frame.to_frame(), + pd.DataFrame( + {'a': [1, 2, 3], + 'b': [4, 5, 6], + 'c': [7, 8, 9]}, + index=['x', 'y', 'z'])) + assert_frames_equal( + test_frame.to_frame(columns=['a', 'c']), + pd.DataFrame( + {'a': [1, 2, 3], + 'c': [7, 8, 9]}, + index=['x', 'y', 'z'])) + + test_func_df = orca._TABLES['test_func'] + assert set(test_func_df.columns) == set(['d', 'e']) + assert_frames_equal( + test_func_df.to_frame(), + pd.DataFrame( + {'a': [0.5, 1, 1.5], + 'b': [2, 2.5, 3], + 'c': [3.5, 4, 4.5], + 'd': [4., 5., 6.], + 'e': [5., 6., 7.]}, + index=['x', 'y', 'z'])) + assert_frames_equal( + test_func_df.to_frame(columns=['b', 'd']), + pd.DataFrame( + {'b': [2, 2.5, 3], + 'd': [4., 5., 6.]}, + index=['x', 'y', 'z'])) + assert set(test_func_df.columns) == set(['a', 'b', 'c', 'd', 'e']) + + assert set(orca.list_columns()) == { + ('test_frame', 'c'), ('test_func', 'd'), ('test_func', 'e')} + + +def test_column_cache(df): + orca.add_injectable('x', 2) + series = pd.Series([1, 2, 3], index=['x', 'y', 'z']) + key = ('table', 'col') + + @orca.table() + def table(): + return df + + @orca.column(*key, cache=True) + def column(variable='x'): + return series * variable + + def c(): + return orca._COLUMNS[key] + + pdt.assert_series_equal(c()(), series * 2) + orca.add_injectable('x', 3) + pdt.assert_series_equal(c()(), series * 2) + c().clear_cached() + pdt.assert_series_equal(c()(), series * 3) + orca.add_injectable('x', 4) + pdt.assert_series_equal(c()(), series * 3) + orca.clear_cache() + pdt.assert_series_equal(c()(), series * 4) + orca.add_injectable('x', 5) + pdt.assert_series_equal(c()(), series * 4) + orca.get_table('table').clear_cached() + pdt.assert_series_equal(c()(), series * 5) + orca.add_injectable('x', 6) + pdt.assert_series_equal(c()(), series * 5) + orca.add_column(*key, column=column, cache=True) + pdt.assert_series_equal(c()(), series * 6) + + +def test_column_cache_disabled(df): + orca.add_injectable('x', 2) + series = pd.Series([1, 2, 3], index=['x', 'y', 'z']) + key = ('table', 'col') + + @orca.table() + def table(): + return df + + @orca.column(*key, cache=True) + def column(x): + return series * x + + def c(): + return orca._COLUMNS[key] + + orca.disable_cache() + + pdt.assert_series_equal(c()(), series * 2) + orca.add_injectable('x', 3) + pdt.assert_series_equal(c()(), series * 3) + + orca.enable_cache() + + orca.add_injectable('x', 4) + pdt.assert_series_equal(c()(), series * 3) + + +def test_update_col(df): + wrapped = orca.add_table('table', df) + + wrapped.update_col('b', pd.Series([7, 8, 9], index=df.index)) + pdt.assert_series_equal( + wrapped['b'], pd.Series([7, 8, 9], index=df.index, name='b')) + + a_dtype = wrapped['a'].dtype + + # test 1 - cast the data type before the update + wrapped.update_col_from_series('a', pd.Series(dtype=a_dtype)) + pdt.assert_series_equal(wrapped['a'], df['a']) + + # test 2 - let the update method do the cast + wrapped.update_col_from_series('a', pd.Series(), True) + pdt.assert_series_equal(wrapped['a'], df['a']) + + # test 3 - don't cast, should raise an error + with pytest.raises(ValueError): + wrapped.update_col_from_series('a', pd.Series()) + + wrapped.update_col_from_series('a', pd.Series([99], index=['y'])) + pdt.assert_series_equal( + wrapped['a'], pd.Series([1, 99, 3], index=df.index, name='a')) + + +class _FakeTable(object): + def __init__(self, name, columns): + self.name = name + self.columns = columns + + +@pytest.fixture +def fta(): + return _FakeTable('a', ['aa', 'ab', 'ac']) + + +@pytest.fixture +def ftb(): + return _FakeTable('b', ['bx', 'by', 'bz']) + + +def test_column_map_raises(fta, ftb): + with pytest.raises(RuntimeError): + orca.column_map([fta, ftb], ['aa', 'by', 'bz', 'cw']) + + +def test_column_map_none(fta, ftb): + assert orca.column_map([fta, ftb], None) == {'a': None, 'b': None} + + +def test_column_map(fta, ftb): + result = orca.column_map([fta, ftb], ['aa', 'by', 'bz']) + assert result['a'] == ['aa'] + assert sorted(result['b']) == ['by', 'bz'] + + result = orca.column_map([fta, ftb], ['by', 'bz']) + assert result['a'] == [] + assert sorted(result['b']) == ['by', 'bz'] + + +def test_is_step(): + @orca.step() + def test_step(): + pass + + assert orca.is_step('test_step') is True + assert orca.is_step('not_a_step') is False + + +def test_steps(df): + orca.add_table('test_table', df) + + df2 = df / 2 + orca.add_table('test_table2', df2) + + @orca.step() + def test_step(test_table, test_column='test_table2.b'): + tt = test_table.to_frame() + test_table['a'] = tt['a'] + tt['b'] + pdt.assert_series_equal(test_column, df2['b']) + + with pytest.raises(KeyError): + orca.get_step('asdf') + + step = orca.get_step('test_step') + assert step._tables_used() == set(['test_table', 'test_table2']) + step() + + table = orca.get_table('test_table') + pdt.assert_frame_equal( + table.to_frame(), + pd.DataFrame( + {'a': [5, 7, 9], + 'b': [4, 5, 6]}, + index=['x', 'y', 'z'])) + + assert orca.list_steps() == ['test_step'] + + +def test_step_run(df): + orca.add_table('test_table', df) + + @orca.table() + def table_func(test_table): + tt = test_table.to_frame() + tt['c'] = [7, 8, 9] + return tt + + @orca.column('table_func') + def new_col(test_table, table_func): + tt = test_table.to_frame() + tf = table_func.to_frame(columns=['c']) + return tt['a'] + tt['b'] + tf['c'] + + @orca.step() + def test_step1(iter_var, test_table, table_func): + tf = table_func.to_frame(columns=['new_col']) + test_table[iter_var] = tf['new_col'] + iter_var + + @orca.step('test_step2') + def asdf(table='test_table'): + tt = table.to_frame() + table['a'] = tt['a'] ** 2 + + orca.run(steps=['test_step1', 'test_step2'], iter_vars=[2000, 3000]) + + test_table = orca.get_table('test_table') + assert_frames_equal( + test_table.to_frame(), + pd.DataFrame( + {'a': [1, 16, 81], + 'b': [4, 5, 6], + 2000: [2012, 2015, 2018], + 3000: [3012, 3017, 3024]}, + index=['x', 'y', 'z'])) + + m = orca.get_step('test_step1') + assert set(m._tables_used()) == {'test_table', 'table_func'} + + +def test_step_func_source_data(): + @orca.step() + def test_step(): + return 'orca' + + filename, lineno, source = orca.get_step('test_step').func_source_data() + + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert source == ( + " @orca.step()\n" + " def test_step():\n" + " return 'orca'\n") + + +def test_get_broadcast(): + orca.broadcast('a', 'b', cast_on='ax', onto_on='bx') + orca.broadcast('x', 'y', cast_on='yx', onto_index=True) + + assert orca.is_broadcast('a', 'b') is True + assert orca.is_broadcast('b', 'a') is False + + with pytest.raises(KeyError): + orca.get_broadcast('b', 'a') + + ab = orca.get_broadcast('a', 'b') + assert isinstance(ab, orca.Broadcast) + assert ab == ('a', 'b', 'ax', 'bx', False, False) + + xy = orca.get_broadcast('x', 'y') + assert isinstance(xy, orca.Broadcast) + assert xy == ('x', 'y', 'yx', None, False, True) + + +def test_get_broadcasts(): + orca.broadcast('a', 'b') + orca.broadcast('b', 'c') + orca.broadcast('z', 'b') + orca.broadcast('f', 'g') + + with pytest.raises(ValueError): + orca._get_broadcasts(['a', 'b', 'g']) + + assert set(orca._get_broadcasts(['a', 'b', 'c', 'z']).keys()) == \ + {('a', 'b'), ('b', 'c'), ('z', 'b')} + assert set(orca._get_broadcasts(['a', 'b', 'z']).keys()) == \ + {('a', 'b'), ('z', 'b')} + assert set(orca._get_broadcasts(['a', 'b', 'c']).keys()) == \ + {('a', 'b'), ('b', 'c')} + + assert set(orca.list_broadcasts()) == \ + {('a', 'b'), ('b', 'c'), ('z', 'b'), ('f', 'g')} + + +def test_collect_variables(df): + orca.add_table('df', df) + + @orca.table() + def df_func(): + return df + + @orca.column('df') + def zzz(): + return df['a'] / 2 + + orca.add_injectable('answer', 42) + + @orca.injectable() + def injected(): + return 'injected' + + @orca.table('source table', cache=True) + def source(): + return df + + with pytest.raises(KeyError): + orca._collect_variables(['asdf']) + + with pytest.raises(KeyError): + orca._collect_variables(names=['df'], expressions=['asdf']) + + names = ['df', 'df_func', 'answer', 'injected', 'source_label', 'df_a'] + expressions = ['source table', 'df.a'] + things = orca._collect_variables(names, expressions) + + assert set(things.keys()) == set(names) + assert isinstance(things['source_label'], orca.DataFrameWrapper) + pdt.assert_frame_equal(things['source_label'].to_frame(), df) + assert isinstance(things['df_a'], pd.Series) + pdt.assert_series_equal(things['df_a'], df['a']) + + +def test_collect_variables_expression_only(df): + @orca.table() + def table(): + return df + + vars = orca._collect_variables(['a'], ['table.a']) + pdt.assert_series_equal(vars['a'], df.a) + + +def test_injectables(): + orca.add_injectable('answer', 42) + + @orca.injectable() + def func1(answer): + return answer * 2 + + @orca.injectable('func2', autocall=False) + def asdf(variable='x'): + return variable / 2 + + @orca.injectable() + def func3(func2): + return func2(4) + + @orca.injectable() + def func4(func='func1'): + return func / 2 + + assert orca._INJECTABLES['answer'] == 42 + assert orca._INJECTABLES['func1']() == 42 * 2 + assert orca._INJECTABLES['func2'](4) == 2 + assert orca._INJECTABLES['func3']() == 2 + assert orca._INJECTABLES['func4']() == 42 + + assert orca.get_injectable('answer') == 42 + assert orca.get_injectable('func1') == 42 * 2 + assert orca.get_injectable('func2')(4) == 2 + assert orca.get_injectable('func3') == 2 + assert orca.get_injectable('func4') == 42 + + with pytest.raises(KeyError): + orca.get_injectable('asdf') + + assert set(orca.list_injectables()) == \ + {'answer', 'func1', 'func2', 'func3', 'func4'} + + +def test_injectables_combined(df): + @orca.injectable() + def column(): + return pd.Series(['a', 'b', 'c'], index=df.index) + + @orca.table() + def table(): + return df + + @orca.step() + def step(table, column): + df = table.to_frame() + df['new'] = column + orca.add_table('table', df) + + orca.run(steps=['step']) + + table_wr = orca.get_table('table').to_frame() + + pdt.assert_frame_equal(table_wr[['a', 'b']], df) + pdt.assert_series_equal(table_wr['new'], pd.Series(column(), name='new')) + + +def test_injectables_cache(): + x = 2 + + @orca.injectable(autocall=True, cache=True) + def inj(): + return x * x + + def i(): + return orca._INJECTABLES['inj'] + + assert i()() == 4 + x = 3 + assert i()() == 4 + i().clear_cached() + assert i()() == 9 + x = 4 + assert i()() == 9 + orca.clear_cache() + assert i()() == 16 + x = 5 + assert i()() == 16 + orca.add_injectable('inj', inj, autocall=True, cache=True) + assert i()() == 25 + + +def test_injectables_cache_disabled(): + x = 2 + + @orca.injectable(autocall=True, cache=True) + def inj(): + return x * x + + def i(): + return orca._INJECTABLES['inj'] + + orca.disable_cache() + + assert i()() == 4 + x = 3 + assert i()() == 9 + + orca.enable_cache() + + assert i()() == 9 + x = 4 + assert i()() == 9 + + orca.disable_cache() + assert i()() == 16 + + +def test_memoized_injectable(): + outside = 'x' + + @orca.injectable(autocall=False, memoize=True) + def x(s): + return outside + s + + assert 'x' in orca._MEMOIZED + + def getx(): + return orca.get_injectable('x') + + assert hasattr(getx(), 'cache') + assert hasattr(getx(), 'clear_cached') + + assert getx()('y') == 'xy' + outside = 'z' + assert getx()('y') == 'xy' + + getx().clear_cached() + + assert getx()('y') == 'zy' + + +def test_memoized_injectable_cache_off(): + outside = 'x' + + @orca.injectable(autocall=False, memoize=True) + def x(s): + return outside + s + + def getx(): + return orca.get_injectable('x')('y') + + orca.disable_cache() + + assert getx() == 'xy' + outside = 'z' + assert getx() == 'zy' + + orca.enable_cache() + outside = 'a' + + assert getx() == 'zy' + + orca.disable_cache() + + assert getx() == 'ay' + + +def test_clear_cache_all(df): + @orca.table(cache=True) + def table(): + return df + + @orca.column('table', cache=True) + def z(table): + return df.a + + @orca.injectable(cache=True) + def x(): + return 'x' + + @orca.injectable(autocall=False, memoize=True) + def y(s): + return s + 'y' + + orca.eval_variable('table.z') + orca.eval_variable('x') + orca.get_injectable('y')('x') + + assert list(orca._TABLE_CACHE.keys()) == ['table'] + assert list(orca._COLUMN_CACHE.keys()) == [('table', 'z')] + assert list(orca._INJECTABLE_CACHE.keys()) == ['x'] + assert orca._MEMOIZED['y'].value.cache == {(('x',), None): 'xy'} + + orca.clear_cache() + + assert orca._TABLE_CACHE == {} + assert orca._COLUMN_CACHE == {} + assert orca._INJECTABLE_CACHE == {} + assert orca._MEMOIZED['y'].value.cache == {} + + +def test_clear_cache_scopes(df): + @orca.table(cache=True, cache_scope='forever') + def table(): + return df + + @orca.column('table', cache=True, cache_scope='iteration') + def z(table): + return df.a + + @orca.injectable(cache=True, cache_scope='step') + def x(): + return 'x' + + @orca.injectable(autocall=False, memoize=True, cache_scope='iteration') + def y(s): + return s + 'y' + + orca.eval_variable('table.z') + orca.eval_variable('x') + orca.get_injectable('y')('x') + + assert list(orca._TABLE_CACHE.keys()) == ['table'] + assert list(orca._COLUMN_CACHE.keys()) == [('table', 'z')] + assert list(orca._INJECTABLE_CACHE.keys()) == ['x'] + assert orca._MEMOIZED['y'].value.cache == {(('x',), None): 'xy'} + + orca.clear_cache(scope='step') + + assert list(orca._TABLE_CACHE.keys()) == ['table'] + assert list(orca._COLUMN_CACHE.keys()) == [('table', 'z')] + assert orca._INJECTABLE_CACHE == {} + assert orca._MEMOIZED['y'].value.cache == {(('x',), None): 'xy'} + + orca.clear_cache(scope='iteration') + + assert list(orca._TABLE_CACHE.keys()) == ['table'] + assert orca._COLUMN_CACHE == {} + assert orca._INJECTABLE_CACHE == {} + assert orca._MEMOIZED['y'].value.cache == {} + + orca.clear_cache(scope='forever') + + assert orca._TABLE_CACHE == {} + assert orca._COLUMN_CACHE == {} + assert orca._INJECTABLE_CACHE == {} + assert orca._MEMOIZED['y'].value.cache == {} + + +def test_cache_scope(df): + orca.add_injectable('x', 11) + orca.add_injectable('y', 22) + orca.add_injectable('z', 33) + orca.add_injectable('iterations', 1) + + @orca.injectable(cache=True, cache_scope='forever') + def a(x): + return x + + @orca.injectable(cache=True, cache_scope='iteration') + def b(y): + return y + + @orca.injectable(cache=True, cache_scope='step') + def c(z): + return z + + @orca.step() + def m1(iter_var, a, b, c): + orca.add_injectable('x', iter_var + a) + orca.add_injectable('y', iter_var + b) + orca.add_injectable('z', iter_var + c) + + assert a == 11 + + @orca.step() + def m2(iter_var, a, b, c, iterations): + assert a == 11 + if iter_var == 1000: + assert b == 22 + assert c == 1033 + elif iter_var == 2000: + assert b == 1022 + assert c == 3033 + + orca.add_injectable('iterations', iterations + 1) + + orca.run(['m1', 'm2'], iter_vars=[1000, 2000]) + + +def test_table_func_local_cols(df): + @orca.table() + def table(): + return df + orca.add_column( + 'table', 'new', pd.Series(['a', 'b', 'c'], index=df.index)) + + assert orca.get_table('table').local_columns == ['a', 'b'] + + +def test_is_table(df): + orca.add_table('table', df) + assert orca.is_table('table') is True + assert orca.is_table('asdf') is False + + +@pytest.fixture +def store_name(request): + fname = tempfile.NamedTemporaryFile(suffix='.h5').name + + def fin(): + if os.path.isfile(fname): + os.remove(fname) + request.addfinalizer(fin) + + return fname + + +def test_write_tables(df, store_name): + orca.add_table('table', df) + + @orca.step() + def step(table): + pass + + step_tables = orca.get_step_table_names(['step']) + + orca.write_tables(store_name, step_tables, None) + with pd.HDFStore(store_name, mode='r') as store: + assert 'table' in store + pdt.assert_frame_equal(store['table'], df) + + orca.write_tables(store_name, step_tables, 1969) + + with pd.HDFStore(store_name, mode='r') as store: + assert '1969/table' in store + pdt.assert_frame_equal(store['1969/table'], df) + + +def test_write_all_tables(df, store_name): + orca.add_table('table', df) + orca.write_tables(store_name) + + with pd.HDFStore(store_name, mode='r') as store: + for t in orca.list_tables(): + assert t in store + + +def test_run_and_write_tables(df, store_name): + orca.add_table('table', df) + + def year_key(y): + return '{}'.format(y) + + def series_year(y): + return pd.Series([y] * 3, index=df.index, name=str(y)) + + @orca.step() + def step(iter_var, table): + table[year_key(iter_var)] = series_year(iter_var) + + orca.run( + ['step'], iter_vars=range(11), data_out=store_name, out_interval=3) + + with pd.HDFStore(store_name, mode='r') as store: + for year in range(0, 11, 3): + key = '{}/table'.format(year) + assert key in store + + for x in range(year): + pdt.assert_series_equal( + store[key][year_key(x)], series_year(x)) + + assert 'base/table' in store + + for x in range(11): + pdt.assert_series_equal( + store['10/table'][year_key(x)], series_year(x)) + + +def test_run_and_write_tables_out_tables_provided(df, store_name): + table_names = ['table', 'table2', 'table3'] + for t in table_names: + orca.add_table(t, df) + + @orca.step() + def step(iter_var, table, table2): + return + + orca.run( + ['step'], + iter_vars=range(1), + data_out=store_name, + out_base_tables=table_names, + out_run_tables=['table']) + + with pd.HDFStore(store_name, mode='r') as store: + + for t in table_names: + assert 'base/{}'.format(t) in store + + assert '0/table' in store + assert '0/table2' not in store + assert '0/table3' not in store + + +def test_get_raw_table(df): + orca.add_table('table1', df) + + @orca.table() + def table2(): + return df + + assert isinstance(orca.get_raw_table('table1'), orca.DataFrameWrapper) + assert isinstance(orca.get_raw_table('table2'), orca.TableFuncWrapper) + + assert orca.table_type('table1') == 'dataframe' + assert orca.table_type('table2') == 'function' + + +def test_get_table(df): + orca.add_table('frame', df) + + @orca.table() + def table(): + return df + + @orca.table(cache=True) + def source(): + return df + + fr = orca.get_table('frame') + ta = orca.get_table('table') + so = orca.get_table('source') + + with pytest.raises(KeyError): + orca.get_table('asdf') + + assert isinstance(fr, orca.DataFrameWrapper) + assert isinstance(ta, orca.DataFrameWrapper) + assert isinstance(so, orca.DataFrameWrapper) + + pdt.assert_frame_equal(fr.to_frame(), df) + pdt.assert_frame_equal(ta.to_frame(), df) + pdt.assert_frame_equal(so.to_frame(), df) + + +def test_cache_disabled_cm(): + x = 3 + + @orca.injectable(cache=True) + def xi(): + return x + + assert orca.get_injectable('xi') == 3 + x = 5 + assert orca.get_injectable('xi') == 3 + + with orca.cache_disabled(): + assert orca.get_injectable('xi') == 5 + + # cache still gets updated even when cacheing is off + assert orca.get_injectable('xi') == 5 + + +def test_injectables_cm(): + orca.add_injectable('a', 'a') + orca.add_injectable('b', 'b') + orca.add_injectable('c', 'c') + + with orca.injectables(): + assert orca._INJECTABLES == { + 'a': 'a', 'b': 'b', 'c': 'c' + } + + with orca.injectables(c='d', x='x', y='y', z='z'): + assert orca._INJECTABLES == { + 'a': 'a', 'b': 'b', 'c': 'd', + 'x': 'x', 'y': 'y', 'z': 'z' + } + + assert orca._INJECTABLES == { + 'a': 'a', 'b': 'b', 'c': 'c' + } + + +def test_temporary_tables_cm(): + orca.add_table('a', pd.DataFrame()) + + with orca.temporary_tables(): + assert sorted(orca._TABLES.keys()) == ['a'] + + with orca.temporary_tables(a=pd.DataFrame(), b=pd.DataFrame()): + assert sorted(orca._TABLES.keys()) == ['a', 'b'] + + assert sorted(orca._TABLES.keys()) == ['a'] + + +def test_is_expression(): + assert orca.is_expression('name') is False + assert orca.is_expression('table.column') is True + + +def test_eval_variable(df): + orca.add_injectable('x', 3) + assert orca.eval_variable('x') == 3 + + @orca.injectable() + def func(x): + return 'xyz' * x + assert orca.eval_variable('func') == 'xyzxyzxyz' + assert orca.eval_variable('func', x=2) == 'xyzxyz' + + @orca.table() + def table(x): + return df * x + pdt.assert_series_equal(orca.eval_variable('table.a'), df.a * 3) + + +def test_eval_step(df): + orca.add_injectable('x', 3) + + @orca.step() + def step(x): + return df * x + + pdt.assert_frame_equal(orca.eval_step('step'), df * 3) + pdt.assert_frame_equal(orca.eval_step('step', x=5), df * 5) + + +def test_always_dataframewrapper(df): + @orca.table() + def table(): + return df / 2 + + @orca.table() + def table2(table): + assert isinstance(table, orca.DataFrameWrapper) + return table.to_frame() / 2 + + result = orca.eval_variable('table2') + pdt.assert_frame_equal(result.to_frame(), df / 4) + + +def test_table_func_source_data(df): + @orca.table() + def table(): + return df * 2 + + t = orca.get_raw_table('table') + filename, lineno, source = t.func_source_data() + + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert 'return df * 2' in source + + +def test_column_type(df): + orca.add_table('test_frame', df) + + @orca.table() + def test_func(): + return df + + s = pd.Series(range(len(df)), index=df.index) + + def col_func(): + return s + + orca.add_column('test_frame', 'col_series', s) + orca.add_column('test_func', 'col_series', s) + orca.add_column('test_frame', 'col_func', col_func) + orca.add_column('test_func', 'col_func', col_func) + + tframe = orca.get_raw_table('test_frame') + tfunc = orca.get_raw_table('test_func') + + assert tframe.column_type('a') == 'local' + assert tframe.column_type('col_series') == 'series' + assert tframe.column_type('col_func') == 'function' + + assert tfunc.column_type('a') == 'local' + assert tfunc.column_type('col_series') == 'series' + assert tfunc.column_type('col_func') == 'function' + + +def test_get_raw_column(df): + orca.add_table('test_frame', df) + + s = pd.Series(range(len(df)), index=df.index) + + def col_func(): + return s + + orca.add_column('test_frame', 'col_series', s) + orca.add_column('test_frame', 'col_func', col_func) + + assert isinstance( + orca.get_raw_column('test_frame', 'col_series'), + orca._SeriesWrapper) + assert isinstance( + orca.get_raw_column('test_frame', 'col_func'), + orca._ColumnFuncWrapper) + + +def test_column_func_source_data(df): + orca.add_table('test_frame', df) + + @orca.column('test_frame') + def col_func(): + return pd.Series(range(len(df)), index=df.index) + + s = orca.get_raw_column('test_frame', 'col_func') + filename, lineno, source = s.func_source_data() + + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert 'def col_func():' in source + + +def test_is_injectable(): + orca.add_injectable('answer', 42) + assert orca.is_injectable('answer') is True + assert orca.is_injectable('nope') is False + + +def test_injectable_type(): + orca.add_injectable('answer', 42) + + @orca.injectable() + def inj1(): + return 42 + + @orca.injectable(autocall=False, memoize=True) + def power(x): + return 42 ** x + + assert orca.injectable_type('answer') == 'variable' + assert orca.injectable_type('inj1') == 'function' + assert orca.injectable_type('power') == 'function' + + +def test_get_injectable_func_source_data(): + @orca.injectable() + def inj1(): + return 42 + + @orca.injectable(autocall=False, memoize=True) + def power(x): + return 42 ** x + + def inj2(): + return 'orca' + + orca.add_injectable('inj2', inj2, autocall=False) + + filename, lineno, source = orca.get_injectable_func_source_data('inj1') + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert '@orca.injectable()' in source + + filename, lineno, source = orca.get_injectable_func_source_data('power') + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert '@orca.injectable(autocall=False, memoize=True)' in source + + filename, lineno, source = orca.get_injectable_func_source_data('inj2') + assert filename.endswith('test_orca.py') + assert isinstance(lineno, int) + assert 'def inj2()' in source diff --git a/activitysim/core/orca/utils/__init__.py b/activitysim/core/orca/utils/__init__.py new file mode 100644 index 000000000..947c0411b --- /dev/null +++ b/activitysim/core/orca/utils/__init__.py @@ -0,0 +1,5 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +from .utils import * diff --git a/activitysim/core/orca/utils/logutil.py b/activitysim/core/orca/utils/logutil.py new file mode 100644 index 000000000..90755ce02 --- /dev/null +++ b/activitysim/core/orca/utils/logutil.py @@ -0,0 +1,127 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +import contextlib +import logging + +US_LOG_FMT = ('%(asctime)s|%(levelname)s|%(name)s|' + '%(funcName)s|%(filename)s|%(lineno)s|%(message)s') +US_LOG_DATE_FMT = '%Y-%m-%d %H:%M:%S' +US_FMT = logging.Formatter(fmt=US_LOG_FMT, datefmt=US_LOG_DATE_FMT) + + +@contextlib.contextmanager +def log_start_finish(msg, logger, level=logging.DEBUG): + """ + A context manager to log messages with "start: " and "finish: " + prefixes before and after a block. + + Parameters + ---------- + msg : str + Will be prefixed with "start: " and "finish: ". + logger : logging.Logger + level : int, optional + Level at which to log, passed to ``logger.log``. + + """ + logger.log(level, 'start: ' + msg) + yield + logger.log(level, 'finish: ' + msg) + + +def set_log_level(level): + """ + Set the logging level for Orca. + + Parameters + ---------- + level : int + A supporting logging level. Use logging constants like logging.DEBUG. + + """ + logging.getLogger('orca').setLevel(level) + + +def _add_log_handler( + handler, level=None, fmt=None, datefmt=None, propagate=None): + """ + Add a logging handler to Orca. + + Parameters + ---------- + handler : logging.Handler subclass + level : int, optional + An optional logging level that will apply only to this stream + handler. + fmt : str, optional + An optional format string that will be used for the log + messages. + datefmt : str, optional + An optional format string for formatting dates in the log + messages. + propagate : bool, optional + Whether the Orca logger should propagate. If None the + propagation will not be modified, otherwise it will be set + to this value. + + """ + if not fmt: + fmt = US_LOG_FMT + if not datefmt: + datefmt = US_LOG_DATE_FMT + + handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt)) + + if level is not None: + handler.setLevel(level) + + logger = logging.getLogger('orca') + logger.addHandler(handler) + + if propagate is not None: + logger.propagate = propagate + + +def log_to_stream(level=None, fmt=None, datefmt=None): + """ + Send log messages to the console. + + Parameters + ---------- + level : int, optional + An optional logging level that will apply only to this stream + handler. + fmt : str, optional + An optional format string that will be used for the log + messages. + datefmt : str, optional + An optional format string for formatting dates in the log + messages. + + """ + _add_log_handler( + logging.StreamHandler(), fmt=fmt, datefmt=datefmt, propagate=False) + + +def log_to_file(filename, level=None, fmt=None, datefmt=None): + """ + Send log output to the given file. + + Parameters + ---------- + filename : str + level : int, optional + An optional logging level that will apply only to this stream + handler. + fmt : str, optional + An optional format string that will be used for the log + messages. + datefmt : str, optional + An optional format string for formatting dates in the log + messages. + + """ + _add_log_handler( + logging.FileHandler(filename), fmt=fmt, datefmt=datefmt) diff --git a/activitysim/core/orca/utils/testing.py b/activitysim/core/orca/utils/testing.py new file mode 100644 index 000000000..448bed40e --- /dev/null +++ b/activitysim/core/orca/utils/testing.py @@ -0,0 +1,73 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +""" +Utilities used in testing of Orca. + +""" +import numpy as np +import numpy.testing as npt +import pandas as pd + + +def assert_frames_equal(actual, expected, use_close=False): + """ + Compare DataFrame items by index and column and + raise AssertionError if any item is not equal. + + Ordering is unimportant, items are compared only by label. + NaN and infinite values are supported. + + Parameters + ---------- + actual : pandas.DataFrame + expected : pandas.DataFrame + use_close : bool, optional + If True, use numpy.testing.assert_allclose instead of + numpy.testing.assert_equal. + + """ + if use_close: + comp = npt.assert_allclose + else: + comp = npt.assert_equal + + assert (isinstance(actual, pd.DataFrame) and + isinstance(expected, pd.DataFrame)), \ + 'Inputs must both be pandas DataFrames.' + + for i, exp_row in expected.iterrows(): + assert i in actual.index, 'Expected row {!r} not found.'.format(i) + + act_row = actual.loc[i] + + for j, exp_item in exp_row.iteritems(): + assert j in act_row.index, \ + 'Expected column {!r} not found.'.format(j) + + act_item = act_row[j] + + try: + comp(act_item, exp_item) + except AssertionError as e: + raise AssertionError( + str(e) + '\n\nColumn: {!r}\nRow: {!r}'.format(j, i)) + + +def assert_index_equal(left, right): + """ + Similar to pdt.assert_index_equal but is not sensitive to key ordering. + + Parameters + ---------- + left: pandas.Index + right: pandas.Index + """ + assert isinstance(left, pd.Index) + assert isinstance(right, pd.Index) + left_diff = left.difference(right) + right_diff = right.difference(left) + if len(left_diff) > 0 or len(right_diff) > 0: + raise AssertionError("keys not in left [{0}], keys not in right [{1}]".format( + left_diff, right_diff)) diff --git a/activitysim/core/orca/utils/tests/__init__.py b/activitysim/core/orca/utils/tests/__init__.py new file mode 100644 index 000000000..0d4e355c7 --- /dev/null +++ b/activitysim/core/orca/utils/tests/__init__.py @@ -0,0 +1,3 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. diff --git a/activitysim/core/orca/utils/tests/test_testing.py b/activitysim/core/orca/utils/tests/test_testing.py new file mode 100644 index 000000000..bd382f822 --- /dev/null +++ b/activitysim/core/orca/utils/tests/test_testing.py @@ -0,0 +1,87 @@ +# Orca +# Copyright (C) 2016 UrbanSim Inc. +# See full license in LICENSE. + +import pandas as pd +import pytest + +from .. import testing + + +def test_frames_equal_not_frames(): + frame = pd.DataFrame({'a': [1]}) + with pytest.raises(AssertionError) as info: + testing.assert_frames_equal(frame, 1) + + assert 'Inputs must both be pandas DataFrames.' in str(info.value) + + +def test_frames_equal_mismatched_columns(): + expected = pd.DataFrame({'a': [1]}) + actual = pd.DataFrame({'b': [2]}) + + with pytest.raises(AssertionError) as info: + testing.assert_frames_equal(actual, expected) + + assert "Expected column 'a' not found." in str(info.value) + + +def test_frames_equal_mismatched_rows(): + expected = pd.DataFrame({'a': [1]}, index=[0]) + actual = pd.DataFrame({'a': [1]}, index=[1]) + + with pytest.raises(AssertionError) as info: + testing.assert_frames_equal(actual, expected) + + assert "Expected row 0 not found." in str(info.value) + + +def test_frames_equal_mismatched_items(): + expected = pd.DataFrame({'a': [1]}) + actual = pd.DataFrame({'a': [2]}) + + with pytest.raises(AssertionError) as info: + testing.assert_frames_equal(actual, expected) + + assert (""" +Items are not equal: + ACTUAL: 2 + DESIRED: 1 + +Column: 'a' +Row: 0""" in str(info.value)) + + +def test_frames_equal(): + frame = pd.DataFrame({'a': [1]}) + testing.assert_frames_equal(frame, frame) + + +def test_frames_equal_close(): + frame1 = pd.DataFrame({'a': [1]}) + frame2 = pd.DataFrame({'a': [1.00000000000002]}) + + with pytest.raises(AssertionError): + testing.assert_frames_equal(frame1, frame2) + + testing.assert_frames_equal(frame1, frame2, use_close=True) + + +def test_index_equal_order_agnostic(): + left = pd.Index([1, 2, 3]) + right = pd.Index([3, 2, 1]) + testing.assert_index_equal(left, right) + + +def test_index_equal_order_agnostic_raises_left(): + left = pd.Index([1, 2, 3, 4]) + right = pd.Index([3, 2, 1]) + with pytest.raises(AssertionError): + testing.assert_index_equal(left, right) + + +def test_index_equal_order_agnostic_raises_right(): + left = pd.Index([1, 2, 3]) + right = pd.Index([3, 2, 1, 4]) + with pytest.raises(AssertionError): + testing.assert_index_equal(left, right) diff --git a/activitysim/core/orca/utils/tests/test_utils.py b/activitysim/core/orca/utils/tests/test_utils.py new file mode 100644 index 000000000..03f5260f7 --- /dev/null +++ b/activitysim/core/orca/utils/tests/test_utils.py @@ -0,0 +1,9 @@ +from .. import utils + + +def test_func_source_data(): + filename, line, source = utils.func_source_data(test_func_source_data) + + assert filename.endswith('test_utils.py') + assert isinstance(line, int) + assert 'assert isinstance(line, int)' in source diff --git a/activitysim/core/orca/utils/utils.py b/activitysim/core/orca/utils/utils.py new file mode 100644 index 000000000..e00994c5a --- /dev/null +++ b/activitysim/core/orca/utils/utils.py @@ -0,0 +1,27 @@ +import inspect + + +def func_source_data(func): + """ + Return data about a function source, including file name, + line number, and source code. + + Parameters + ---------- + func : object + May be anything support by the inspect module, such as a function, + method, or class. + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + filename = inspect.getsourcefile(func) + lineno = inspect.getsourcelines(func)[1] + source = inspect.getsource(func) + + return filename, lineno, source diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 9664f7899..345bd2fad 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -2,7 +2,7 @@ import datetime as dt import pandas as pd -import orca +from . import orca import logging import inject @@ -181,6 +181,8 @@ def write_df(df, table_name, checkpoint_name=None): store[pipeline_table_key(table_name, checkpoint_name)] = df + store.flush() + def rewrap(table_name, df=None): """ @@ -449,7 +451,7 @@ def run_model(model_name): add_checkpoint(model_name) t0 = print_elapsed_time("add_checkpoint '%s'" % model_name, t0, debug=True) else: - logger.warn("##### skipping %s checkpoint for %s\n" % (step_name, model_name)) + logger.info("##### skipping %s checkpoint for %s" % (step_name, model_name)) def open_pipeline(resume_after=None): @@ -490,6 +492,22 @@ def open_pipeline(resume_after=None): logger.debug("open_pipeline complete") +def last_checkpoint(): + """ + + Returns + ------- + last_checkpoint: str + name of last checkpoint + """ + + #fixme + if not _PIPELINE.is_open: + raise RuntimeError("Pipeline is not open!") + + return _PIPELINE.last_checkpoint[CHECKPOINT_NAME] + + def close_pipeline(): """ Close any known open files @@ -526,16 +544,18 @@ def run(models, resume_after=None): model_name of checkpoint to load checkpoint and AFTER WHICH to resume model run """ - if resume_after: - logger.info('resume_after %s' % resume_after) - if resume_after in models: - models = models[models.index(resume_after) + 1:] - t0 = print_elapsed_time() open_pipeline(resume_after) t0 = print_elapsed_time('open_pipeline', t0) + if resume_after == '_': + resume_after = _PIPELINE.last_checkpoint[CHECKPOINT_NAME] + logger.info("Setting resume_after to %s" % (resume_after, )) + if resume_after in models: + models = models[models.index(resume_after) + 1:] + #bug + # preload any bulky injectables (e.g. skims) not in pipeline if orca.is_injectable('preload_injectables'): orca.get_injectable('preload_injectables') diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 1160b7dd5..8e870d018 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -197,9 +197,7 @@ def to_array(x): return a values = OrderedDict() - print('eval_variables', end='') # print ... for each expression for expr in exprs: - print('.', end='') sys.stdout.flush() # logger.debug("eval_variables: %s" % expr) # logger.debug("eval_variables %s" % util.memory_info()) @@ -216,7 +214,6 @@ def to_array(x): logger.exception("Variable evaluation failed for: %s" % str(expr)) raise err - print() # print ... values = util.df_from_dict(values, index=df.index) diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index df873203e..0d78aa725 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -5,7 +5,6 @@ import tempfile import numpy as np -import orca import pytest import yaml @@ -25,15 +24,15 @@ def test_defaults(): orca.clear_cache() with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("configs_dir") + inject.get_injectable("configs_dir") assert "directory does not exist" in str(excinfo.value) with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("data_dir") + inject.get_injectable("data_dir") assert "directory does not exist" in str(excinfo.value) with pytest.raises(RuntimeError) as excinfo: - output_dir = orca.get_injectable("output_dir") + output_dir = inject.get_injectable("output_dir") print "output_dir", output_dir assert "directory does not exist" in str(excinfo.value) diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 835edaca1..4bd7d3551 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -9,7 +9,7 @@ import pandas.util.testing as pdt import pytest -import orca +from .. import inject from .. import simulate @@ -71,7 +71,7 @@ def test_eval_variables(spec, data): def test_simple_simulate(data, spec): - orca.add_injectable("check_for_variability", False) + inject.add_injectable("check_for_variability", False) choices = simulate.simple_simulate(choosers=data, spec=spec, nest_spec=None) expected = pd.Series([1, 1, 1], index=data.index) @@ -80,7 +80,7 @@ def test_simple_simulate(data, spec): def test_simple_simulate_chunked(data, spec): - orca.add_injectable("check_for_variability", False) + inject.add_injectable("check_for_variability", False) choices = simulate.simple_simulate(choosers=data, spec=spec, nest_spec=None, chunk_size=2) expected = pd.Series([1, 1, 1], index=data.index) diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index 1db224dba..370e46242 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -96,8 +96,6 @@ def tour_map(persons, tours, tdd_alts, persons_id_col='person_id'): tour_type = keys[0] tour_sigil = sigil[tour_type] - print "xxx tour_type", tour_type, tour_sigil - # numpy array with one time window row for each row in nth_tours tour_windows = window_periods_df.loc[nth_tours.tdd].values diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 415cdef43..e8e76157a 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -42,11 +42,15 @@ def extend_trace_label(trace_label, extension): return trace_label +def format_elapsed_time(t): + return "%s seconds (%s minutes)" % (round(t, 3), round(t / 60.0, 1)) + + def print_elapsed_time(msg=None, t0=None, debug=False): t1 = time.time() if msg: t = t1 - (t0 or t1) - msg = "Time to execute %s : %s seconds (%s minutes)" % (msg, round(t, 3), round(t/60.0, 1)) + msg = "Time to execute %s : %s" % (msg, format_elapsed_time(t)) if debug: logger.debug(msg) else: @@ -118,7 +122,7 @@ def config_logger(basic=False): if not basic: log_config_file = config.config_file_path(LOGGING_CONF_FILE_NAME, mandatory=False) - print "log_config_file", log_config_file + #print "log_config_file", log_config_file if log_config_file: with open(log_config_file) as f: @@ -339,7 +343,7 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, file_path = config.trace_file_path(file_name) if os.path.isfile(file_path): - logger.error("write_csv file exists %s %s" % (type(df).__name__, file_name)) + logger.debug("write_csv file exists %s %s" % (type(df).__name__, file_name)) if isinstance(df, pd.DataFrame): # logger.debug("dumping %s dataframe to %s" % (df.shape, file_name)) @@ -569,7 +573,17 @@ def trace_df(df, label, slicer=None, columns=None, Nothing """ - df = slice_canonically(df, slicer, label, warn_if_empty) + #df = slice_canonically(df, slicer, label, warn_if_empty) + + target_ids, column = get_trace_target(df, slicer) + + if target_ids is not None: + df = slice_ids(df, target_ids, column) + + if warn_if_empty and df.shape[0] == 0 and target_ids != []: + column_name = column or slicer + logger.warn("slice_canonically: no rows in %s with %s == %s" + % (label, column_name, target_ids)) if df.shape[0] > 0: write_csv(df, file_name=label, index_label=(index_label or slicer), columns=columns, diff --git a/example/configs/tour_mode_choice.yaml b/example/configs/tour_mode_choice.yaml index 513738f58..66205f67d 100644 --- a/example/configs/tour_mode_choice.yaml +++ b/example/configs/tour_mode_choice.yaml @@ -52,7 +52,7 @@ SPEC: tour_mode_choice.csv COEFFS: tour_mode_choice_coeffs.csv CONSTANTS: - valueOfTime: 8.00 + #valueOfTime: 8.00 costPerMile: 18.48 costShareSr2: 1.75 costShareSr3: 2.50 diff --git a/example/configs/tour_mode_choice_coeffs.csv b/example/configs/tour_mode_choice_coeffs.csv index b37523518..3e12e41db 100644 --- a/example/configs/tour_mode_choice_coeffs.csv +++ b/example/configs/tour_mode_choice_coeffs.csv @@ -9,7 +9,8 @@ c_walktimeshort,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt c_walktimelong,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,10.00 * c_ivt,-0.188 c_biketimeshort,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,4.00 * c_ivt,-0.0752 c_biketimelong,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,20.00 * c_ivt,-0.376 -c_cost,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.6*c_ivt) / valueOfTime +#value_of_time is a person attribute so we cant compute c_cost as scalar here,,,,,,,,,, +#c_cost,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.60 * c_ivt) / valueOfTime,(0.6*c_ivt) / valueOfTime c_short_i_wait,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,-0.0376 c_long_i_wait,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,1.00 * c_ivt,-0.0188 c_wacc,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,2.00 * c_ivt,-0.0376 diff --git a/example/output/.gitignore b/example/output/.gitignore index 4a2323ec0..b987779f4 100644 --- a/example/output/.gitignore +++ b/example/output/.gitignore @@ -3,3 +3,4 @@ *.prof *.h5 *.txt +*.yaml diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index 171b51fa6..71bf056b6 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -14,6 +14,11 @@ logging: loggers: + tasks: + level: !!python/name:logging.DEBUG + handlers: [mp_console, logfile] + propagate: false + activitysim: level: !!python/name:logging.DEBUG handlers: [console, logfile] @@ -37,15 +42,22 @@ logging: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - #level: !!python/name:logging.NOTSET - level: !!python/name:logging.DEBUG + #level: !!python/name:logging.WARN + level: !!python/object/apply:tasks.if_sub_task_opt [INFO, NOTSET] + + mp_console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + level: !!python/name:logging.NOTSET formatters: simpleFormatter: class: !!python/name:logging.Formatter - # format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' - format: !!python/object/apply:tasks.console_logger_format ['%(levelname)s - %(name)s - %(message)s'] + format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' +# format: !!python/object/apply:tasks.if_sub_task ['%(processName)-10s %(levelname)s - %(name)s - %(message)s', +# '%(levelname)s - %(name)s - %(message)s'] datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index e50a7d24b..af0a03539 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -2,7 +2,7 @@ inherit_settings: True #number of households to simulate -households_sample_size: 1000 +households_sample_size: 100 #trace household id; comment out for no trace # household with all tour categories @@ -55,30 +55,38 @@ models: - write_tables #resume_after: school_location_simulate +# +#resume: _workplace_location_sample +#restore_checkpoint: school_location_simulate + # comment out to run single-threaded multiprocess: False chunk_size: 4000000000 +# 160 GB +#chunk_size: 10000000000 + multiprocess_steps: - - label: mp_initialize + - name: mp_initialize begin: initialize_landuse - - label: mp_households + - name: mp_households begin: _school_location_sample num_processes: 3 + stagger: 10 #chunk_size: 1000000000 slice: tables: - households - persons - - label: mp_summarize + - name: mp_summarize begin: write_data_dictionary #multiprocess_steps: -# - label: mp_initialize_landuse +# - name: mp_initialize_landuse # begin: initialize_landuse -# - label: mp_accessibility +# - name: mp_accessibility # begin: compute_accessibility # num_processes: 2 # slice: @@ -86,17 +94,17 @@ multiprocess_steps: # - accessibility # except: # - land_use -# - label: mp_initialize_households +# - name: mp_initialize_households # begin: initialize_households -# - label: mp_households -# begin: school_location_sample +# - name: mp_households +# begin: _school_location_sample # # num_processes = 0 means use all available cpus -# num_processes: 0 +# num_processes: 2 # slice: # tables: # - households # - persons -# - label: mp_summarize +# - name: mp_summarize # begin: write_data_dictionary diff --git a/example_mp/output/.gitignore b/example_mp/output/.gitignore index 4a2323ec0..b987779f4 100644 --- a/example_mp/output/.gitignore +++ b/example_mp/output/.gitignore @@ -3,3 +3,4 @@ *.prof *.h5 *.txt +*.yaml diff --git a/example_mp/simulation.py b/example_mp/simulation.py index bba26461d..fd41db0a4 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -23,6 +23,7 @@ def cleanup_output_files(): tracing.delete_output_files('h5') tracing.delete_output_files('csv') tracing.delete_output_files('txt') + tracing.delete_output_files('yaml') if __name__ == '__main__': @@ -35,6 +36,8 @@ def cleanup_output_files(): config.handle_standard_args() tracing.config_logger() + t0 = tracing.print_elapsed_time() + injectables = ['data_dir', 'configs_dir', 'output_dir'] injectables = {k: inject.get_injectable(k) for k in injectables} @@ -46,15 +49,10 @@ def cleanup_output_files(): with open(config.output_file_path('run_list.txt'), 'w') as file: tasks.print_run_list(run_list, file) - t0 = tracing.print_elapsed_time() + # tasks.print_run_list(run_list) if run_list['multiprocess']: logger.info("run multiprocess simulation") - logger.info("main process pid : %s" % os.getpid()) - logger.info("sys.executable : %s" % sys.executable) - logger.info("cpu count : %s" % multiprocessing.cpu_count()) - - tasks.print_run_list(run_list, sys.stdout) tasks.run_multiprocess(run_list, injectables) @@ -67,4 +65,4 @@ def cleanup_output_files(): # tables will no longer be available after pipeline is closed pipeline.close_pipeline() - t0 = tracing.print_elapsed_time("mp_simulation", t0) + t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 9fffe7e01..1320ac643 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -1,8 +1,14 @@ + +from __future__ import print_function + +import sys import os import time import logging +import yaml from collections import OrderedDict +from collections import Iterable import numpy as np import pandas as pd @@ -105,7 +111,7 @@ def build_slice_rules(slice_info, tables): slice_rules[table_name] = rule - print "## rule %s: %s" % (table_name, rule) + #print("## rule %s: %s" % (table_name, rule)) for table_name in slice_rules: logger.debug("%s: %s" % (table_name, slice_rules[table_name])) @@ -225,7 +231,7 @@ def coalesce_pipelines(sub_process_names, slice_info): checkpoint_name, hdf5_keys = pipeline_table_keys(pipeline_store) for table_name, hdf5_key in hdf5_keys.iteritems(): - print "loading table", table_name, hdf5_key + logger.debug("loading table %s %s" % (table_name, hdf5_key)) tables[table_name] = pipeline_store[hdf5_key] # use slice rules followed by apportion_pipeline to identify singleton tables @@ -295,15 +301,18 @@ def setup_injectables_and_logging(injectables): for k, v in injectables.iteritems(): inject.add_injectable(k, v) + inject.add_injectable("is_sub_task", True) + process_name = mp.current_process().name inject.add_injectable("log_file_prefix", process_name) tracing.config_logger() -def run_mp_simulation(injectables, step_info, resume_after, pipeline_prefix, **kwargs): +def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): skim_buffer = kwargs + step_label = step_info['name'] models = step_info['models'] num_processes = step_info['num_processes'] chunk_size = step_info['chunk_size'] @@ -312,21 +321,59 @@ def run_mp_simulation(injectables, step_info, resume_after, pipeline_prefix, **k # do this before config_logger so log file is named appropriately process_name = mp.current_process().name - if pipeline_prefix: - pipeline_prefix = process_name if pipeline_prefix is True else pipeline_prefix + if num_processes > 1: + pipeline_prefix = process_name logger.info("injecting pipeline_file_prefix '%s'" % pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) setup_injectables_and_logging(injectables) - logger.info("run_mp_simulation %s num_processes %s" % (process_name, num_processes)) + logger.info("mp_run_simulation %s num_processes %s" % (process_name, num_processes)) + if resume_after: + logger.info('resume_after %s' % resume_after) inject.add_injectable('skim_buffer', skim_buffer) inject.add_injectable("chunk_size", chunk_size) - pipeline.run(models=models, resume_after=resume_after) + if resume_after and resume_after != '_': + # if they specified a resume_after model, check to make sure it is checkpointed + if resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: + # if not checkpointed, then fall back to last checkpoint + logger.warn("resume_after checkpoint '%s' not in pipeline" % (resume_after, )) + resume_after = '_' + + pipeline.open_pipeline(resume_after) + last_checkpoint = pipeline.last_checkpoint() + + if last_checkpoint in models: + logger.info("Resuming model run list after %s" % (last_checkpoint, )) + models = models[models.index(last_checkpoint) + 1:] + + # preload any bulky injectables (e.g. skims) not in pipeline + t0 = tracing.print_elapsed_time() + preload_injectables = inject.get_injectable('preload_injectables', None) + if preload_injectables is not None: + t0 = tracing.print_elapsed_time('preload_injectables', t0) + + t0 = tracing.print_elapsed_time() + for model in models: + + t1 = tracing.print_elapsed_time() + pipeline.run_model(model) + + queue.put({'model': model, 'time': time.time()-t1}) + + t1 = tracing.print_elapsed_time("run_model %s %s" % (step_label, model,), t1) + + #logger.debug('#mem after %s, %s' % (model, util.memory_info())) + + t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) + + ########################### + pipeline.close_pipeline() + #fixme # try: # run_simulation(models, resume_after) # except Exception as e: @@ -351,7 +398,7 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): coalesce_pipelines(sub_job_proc_names, slice_info) -def run_sub_process(p): +def run_sub_task(p): logger.info("running sub_process %s" % p.name) p.start() p.join() @@ -362,90 +409,198 @@ def run_sub_process(p): raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) -def run_sub_procs(procs): +def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_names, resume_after): + + step_name = step_info['name'] + + logger.info('run_sub_simulations step %s models resume_after %s' % (step_name, resume_after)) + + # if not the first step, resume_after the last checkpoint from the previous step + if resume_after is None and step_info['step_num'] > 0: + resume_after = '_' + + num_simulations = len(process_names) + last_checkpoints = [None] * num_simulations + procs = [] + queues = [] + + for process_name in process_names: + q = mp.Queue() + p = mp.Process(target=mp_run_simulation, name=process_name, + args=(q, injectables, step_info, resume_after), + kwargs=shared_skim_buffer) + procs.append(p) + queues.append(q) + + def handle_queued_messages(): + for i, p, q in zip(range(num_simulations), procs, queues): + while not q.empty(): + msg = q.get(block=False) + model = msg['model'] + t = msg['time'] + logger.info("%s %s : %s" % (p.name, model, tracing.format_elapsed_time(t))) + if model[0] != '_': + last_checkpoints[i] = model + #update_journal(step_name, 'checkpoints', last_checkpoints) + + stagger = 0 for p in procs: + if stagger > 0: + logger.info("stagger process %s by %s seconds" % (p.name, step_info['stagger'])) + for i in range(stagger): + handle_queued_messages() + time.sleep(1) + stagger = step_info['stagger'] logger.info("start process %s" % p.name) p.start() while mp.active_children(): - logger.info("%s active processes" % len(mp.active_children())) - time.sleep(15) + handle_queued_messages() + time.sleep(1) + handle_queued_messages() for p in procs: p.join() - error_procs = [p for p in procs if p.exitcode] + # log exitcode of sub_simulations that failed + error_count = 0 + for p in procs: + if p.exitcode: + logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) + error_count += 1 + + return error_count + + +def update_journal(step_name, key, value): + + run_status = inject.get_injectable('run_status', OrderedDict()) + if not run_status: + inject.add_injectable('run_status', run_status) - return error_procs + run_status.setdefault(step_name, {'name': step_name})[key] = value + save_journal(run_status) def run_multiprocess(run_list, injectables): + #resume_after = run_list['resume_after'] + resume_journal = run_list.get('resume_journal', {}) + logger.info('setup shared skim data') shared_skim_buffer = allocate_shared_skim_buffer() - run_sub_process( + def skip(step_name, key): + + already_did_this = resume_journal and resume_journal.get(step_name, {}).get(key, False) + + if already_did_this: + logger.info("Skipping %s %s" % (step_name, key)) + time.sleep(1) + + return already_did_this + + # - mp_setup_skims + run_sub_task( mp.Process(target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), kwargs=shared_skim_buffer) ) - resume_after = None - for step_info in run_list['multiprocess_steps']: - label = step_info['label'] + step_name = step_info['name'] + + num_processes = step_info['num_processes'] slice_info = step_info.get('slice', None) - if not slice_info: + if num_processes == 1: + sub_proc_names = [step_name] + else: + sub_proc_names = ["%s_%s" % (step_name, i) for i in range(num_processes)] - assert step_info['num_processes'] == 1 + update_journal(step_name, 'sub_proc_names', sub_proc_names) - logger.info('running step %s single process' % (label,)) + logger.info('running step %s with %s processes' % (step_name, num_processes,)) - # unsliced steps run single-threaded - sub_proc_name = label + # - mp_apportion_pipeline + if not skip(step_name, 'apportion'): + if num_processes > 1: + logger.info('apportioning households to sub_processes') + run_sub_task( + mp.Process(target=mp_apportion_pipeline, name='%s_apportion' % step_name, + args=(injectables, sub_proc_names, slice_info)) + ) + update_journal(step_name, 'apportion', True) - run_sub_process( - mp.Process(target=run_mp_simulation, name=sub_proc_name, - args=(injectables, step_info, resume_after, False), - kwargs=shared_skim_buffer) - ) + # - run_sub_simulations + if not skip(step_name, 'simulate'): + error_count = \ + run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, + resume_after=step_info.get('resume_after', None)) + if error_count: + raise RuntimeError("%s processes failed in step %s" % (error_count, step_name)) - else: + update_journal(step_name, 'simulate', True) - num_processes = step_info['num_processes'] + # - mp_coalesce_pipelines + if not skip(step_name, 'coalesce'): + if num_processes > 1: + logger.info('coalescing sub_process pipelines') + run_sub_task( + mp.Process(target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, + args=(injectables, sub_proc_names, slice_info)) + ) + update_journal(step_name, 'coalesce', True) - logger.info('running step %s multiprocess with %s processes' % (label, num_processes,)) - sub_proc_names = ["%s_sub-%s" % (label, i) for i in range(num_processes)] +def get_resume_journal(run_list): - logger.info('apportioning households to sub_processes') - run_sub_process( - mp.Process(target=mp_apportion_pipeline, name='%s_apportion' % label, - args=(injectables, sub_proc_names, slice_info)) - ) + resume_after = run_list['resume_after'] + assert resume_after is not None - logger.info('starting sub_processes') - error_procs = run_sub_procs([ - mp.Process(target=run_mp_simulation, name=process_name, - args=(injectables, step_info, resume_after, True), - kwargs=shared_skim_buffer) - for process_name in sub_proc_names - ]) + previous_journal = read_journal() - if error_procs: - for p in error_procs: - logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) - raise RuntimeError("%s processes failed in %s" % (len(error_procs), label)) + if not previous_journal: + logger.error("empty journal for resume_after '%s'" % (resume_after,)) + raise RuntimeError("empty journal for resume_after '%s'" % (resume_after,)) - logger.info('coalescing sub_process pipelines') - run_sub_process( - mp.Process(target=mp_coalesce_pipelines, name='%s_coalesce' % label, - args=(injectables, sub_proc_names, slice_info)) - ) + if resume_after == '_': + resume_step_name = previous_journal.keys()[-1] + else: - resume_after = '_' + previous_steps = previous_journal.keys() + + # run_list step resume_after is in + resume_step_name = next((step['name'] for step in run_list['multiprocess_steps'] + if resume_after in step['models']), None) + + if resume_step_name not in previous_steps: + logger.error("resume_after model '%s' not in journal" % (resume_after,)) + raise RuntimeError("resume_after model '%s' not in journal" % (resume_after,)) + + # drop any previous_journal steps after resume_step + for step in previous_steps[previous_steps.index(resume_step_name) + 1:]: + del previous_journal[step] + + multiprocess_step = next((step for step in run_list['multiprocess_steps'] if step['name']==resume_step_name), []) + print("resume_step_models", multiprocess_step['models']) + if resume_after in multiprocess_step['models'][:-1]: + + # if resume_after is specified by name, and is not the last model in the step + # then we need to rerun the simulations, even if they succeeded + + if previous_journal[resume_step_name].get('simulate', None): + previous_journal[resume_step_name]['simulate'] = None + + if previous_journal[resume_step_name].get('coalesce', None): + previous_journal[resume_step_name]['coalesce'] = None + + multiprocess_step_names = [step['name'] for step in run_list['multiprocess_steps']] + if previous_journal.keys() != multiprocess_step_names[:len(previous_journal)]: + raise RuntimeError("last run steps don't match run list: %s" % previous_journal.keys()) + + return previous_journal def get_run_list(): @@ -472,46 +627,47 @@ def get_run_list(): if resume_after not in models + ['_', None]: raise RuntimeError("resume_after '%s' not in models list" % resume_after) + if resume_after == models[-1]: + raise RuntimeError("resume_after '%s' is last model in models list" % resume_after) if multiprocess: - if resume_after: - raise RuntimeError("resume_after not implemented for multiprocessing") - if not multiprocess_steps: raise RuntimeError("multiprocess setting is %s but no multiprocess_steps setting" % multiprocess) - # check label, num_processes, chunk_size and presence of slice info - labels = set() + # check step name, num_processes, chunk_size and presence of slice info + step_names = set() for istep in range(len(multiprocess_steps)): step = multiprocess_steps[istep] - # - validate label - label = step.get('label', None) - if not label: - raise RuntimeError("missing label for step %s" + step['step_num'] = istep + + # - validate step name + name = step.get('name', None) + if not name: + raise RuntimeError("missing name for step %s" " in multiprocess_steps" % istep) - if label in labels: - raise RuntimeError("duplicate step label %s" - " in multiprocess_steps" % label) - labels.add(label) + if name in step_names: + raise RuntimeError("duplicate step name %s" + " in multiprocess_steps" % name) + step_names.add(name) # - validate num_processes and assign default num_processes = step.get('num_processes', 0) if not isinstance(num_processes, int) or num_processes < 0: raise RuntimeError("bad value (%s) for num_processes for step %s" - " in multiprocess_steps" % (num_processes, label)) + " in multiprocess_steps" % (num_processes, name)) if 'slice' in step: if num_processes == 0: logger.info("Setting num_processes = %s for step %s" % - (num_processes, label)) + (num_processes, name)) num_processes = mp.cpu_count() if num_processes == 1: raise RuntimeError("num_processes = 1 but found slice info for step %s" - " in multiprocess_steps" % label) + " in multiprocess_steps" % name) if num_processes > mp.cpu_count(): logger.warn("num_processes setting (%s) greater than cpu count (%s" % (num_processes, mp.cpu_count())) @@ -520,7 +676,7 @@ def get_run_list(): num_processes = 1 if num_processes > 1: raise RuntimeError("num_processes > 1 but no slice info for step %s" - " in multiprocess_steps" % label) + " in multiprocess_steps" % name) multiprocess_steps[istep]['num_processes'] = num_processes @@ -535,13 +691,16 @@ def get_run_list(): multiprocess_steps[istep]['chunk_size'] = chunk_size + # - validate stagger and assign default + multiprocess_steps[istep]['stagger'] = max(int(step.get('stagger', 0)), 0) + # - determine index in models list of step starts START = 'begin' starts = [0] * len(multiprocess_steps) for istep in range(len(multiprocess_steps)): step = multiprocess_steps[istep] - label = step['label'] + name = step['name'] slice = step.get('slice', None) if slice: @@ -550,65 +709,139 @@ def get_run_list(): " in multiprocess_steps" % istep) start = step.get(START, None) - if not label: + if not name: raise RuntimeError("missing %s tag for step '%s' (%s)" " in multiprocess_steps" % - (START, label, istep)) + (START, name, istep)) if start not in models: raise RuntimeError("%s tag '%s' for step '%s' (%s) not in models list" % - (START, start, label, istep)) + (START, start, name, istep)) starts[istep] = models.index(start) if istep == 0 and starts[istep] != 0: - raise RuntimeError("%s tag '%s' for first is not first model in models list" % - (START, start, label, istep)) + raise RuntimeError("%s tag '%s' for first step '%s' (%s)" + " is not first model in models list" % + (START, start, name, istep)) if istep > 0 and starts[istep] <= starts[istep - 1]: raise RuntimeError("%s tag '%s' for step '%s' (%s)" " falls before that of prior step in models list" % - (START, start, label, istep)) + (START, start, name, istep)) # - build step model lists starts.append(len(models)) # so last step gets remaining models in list for istep in range(len(multiprocess_steps)): step_models = models[starts[istep]: starts[istep + 1]] - # suppress_intermediate_checkpoints until we support resume_after - suppress_intermediate_checkpoints = True - if suppress_intermediate_checkpoints: - step_models = ['_' + m if m[0] != '_' and m != step_models[-1] else m - for m in step_models] + if step_models[-1][0] == '_': + raise RuntimeError("Final model '%s' in step %s models list not checkpointed" % + (step_models[-1], name)) multiprocess_steps[istep]['models'] = step_models run_list['multiprocess_steps'] = multiprocess_steps + if resume_after: + resume_journal = get_resume_journal(run_list) + if resume_journal: + run_list['resume_journal'] = resume_journal + # - add resume_after to resume_step + istep = len(resume_journal) - 1 + multiprocess_steps[istep]['resume_after'] = resume_after + + return run_list -def print_run_list(run_list, file): +def print_run_list(run_list, file=None): - print >> file, "resume_after:", run_list['resume_after'] - print >> file, "multiprocess:", run_list['multiprocess'] + if file is None: + file = sys.stdout + + print("resume_after:", run_list['resume_after'], file=file) + print("multiprocess:", run_list['multiprocess'], file=file) + + print("models", file=file) + for m in run_list['models']: + print(" - ", m, file=file) if run_list['multiprocess']: + print("\nmultiprocess_steps:", file=file) for step in run_list['multiprocess_steps']: - print >> file, "step:", step['label'] - print >> file, " num_processes:", step['num_processes'] - print >> file, " chunk_size:", step['chunk_size'] - print >> file, " models" - for m in step['models']: - print >> file, " - ", m + print(" step:", step['name'], file=file) + for k in step: + if isinstance(step[k], (list, )): + print(" ", k, file=file) + for v in step[k]: + print(" -", v, file=file) + else: + print(" %s: %s" % (k, step[k]), file=file) + + if run_list.get('resume_journal'): + print("\nresume_journal:", file=file) + print_journal(run_list['resume_journal'], file) + else: - print >> file, "models" + print("models", file=file) for m in run_list['models']: - print >> file, " - ", m + print(" - ", m, file=file) + + +def print_journal(journal, file=None): + + if file is None: + file = sys.stdout + + for step_name in journal: + step = journal[step_name] + print(" step:", step_name, file=file) + for k in step: + if isinstance(k, str): + print(" ", k, step[k], file=file) + else: + print(" ", k, file=file) + for v in step[k]: + print(" ", v, file=file) + + +def journal_file_path(file_name=None): + return config.build_output_file_path(file_name or 'journal.yaml') + +def read_journal(file_name=None): + file_path = journal_file_path(file_name) + if not os.path.exists(file_path): + raise IOError("Could not find saved journal file '%s'" % file_path) + with open(file_path, 'r') as f: + journal = yaml.load(f) -def console_logger_format(format): + journal = OrderedDict([(step['name'], step) for step in journal]) + return journal - if inject.get_injectable('log_file_prefix', None): - format = "%(processName)-10s " + format - return format +def save_journal(journal, file_name=None): + with open(journal_file_path(file_name), 'w') as f: + journal = [step for step in journal.values()] + yaml.dump(journal, f) + +def is_sub_task(): + + return inject.get_injectable('is_sub_task', False) + + +def if_sub_task(if_is, if_isnt): + + return if_is if is_sub_task() else if_isnt + +def if_sub_task_opt(if_is, if_isnt): + + opt = { + 'ERROR': logging.ERROR, + 'WARN': logging.WARN, + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + 'NOTSET': logging.NOTSET, + } + return opt[if_is] if is_sub_task() else opt[if_isnt] + diff --git a/example_multi/simulation.py b/example_multi/simulation.py index 37a326780..2e5f6a578 100644 --- a/example_multi/simulation.py +++ b/example_multi/simulation.py @@ -41,7 +41,7 @@ def data_dir(): @inject.injectable(override=True) def preload_injectables(): # don't want to load standard skims - pass + return False def print_elapsed_time(msg=None, t0=None): diff --git a/setup.py b/setup.py index 38322014d..d90ac1922 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ install_requires=[ 'numpy >= 1.13.0', 'openmatrix >= 0.2.4', - 'orca >= 1.1', + #'orca >= 1.1', 'pandas >= 0.20.3', 'pyyaml >= 3.0', 'tables >= 3.3.0', From ee77f558124657981e44b9bd634d1e152f798b35 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Sat, 13 Oct 2018 00:00:40 -0400 Subject: [PATCH 021/122] passing tests --- .pylintrc | 16 ++ .travis.yml | 2 +- .../test/test_vectorize_tour_scheduling.py | 9 +- activitysim/abm/test/test_misc.py | 28 +- activitysim/abm/test/test_pipeline.py | 49 ++-- activitysim/core/inject.py | 21 ++ activitysim/core/orca/orca.py | 3 +- activitysim/core/pipeline.py | 10 +- activitysim/core/test/test_assign.py | 6 +- activitysim/core/test/test_inject_defaults.py | 12 +- activitysim/core/test/test_logit.py | 6 +- activitysim/core/test/test_pipeline.py | 37 ++- activitysim/core/test/test_tracing.py | 28 +- activitysim/core/tracing.py | 35 --- example/simulation.py | 20 +- example_mp/simulation.py | 13 +- example_mp/tasks.py | 269 ++++++++---------- setup.py | 6 +- 18 files changed, 269 insertions(+), 301 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..cef093dda --- /dev/null +++ b/.pylintrc @@ -0,0 +1,16 @@ +[MESSAGES CONTROL] +disable=locally-disabled, + # False positive for type annotations with typing module + # invalid-sequence-index, + # False positive for OK test methods names and few other places + invalid-name, + # False positive for test file classes and methods + missing-docstring + +[REPORTS] +# Simplify pylint reports +reports=no + +[SIMILARITIES] +min-similarity-lines=10 +ignore-docstrings=yes diff --git a/.travis.yml b/.travis.yml index 415d9a5c7..114187ad2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ install: - | conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION cytoolz numpy pandas pip pytables pyyaml toolz psutil - source activate test-environment -- pip install orca openmatrix zbox +- pip install openmatrix zbox future - pip install pytest pytest-cov coveralls pycodestyle - pip install sphinx numpydoc sphinx_rtd_theme - pip install . diff --git a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py index 18c051413..f3405f699 100644 --- a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py @@ -5,7 +5,6 @@ import pytest import pandas as pd import numpy as np -import orca import pandas.util.testing as pdt @@ -17,7 +16,7 @@ def test_vts(): - orca.add_injectable("settings", {}) + inject.add_injectable("settings", {}) # note: need 0 duration tour on one end of day to guarantee at least one available tour alts = pd.DataFrame({ @@ -25,7 +24,7 @@ def test_vts(): "end": [1, 4, 5, 6] }) alts['duration'] = alts.end - alts.start - orca.add_injectable("tdd_alts", alts) + inject.add_injectable("tdd_alts", alts) current_tour_person_ids = pd.Series(['b', 'c'], index=['d', 'e']) @@ -54,13 +53,13 @@ def test_vts(): persons = pd.DataFrame({ "income": [20, 30, 25] }, index=[1, 2, 3]) - orca.add_table('persons', persons) + inject.add_table('persons', persons) spec = pd.DataFrame({"Coefficient": [1.2]}, index=["income"]) spec.index.name = "Expression" - orca.add_injectable("check_for_variability", True) + inject.add_injectable("check_for_variability", True) tdd_choices = vectorize_tour_scheduling(tours, persons, alts, spec) diff --git a/activitysim/abm/test/test_misc.py b/activitysim/abm/test/test_misc.py index 573b3aa49..380b43a3b 100644 --- a/activitysim/abm/test/test_misc.py +++ b/activitysim/abm/test/test_misc.py @@ -5,46 +5,40 @@ import tempfile import numpy as np -import orca import pytest import yaml -# orca injectables complicate matters because the decorators are executed at module load time -# and since py.test collects modules and loads them at the start of a run -# if a test method does something that has a lasting side-effect, then that side effect -# will carry over not just to subsequent test functions, but to subsequently called modules -# for instance, columns added with add_column will remain attached to orca tables -# pytest-xdist allows us to run py.test with the --boxed option which runs every function -# with a brand new python interpreter +from activitysim.core import inject -# Also note that the following import statement has the side-effect of registering injectables: + +# The following import statement has the side-effect of registering injectables: from .. import __init__ def test_misc(): - orca.clear_cache() + inject.clear_cache() with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("configs_dir") + inject.get_injectable("configs_dir") assert "directory does not exist" in str(excinfo.value) with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("data_dir") + inject.get_injectable("data_dir") assert "directory does not exist" in str(excinfo.value) with pytest.raises(RuntimeError) as excinfo: - orca.get_injectable("output_dir") + inject.get_injectable("output_dir") assert "directory does not exist" in str(excinfo.value) configs_dir = os.path.join(os.path.dirname(__file__), 'configs_test_misc') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) - settings = orca.get_injectable("settings") + settings = inject.get_injectable("settings") assert isinstance(settings, dict) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) # default values if not specified in settings - assert orca.get_injectable("chunk_size") == 0 + assert inject.get_injectable("chunk_size") == 0 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 64ecfe265..2da6c204b 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -6,7 +6,6 @@ import logging import numpy as np -import orca import pandas as pd import pandas.util.testing as pdt import pytest @@ -35,7 +34,7 @@ def teardown_function(func): - orca.clear_cache() + inject.clear_cache() inject.reinject_decorated_tables() @@ -64,7 +63,7 @@ def inject_settings(configs_dir, households_sample_size, chunk_size=None, if check_for_variability is not None: settings['check_for_variability'] = check_for_variability - orca.add_injectable("settings", settings) + inject.add_injectable("settings", settings) return settings @@ -72,17 +71,17 @@ def inject_settings(configs_dir, households_sample_size, chunk_size=None, def test_rng_access(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - orca.clear_cache() + inject.clear_cache() inject.add_injectable('rng_base_seed', 0) @@ -91,7 +90,7 @@ def test_rng_access(): rng = pipeline.get_rn_generator() pipeline.close_pipeline() - orca.clear_cache() + inject.clear_cache() def regress_mini_auto(): @@ -149,22 +148,20 @@ def regress_mini_mtf(): def test_mini_pipeline_run(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - orca.clear_cache() + inject.clear_cache() tracing.config_logger() - # assert len(orca.get_table("households").index) == HOUSEHOLDS_SAMPLE_SIZE - _MODELS = [ 'initialize_landuse', 'compute_accessibility', @@ -198,7 +195,7 @@ def test_mini_pipeline_run(): assert "not in checkpoints" in str(excinfo.value) pipeline.close_pipeline() - orca.clear_cache() + inject.clear_cache() close_handlers() @@ -210,17 +207,17 @@ def test_mini_pipeline_run2(): # when we restart pipeline configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - orca.clear_cache() + inject.clear_cache() # should be able to get this BEFORE pipeline is opened checkpoints_df = pipeline.get_checkpoints() @@ -249,7 +246,7 @@ def test_mini_pipeline_run2(): assert len(checkpoints_df.index) == prev_checkpoint_count pipeline.close_pipeline() - orca.clear_cache() + inject.clear_cache() def full_run(resume_after=None, chunk_size=0, @@ -257,13 +254,13 @@ def full_run(resume_after=None, chunk_size=0, trace_hh_id=None, trace_od=None, check_for_variability=None): configs_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'example', 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) settings = inject_settings( configs_dir, @@ -273,7 +270,7 @@ def full_run(resume_after=None, chunk_size=0, trace_od=trace_od, check_for_variability=check_for_variability) - orca.clear_cache() + inject.clear_cache() tracing.config_logger() @@ -284,10 +281,6 @@ def full_run(resume_after=None, chunk_size=0, tours = pipeline.get_table('tours') tour_count = len(tours.index) - # pipeline.close_pipeline() - # - # orca.clear_cache() - return tour_count diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index 3cfcbf56b..da31db36e 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -1,3 +1,6 @@ +# ActivitySim +# See full license in LICENSE.txt. + import logging import pandas as pd @@ -84,6 +87,7 @@ def add_table(table_name, table, cache=False): return orca.add_table(table_name, table, cache=cache) +#fixme remove? def add_column(table_name, column_name, column, cache=False): return orca.add_column(table_name, column_name, column, cache=cache) @@ -114,6 +118,13 @@ def get_injectable(name, default=_NO_DEFAULT): return default +def remove_injectable(name): + + #fixme + #del orca.orca._INJECTABLES[name] + orca.orca._INJECTABLES.pop(name, None) + + def reinject_decorated_tables(): """ reinject the decorated tables (and columns) @@ -141,6 +152,10 @@ def reinject_decorated_tables(): orca.add_injectable(name, args['func'], cache=args['cache']) +def clear_cache(): + return orca.clear_cache() + + def set_step_args(args=None): assert isinstance(args, dict) or args is None @@ -156,3 +171,9 @@ def get_step_arg(arg_name, default=_NO_DEFAULT): raise "step arg '%s' not found and no default" % arg_name return args.get(arg_name, default) + + +def dump_state(): + + print "_DECORATED_STEPS", _DECORATED_STEPS.keys() + print "orca._STEPS", orca.orca._STEPS.keys() diff --git a/activitysim/core/orca/orca.py b/activitysim/core/orca/orca.py index f5dc0c37b..20514c9a6 100644 --- a/activitysim/core/orca/orca.py +++ b/activitysim/core/orca/orca.py @@ -24,7 +24,7 @@ from collections import namedtuple warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) -logger = logging.getLogger('orca') +logger = logging.getLogger(__name__) _TABLES = {} _COLUMNS = {} @@ -1978,7 +1978,6 @@ def run(steps, iter_vars=None, data_out=None, out_interval=1, 'running iteration {} with iteration value {!r}'.format( i, var)) - t1 = time.time() for j, step_name in enumerate(steps): add_injectable('iter_step', iter_step(j, step_name)) with log_start_finish( diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 345bd2fad..abaf435c9 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,3 +1,6 @@ +# ActivitySim +# See full license in LICENSE.txt. + import os import datetime as dt @@ -224,6 +227,7 @@ def rewrap(table_name, df=None): for column_name in orca.list_columns_for_table(table_name): # logger.debug("pop %s.%s: %s" % (table_name, column_name, t.column_type(column_name))) + #fixme orca.orca._COLUMNS.pop((table_name, column_name), None) # remove from orca's table list @@ -501,7 +505,6 @@ def last_checkpoint(): name of last checkpoint """ - #fixme if not _PIPELINE.is_open: raise RuntimeError("Pipeline is not open!") @@ -551,10 +554,11 @@ def run(models, resume_after=None): if resume_after == '_': resume_after = _PIPELINE.last_checkpoint[CHECKPOINT_NAME] - logger.info("Setting resume_after to %s" % (resume_after, )) + + if resume_after: + logger.info('resume_after %s' % resume_after) if resume_after in models: models = models[models.index(resume_after) + 1:] - #bug # preload any bulky injectables (e.g. skims) not in pipeline if orca.is_injectable('preload_injectables'): diff --git a/activitysim/core/test/test_assign.py b/activitysim/core/test/test_assign.py index 27946f32b..f073bf236 100644 --- a/activitysim/core/test/test_assign.py +++ b/activitysim/core/test/test_assign.py @@ -12,8 +12,6 @@ import pandas.util.testing as pdt import pytest -import orca - from .. import assign from .. import tracing from .. import inject @@ -30,7 +28,7 @@ def close_handlers(): def teardown_function(func): - orca.clear_cache() + inject.clear_cache() inject.reinject_decorated_tables() @@ -159,7 +157,7 @@ def test_assign_variables_failing(capsys, data): close_handlers() output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) tracing.config_logger(basic=True) diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index 0d78aa725..c8bd73083 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -15,13 +15,13 @@ def teardown_function(func): - orca.clear_cache() + inject.clear_cache() inject.reinject_decorated_tables() def test_defaults(): - orca.clear_cache() + inject.clear_cache() with pytest.raises(RuntimeError) as excinfo: inject.get_injectable("configs_dir") @@ -37,13 +37,11 @@ def test_defaults(): assert "directory does not exist" in str(excinfo.value) configs_dir = os.path.join(os.path.dirname(__file__), 'configs_test_defaults') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) - settings = orca.get_injectable("settings") + settings = inject.get_injectable("settings") assert isinstance(settings, dict) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) - # default values if not specified in settings - assert orca.get_injectable("chunk_size") == 0 diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index c9a5af3e7..c23d24acb 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -5,13 +5,13 @@ import numpy as np import pandas as pd -import orca import pandas.util.testing as pdt import pytest from ..simulate import eval_variables from .. import logit +from .. import inject @pytest.fixture(scope='module') @@ -22,10 +22,10 @@ def data_dir(): def add_canonical_dirs(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) # this is lifted straight from urbansim's test_mnl.py diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index 61670811b..a0274e38b 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -5,19 +5,16 @@ import tempfile import logging -import numpy as np -import orca -import pandas as pd import pandas.util.testing as pdt import pytest -import yaml - -import extensions from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import inject +#from . import extensions +from .extensions import steps + # set the max households for all tests (this is to limit memory use on travis) HOUSEHOLDS_SAMPLE_SIZE = 100 HH_ID = 961042 @@ -25,25 +22,25 @@ def setup(): - orca.orca._INJECTABLES.pop('skim_dict', None) - orca.orca._INJECTABLES.pop('skim_stack', None) + inject.remove_injectable('skim_dict') + inject.remove_injectable('skim_stack') configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) data_dir = os.path.join(os.path.dirname(__file__), 'data') - orca.add_injectable("data_dir", data_dir) + inject.add_injectable("data_dir", data_dir) - orca.clear_cache() + inject.clear_cache() tracing.config_logger() def teardown_function(func): - orca.clear_cache() + inject.clear_cache() inject.reinject_decorated_tables() @@ -61,6 +58,13 @@ def test_pipeline_run(): setup() + #fixme + inject.add_step('step1', steps.step1) + inject.add_step('step2', steps.step2) + inject.add_step('step3', steps.step3) + inject.add_step('step_add_col', steps.step_add_col) + inject.dump_state() + _MODELS = [ 'step1', 'step2', @@ -102,6 +106,13 @@ def test_pipeline_checkpoint_drop(): setup() + #fixme + inject.add_step('step1', steps.step1) + inject.add_step('step2', steps.step2) + inject.add_step('step3', steps.step3) + inject.add_step('step_add_col', steps.step_add_col) + inject.add_step('step_forget_tab', steps.step_forget_tab) + _MODELS = [ 'step1', '_step2', diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index 2fffca60a..285df8862 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -6,10 +6,10 @@ import pytest -import orca import pandas as pd -from .. import tracing as tracing +from .. import tracing +from .. import inject def close_handlers(): @@ -24,13 +24,13 @@ def close_handlers(): def add_canonical_dirs(): - orca.clear_cache() + inject.clear_cache() configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) def test_config_logger(capsys): @@ -98,8 +98,8 @@ def test_register_households(capsys): df = pd.DataFrame({'zort': ['a', 'b', 'c']}, index=[1, 2, 3]) - orca.add_injectable('traceable_tables', ['households']) - orca.add_injectable("trace_hh_id", 5) + inject.add_injectable('traceable_tables', ['households']) + inject.add_injectable("trace_hh_id", 5) tracing.register_traceable_table('households', df) out, err = capsys.readouterr() @@ -124,11 +124,11 @@ def test_register_tours(capsys): tracing.config_logger() - orca.add_injectable('traceable_tables', ['households', 'tours']) - orca.add_injectable('traceable_table_refs', None) + inject.add_injectable('traceable_tables', ['households', 'tours']) + inject.add_injectable('traceable_table_refs', None) # in case another test injected this - orca.add_injectable("trace_tours", []) + inject.add_injectable("trace_tours", []) tours_df = pd.DataFrame({'zort': ['a', 'b', 'c']}, index=[10, 11, 12]) tours_df.index.name = 'tour_id' @@ -140,7 +140,7 @@ def test_register_tours(capsys): assert "can't find a registered table to slice table 'tours' index name 'tour_id'" in out - orca.add_injectable("trace_hh_id", 3) + inject.add_injectable("trace_hh_id", 3) households_df = pd.DataFrame({'dzing': ['a', 'b', 'c']}, index=[1, 2, 3]) households_df.index.name = 'household_id' tracing.register_traceable_table('households', households_df) @@ -159,7 +159,7 @@ def test_register_tours(capsys): print out # don't consume output # should be tracing tour with tour_id 3 - assert orca.get_injectable('trace_tours') == [12] + assert inject.get_injectable('trace_tours') == [12] close_handlers() @@ -205,10 +205,10 @@ def test_basic(capsys): close_handlers() configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - orca.add_injectable("configs_dir", configs_dir) + inject.add_injectable("configs_dir", configs_dir) output_dir = os.path.join(os.path.dirname(__file__), 'output') - orca.add_injectable("output_dir", output_dir) + inject.add_injectable("output_dir", output_dir) # remove existing handlers or basicConfig is a NOP logging.getLogger().handlers = [] diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index e8e76157a..bd11fc8a5 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -122,8 +122,6 @@ def config_logger(basic=False): if not basic: log_config_file = config.config_file_path(LOGGING_CONF_FILE_NAME, mandatory=False) - #print "log_config_file", log_config_file - if log_config_file: with open(log_config_file) as f: config_dict = yaml.load(f) @@ -452,37 +450,6 @@ def get_trace_target(df, slicer): return target_ids, column -def slice_canonically(df, slicer, label, warn_if_empty=False): - """ - Slice dataframe by traced household or person id dataframe and write to CSV - - Parameters - ---------- - df: pandas.DataFrame - dataframe to slice - slicer: str - name of column or index to use for slicing - label: str - tracer name - only used to report bad slicer - - Returns - ------- - sliced subset of dataframe - """ - - target_ids, column = get_trace_target(df, slicer) - - if target_ids is not None: - df = slice_ids(df, target_ids, column) - - if warn_if_empty and df.shape[0] == 0: - column_name = column or slicer - logger.warn("slice_canonically: no rows in %s with %s == %s" - % (label, column_name, target_ids)) - - return df - - def trace_targets(df, slicer=None): target_ids, column = get_trace_target(df, slicer) @@ -573,8 +540,6 @@ def trace_df(df, label, slicer=None, columns=None, Nothing """ - #df = slice_canonically(df, slicer, label, warn_if_empty) - target_ids, column = get_trace_target(df, slicer) if target_ids is not None: diff --git a/example/simulation.py b/example/simulation.py index b163fd1a7..89b5e8ae5 100644 --- a/example/simulation.py +++ b/example/simulation.py @@ -1,20 +1,12 @@ -import os -import logging -import time -from random import randint +# ActivitySim +# See full license in LICENSE.txt. -import numpy as np -import multiprocessing as mp +import logging -from activitysim.core import inject +from activitysim import abm from activitysim.core import tracing - -from activitysim.core.tracing import print_elapsed_time from activitysim.core.config import handle_standard_args from activitysim.core.config import setting - - -from activitysim import abm from activitysim.core import pipeline logger = logging.getLogger('activitysim') @@ -30,8 +22,6 @@ def run(): tracing.config_logger() tracing.delete_csv_files() - t0 = print_elapsed_time() - MODELS = setting('models') # If you provide a resume_after argument to pipeline.run @@ -47,8 +37,6 @@ def run(): # tables will no longer be available after pipeline is closed pipeline.close_pipeline() - t0 = print_elapsed_time("all models", t0) - if __name__ == '__main__': run() diff --git a/example_mp/simulation.py b/example_mp/simulation.py index fd41db0a4..3d8789cf8 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -1,7 +1,9 @@ -import os -import sys +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import print_function + import logging -import multiprocessing from activitysim.core import inject from activitysim.core import tracing @@ -46,10 +48,11 @@ def cleanup_output_files(): cleanup_output_files() run_list = tasks.get_run_list() - with open(config.output_file_path('run_list.txt'), 'w') as file: - tasks.print_run_list(run_list, file) + with open(config.output_file_path('run_list.txt'), 'w') as f: + tasks.print_run_list(run_list, f) # tasks.print_run_list(run_list) + # bug if run_list['multiprocess']: logger.info("run multiprocess simulation") diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 1320ac643..b1ed1bd68 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -1,18 +1,20 @@ +# ActivitySim +# See full license in LICENSE.txt. from __future__ import print_function +from future.utils import iteritems + import sys import os import time import logging -import yaml - +import multiprocessing as mp from collections import OrderedDict -from collections import Iterable +import yaml import numpy as np import pandas as pd -import multiprocessing as mp from activitysim.core import inject from activitysim.core import tracing @@ -53,7 +55,7 @@ def pipeline_table_keys(pipeline_store, checkpoint_name=None): # hdf5 key is / # FIXME - pathologically knows the format used by pipeline.pipeline_table_key() checkpoint_tables = {table_name: table_name + '/' + checkpoint_name - for table_name, checkpoint_name in checkpoint_tables.iteritems()} + for table_name, checkpoint_name in iteritems(checkpoint_tables)} # checkpoint name and series mapping table name to hdf5 key for tables in that checkpoint return checkpoint_name, checkpoint_tables @@ -68,10 +70,10 @@ def build_slice_rules(slice_info, tables): if primary_slicer not in tables: raise RuntimeError("primary slice table '%s' not in pipeline" % primary_slicer) - logger.debug("build_slice_rules tables %s" % tables.keys()) - logger.debug("build_slice_rules primary_slicer %s" % primary_slicer) - logger.debug("build_slice_rules slicer_table_names %s" % slicer_table_names) - logger.debug("build_slice_rules slicer_table_exceptions %s" % slicer_table_exceptions) + logger.debug("build_slice_rules tables %s", tables.keys()) + logger.debug("build_slice_rules primary_slicer %s", primary_slicer) + logger.debug("build_slice_rules slicer_table_names %s", slicer_table_names) + logger.debug("build_slice_rules slicer_table_exceptions %s", slicer_table_exceptions) # dict mapping slicer table_name to index name # (also presumed to be name of ref col name in referencing table) @@ -79,7 +81,7 @@ def build_slice_rules(slice_info, tables): # build slice rules for loaded tables slice_rules = {} - for table_name, df in tables.iteritems(): + for table_name, df in iteritems(tables): rule = {} if table_name == primary_slicer: @@ -96,7 +98,7 @@ def build_slice_rules(slice_info, tables): # if df has a column with same name as the ref_col (index) of a slicer? try: source, ref_col = next((t, c) - for t, c in slicer_ref_cols.iteritems() + for t, c in iteritems(slicer_ref_cols) if c in df.columns) # then we can use that table to slice this df rule = {'slice_by': 'column', @@ -111,10 +113,8 @@ def build_slice_rules(slice_info, tables): slice_rules[table_name] = rule - #print("## rule %s: %s" % (table_name, rule)) - for table_name in slice_rules: - logger.debug("%s: %s" % (table_name, slice_rules[table_name])) + logger.debug("%s: %s", table_name, slice_rules[table_name]) return slice_rules @@ -128,7 +128,7 @@ def apportion_pipeline(sub_job_names, slice_info): # get last checkpoint from first job pipeline pipeline_path = config.build_output_file_path(pipeline_file_name) - logger.debug("apportion_pipeline pipeline_path: %s" % pipeline_path) + logger.debug("apportion_pipeline pipeline_path: %s", pipeline_path) # load all tables from pipeline with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: @@ -144,13 +144,13 @@ def apportion_pipeline(sub_job_names, slice_info): raise RuntimeError("slicer table %s not found in pipeline" % table_name) # load all tables from pipeline - for table_name, hdf5_key in hdf5_keys.iteritems(): + for table_name, hdf5_key in iteritems(hdf5_keys): # new checkpoint for all tables the same checkpoints_df[table_name] = checkpoint_name # load the dataframe tables[table_name] = pipeline_store[hdf5_key] - logger.debug("loaded table %s %s" % (table_name, tables[table_name].shape)) + logger.debug("loaded table %s %s", table_name, tables[table_name].shape) # keep only the last row of checkpoints and patch the last checkpoint name checkpoints_df = checkpoints_df.tail(1).copy() @@ -169,14 +169,14 @@ def apportion_pipeline(sub_job_names, slice_info): # remove existing file try: os.unlink(pipeline_path) - except OSError as e: + except OSError: pass with pd.HDFStore(pipeline_path, mode='a') as pipeline_store: # remember sliced_tables so we can cascade slicing to other tables sliced_tables = {} - for table_name, rule in slice_rules.iteritems(): + for table_name, rule in iteritems(slice_rules): df = tables[table_name] @@ -203,12 +203,12 @@ def apportion_pipeline(sub_job_names, slice_info): hdf5_key = pipeline.pipeline_table_key(table_name, checkpoint_name) - logger.debug("writing %s (%s) to %s in %s" % - (table_name, sliced_tables[table_name].shape, hdf5_key, pipeline_path)) + logger.debug("writing %s (%s) to %s in %s", + table_name, sliced_tables[table_name].shape, hdf5_key, pipeline_path) pipeline_store[hdf5_key] = sliced_tables[table_name] - logger.debug("writing checkpoints (%s) to %s in %s" % - (checkpoints_df.shape, pipeline.CHECKPOINT_TABLE_NAME, pipeline_path)) + logger.debug("writing checkpoints (%s) to %s in %s", + checkpoints_df.shape, pipeline.CHECKPOINT_TABLE_NAME, pipeline_path) pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] = checkpoints_df @@ -216,7 +216,7 @@ def coalesce_pipelines(sub_process_names, slice_info): pipeline_file_name = inject.get_injectable('pipeline_file_name') - logger.debug("coalesce_pipelines to: %s" % pipeline_file_name) + logger.debug("coalesce_pipelines to: %s", pipeline_file_name) # tables that are identical in every pipeline and so don't need to be concatenated @@ -230,39 +230,39 @@ def coalesce_pipelines(sub_process_names, slice_info): # hdf5_keys is a dict mapping table_name to pipeline hdf5_key checkpoint_name, hdf5_keys = pipeline_table_keys(pipeline_store) - for table_name, hdf5_key in hdf5_keys.iteritems(): - logger.debug("loading table %s %s" % (table_name, hdf5_key)) + for table_name, hdf5_key in iteritems(hdf5_keys): + logger.debug("loading table %s %s", table_name, hdf5_key) tables[table_name] = pipeline_store[hdf5_key] # use slice rules followed by apportion_pipeline to identify singleton tables slice_rules = build_slice_rules(slice_info, tables) - singleton_table_names = [t for t, rule in slice_rules.iteritems() if rule['slice_by'] is None] + singleton_table_names = [t for t, rule in iteritems(slice_rules) if rule['slice_by'] is None] singleton_tables = {t: tables[t] for t in singleton_table_names} - omnibus_keys = {t: k for t, k in hdf5_keys.iteritems() if t not in singleton_table_names} + omnibus_keys = {t: k for t, k in iteritems(hdf5_keys) if t not in singleton_table_names} - logger.debug("coalesce_pipelines to: %s" % pipeline_file_name) - logger.debug("singleton_table_names: %s" % singleton_table_names) - logger.debug("omnibus_keys: %s" % omnibus_keys) + logger.debug("coalesce_pipelines to: %s", pipeline_file_name) + logger.debug("singleton_table_names: %s", singleton_table_names) + logger.debug("omnibus_keys: %s", omnibus_keys) # concat omnibus tables from all sub_processes omnibus_tables = {table_name: [] for table_name in omnibus_keys} for process_name in sub_process_names: pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) - logger.info("coalesce pipeline %s" % pipeline_path) + logger.info("coalesce pipeline %s", pipeline_path) with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: - for table_name, hdf5_key in omnibus_keys.iteritems(): + for table_name, hdf5_key in iteritems(omnibus_keys): omnibus_tables[table_name].append(pipeline_store[hdf5_key]) pipeline.open_pipeline() for table_name in singleton_tables: df = singleton_tables[table_name] - logger.info("adding singleton table %s %s" % (table_name, df.shape)) + logger.info("adding singleton table %s %s", table_name, df.shape) pipeline.replace_table(table_name, df) for table_name in omnibus_tables: df = pd.concat(omnibus_tables[table_name], sort=False) - logger.info("adding omnibus table %s %s" % (table_name, df.shape)) + logger.info("adding omnibus table %s %s", table_name, df.shape) pipeline.replace_table(table_name, df) pipeline.add_checkpoint(checkpoint_name) @@ -298,7 +298,7 @@ def allocate_shared_skim_buffer(): def setup_injectables_and_logging(injectables): - for k, v in injectables.iteritems(): + for k, v in iteritems(injectables): inject.add_injectable(k, v) inject.add_injectable("is_sub_task", True) @@ -323,14 +323,14 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): process_name = mp.current_process().name if num_processes > 1: pipeline_prefix = process_name - logger.info("injecting pipeline_file_prefix '%s'" % pipeline_prefix) + logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) setup_injectables_and_logging(injectables) - logger.info("mp_run_simulation %s num_processes %s" % (process_name, num_processes)) + logger.info("mp_run_simulation %s num_processes %s", process_name, num_processes) if resume_after: - logger.info('resume_after %s' % resume_after) + logger.info('resume_after %s', resume_after) inject.add_injectable('skim_buffer', skim_buffer) inject.add_injectable("chunk_size", chunk_size) @@ -339,48 +339,32 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): # if they specified a resume_after model, check to make sure it is checkpointed if resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: # if not checkpointed, then fall back to last checkpoint - logger.warn("resume_after checkpoint '%s' not in pipeline" % (resume_after, )) + logger.warn("resume_after checkpoint '%s' not in pipeline", resume_after) resume_after = '_' pipeline.open_pipeline(resume_after) last_checkpoint = pipeline.last_checkpoint() if last_checkpoint in models: - logger.info("Resuming model run list after %s" % (last_checkpoint, )) + logger.info("Resuming model run list after %s", last_checkpoint) models = models[models.index(last_checkpoint) + 1:] # preload any bulky injectables (e.g. skims) not in pipeline - t0 = tracing.print_elapsed_time() - preload_injectables = inject.get_injectable('preload_injectables', None) - if preload_injectables is not None: - t0 = tracing.print_elapsed_time('preload_injectables', t0) + inject.get_injectable('preload_injectables', None) t0 = tracing.print_elapsed_time() for model in models: t1 = tracing.print_elapsed_time() pipeline.run_model(model) + tracing.print_elapsed_time("run_model %s %s" % (step_label, model,), t1) queue.put({'model': model, 'time': time.time()-t1}) - t1 = tracing.print_elapsed_time("run_model %s %s" % (step_label, model,), t1) - - #logger.debug('#mem after %s, %s' % (model, util.memory_info())) - t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) - ########################### - pipeline.close_pipeline() - #fixme - # try: - # run_simulation(models, resume_after) - # except Exception as e: - # print(e) - # logger.error("Error running simulation: %s" % (e,)) - # raise e - def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): setup_injectables_and_logging(injectables) @@ -399,13 +383,13 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): def run_sub_task(p): - logger.info("running sub_process %s" % p.name) + logger.info("running sub_process %s", p.name) p.start() p.join() # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) if p.exitcode: - logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) + logger.error("Process %s returned exitcode %s", p.name, p.exitcode) raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) @@ -413,14 +397,13 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name step_name = step_info['name'] - logger.info('run_sub_simulations step %s models resume_after %s' % (step_name, resume_after)) + logger.info('run_sub_simulations step %s models resume_after %s', step_name, resume_after) # if not the first step, resume_after the last checkpoint from the previous step if resume_after is None and step_info['step_num'] > 0: resume_after = '_' num_simulations = len(process_names) - last_checkpoints = [None] * num_simulations procs = [] queues = [] @@ -432,32 +415,33 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name procs.append(p) queues.append(q) - def handle_queued_messages(): - for i, p, q in zip(range(num_simulations), procs, queues): - while not q.empty(): - msg = q.get(block=False) - model = msg['model'] - t = msg['time'] - logger.info("%s %s : %s" % (p.name, model, tracing.format_elapsed_time(t))) - if model[0] != '_': - last_checkpoints[i] = model - #update_journal(step_name, 'checkpoints', last_checkpoints) + def log_queued_messages(): + for i, process, queue in zip(range(num_simulations), procs, queues): + while not queue.empty(): + msg = queue.get(block=False) + logger.info("%s %s : %s", + process.name, + msg['model'], + tracing.format_elapsed_time(msg['time'])) + + def idle(seconds): + log_queued_messages() + for _ in range(seconds): + time.sleep(1) + log_queued_messages() stagger = 0 for p in procs: if stagger > 0: - logger.info("stagger process %s by %s seconds" % (p.name, step_info['stagger'])) - for i in range(stagger): - handle_queued_messages() - time.sleep(1) + logger.info("stagger process %s by %s seconds", p.name, stagger) + idle(stagger) stagger = step_info['stagger'] - logger.info("start process %s" % p.name) + logger.info("start process %s", p.name) p.start() while mp.active_children(): - handle_queued_messages() - time.sleep(1) - handle_queued_messages() + idle(1) + log_queued_messages() for p in procs: p.join() @@ -466,40 +450,31 @@ def handle_queued_messages(): error_count = 0 for p in procs: if p.exitcode: - logger.error("Process %s returned exitcode %s" % (p.name, p.exitcode)) + logger.error("Process %s returned exitcode %s", p.name, p.exitcode) error_count += 1 return error_count -def update_journal(step_name, key, value): - - run_status = inject.get_injectable('run_status', OrderedDict()) - if not run_status: - inject.add_injectable('run_status', run_status) - - run_status.setdefault(step_name, {'name': step_name})[key] = value - save_journal(run_status) - - def run_multiprocess(run_list, injectables): - #resume_after = run_list['resume_after'] resume_journal = run_list.get('resume_journal', {}) - - logger.info('setup shared skim data') - shared_skim_buffer = allocate_shared_skim_buffer() + run_status = OrderedDict() def skip(step_name, key): - already_did_this = resume_journal and resume_journal.get(step_name, {}).get(key, False) - if already_did_this: - logger.info("Skipping %s %s" % (step_name, key)) + logger.info("Skipping %s %s", step_name, key) time.sleep(1) - return already_did_this + def update_journal(step_name, key, value): + run_status.setdefault(step_name, {'name': step_name})[key] = value + save_journal(run_status) + + logger.info('setup shared skim data') + shared_skim_buffer = allocate_shared_skim_buffer() + # - mp_setup_skims run_sub_task( mp.Process(target=mp_setup_skims, name='mp_setup_skims', @@ -521,7 +496,7 @@ def skip(step_name, key): update_journal(step_name, 'sub_proc_names', sub_proc_names) - logger.info('running step %s with %s processes' % (step_name, num_processes,)) + logger.info('running step %s with %s processes', step_name, num_processes,) # - mp_apportion_pipeline if not skip(step_name, 'apportion'): @@ -562,8 +537,8 @@ def get_resume_journal(run_list): previous_journal = read_journal() if not previous_journal: - logger.error("empty journal for resume_after '%s'" % (resume_after,)) - raise RuntimeError("empty journal for resume_after '%s'" % (resume_after,)) + logger.error("empty journal for resume_after '%s'", resume_after) + raise RuntimeError("empty journal for resume_after '%s'" % resume_after) if resume_after == '_': resume_step_name = previous_journal.keys()[-1] @@ -576,14 +551,16 @@ def get_resume_journal(run_list): if resume_after in step['models']), None) if resume_step_name not in previous_steps: - logger.error("resume_after model '%s' not in journal" % (resume_after,)) - raise RuntimeError("resume_after model '%s' not in journal" % (resume_after,)) + logger.error("resume_after model '%s' not in journal", resume_after) + raise RuntimeError("resume_after model '%s' not in journal" % resume_after) # drop any previous_journal steps after resume_step for step in previous_steps[previous_steps.index(resume_step_name) + 1:]: del previous_journal[step] - multiprocess_step = next((step for step in run_list['multiprocess_steps'] if step['name']==resume_step_name), []) + multiprocess_step = next((step for step in run_list['multiprocess_steps'] + if step['name'] == resume_step_name), []) + print("resume_step_models", multiprocess_step['models']) if resume_after in multiprocess_step['models'][:-1]: @@ -624,7 +601,6 @@ def get_run_list(): if not models or not isinstance(models, list): raise RuntimeError('No models list in settings file') - if resume_after not in models + ['_', None]: raise RuntimeError("resume_after '%s' not in models list" % resume_after) if resume_after == models[-1]: @@ -637,8 +613,9 @@ def get_run_list(): multiprocess) # check step name, num_processes, chunk_size and presence of slice info + num_steps = len(multiprocess_steps) step_names = set() - for istep in range(len(multiprocess_steps)): + for istep in range(num_steps): step = multiprocess_steps[istep] step['step_num'] = istep @@ -662,15 +639,15 @@ def get_run_list(): if 'slice' in step: if num_processes == 0: - logger.info("Setting num_processes = %s for step %s" % - (num_processes, name)) + logger.info("Setting num_processes = %s for step %s", + num_processes, name) num_processes = mp.cpu_count() if num_processes == 1: raise RuntimeError("num_processes = 1 but found slice info for step %s" " in multiprocess_steps" % name) if num_processes > mp.cpu_count(): - logger.warn("num_processes setting (%s) greater than cpu count (%s" % - (num_processes, mp.cpu_count())) + logger.warn("num_processes setting (%s) greater than cpu count (%s", + num_processes, mp.cpu_count()) else: if num_processes == 0: num_processes = 1 @@ -695,9 +672,9 @@ def get_run_list(): multiprocess_steps[istep]['stagger'] = max(int(step.get('stagger', 0)), 0) # - determine index in models list of step starts - START = 'begin' + start_tag = 'begin' starts = [0] * len(multiprocess_steps) - for istep in range(len(multiprocess_steps)): + for istep in range(num_steps): step = multiprocess_steps[istep] name = step['name'] @@ -708,30 +685,30 @@ def get_run_list(): raise RuntimeError("missing tables list for step %s" " in multiprocess_steps" % istep) - start = step.get(START, None) + start = step.get(start_tag, None) if not name: raise RuntimeError("missing %s tag for step '%s' (%s)" " in multiprocess_steps" % - (START, name, istep)) + (start_tag, name, istep)) if start not in models: raise RuntimeError("%s tag '%s' for step '%s' (%s) not in models list" % - (START, start, name, istep)) + (start_tag, start, name, istep)) starts[istep] = models.index(start) if istep == 0 and starts[istep] != 0: raise RuntimeError("%s tag '%s' for first step '%s' (%s)" " is not first model in models list" % - (START, start, name, istep)) + (start_tag, start, name, istep)) if istep > 0 and starts[istep] <= starts[istep - 1]: raise RuntimeError("%s tag '%s' for step '%s' (%s)" " falls before that of prior step in models list" % - (START, start, name, istep)) + (start_tag, start, name, istep)) # - build step model lists starts.append(len(models)) # so last step gets remaining models in list - for istep in range(len(multiprocess_steps)): + for istep in range(num_steps): step_models = models[starts[istep]: starts[istep + 1]] if step_models[-1][0] == '_': @@ -742,6 +719,7 @@ def get_run_list(): run_list['multiprocess_steps'] = multiprocess_steps + # - add resume_journal if resume_after: resume_journal = get_resume_journal(run_list) if resume_journal: @@ -750,59 +728,59 @@ def get_run_list(): istep = len(resume_journal) - 1 multiprocess_steps[istep]['resume_after'] = resume_after - return run_list -def print_run_list(run_list, file=None): +def print_run_list(run_list, output_file=None): - if file is None: - file = sys.stdout + if output_file is None: + output_file = sys.stdout - print("resume_after:", run_list['resume_after'], file=file) - print("multiprocess:", run_list['multiprocess'], file=file) + print("resume_after:", run_list['resume_after'], file=output_file) + print("multiprocess:", run_list['multiprocess'], file=output_file) - print("models", file=file) + print("models", file=output_file) for m in run_list['models']: - print(" - ", m, file=file) + print(" - ", m, file=output_file) if run_list['multiprocess']: - print("\nmultiprocess_steps:", file=file) + print("\nmultiprocess_steps:", file=output_file) for step in run_list['multiprocess_steps']: - print(" step:", step['name'], file=file) + print(" step:", step['name'], file=output_file) for k in step: if isinstance(step[k], (list, )): - print(" ", k, file=file) + print(" ", k, file=output_file) for v in step[k]: - print(" -", v, file=file) + print(" -", v, file=output_file) else: - print(" %s: %s" % (k, step[k]), file=file) + print(" %s: %s" % (k, step[k]), file=output_file) if run_list.get('resume_journal'): - print("\nresume_journal:", file=file) - print_journal(run_list['resume_journal'], file) + print("\nresume_journal:", file=output_file) + print_journal(run_list['resume_journal'], output_file) else: - print("models", file=file) + print("models", file=output_file) for m in run_list['models']: - print(" - ", m, file=file) + print(" - ", m, file=output_file) -def print_journal(journal, file=None): - if file is None: - file = sys.stdout +def print_journal(journal, output_file=None): + + if output_file is None: + output_file = sys.stdout for step_name in journal: step = journal[step_name] - print(" step:", step_name, file=file) + print(" step:", step_name, file=output_file) for k in step: if isinstance(k, str): - print(" ", k, step[k], file=file) + print(" ", k, step[k], file=output_file) else: - print(" ", k, file=file) + print(" ", k, file=output_file) for v in step[k]: - print(" ", v, file=file) + print(" ", v, file=output_file) def journal_file_path(file_name=None): @@ -825,6 +803,7 @@ def save_journal(journal, file_name=None): journal = [step for step in journal.values()] yaml.dump(journal, f) + def is_sub_task(): return inject.get_injectable('is_sub_task', False) @@ -834,6 +813,7 @@ def if_sub_task(if_is, if_isnt): return if_is if is_sub_task() else if_isnt + def if_sub_task_opt(if_is, if_isnt): opt = { @@ -844,4 +824,3 @@ def if_sub_task_opt(if_is, if_isnt): 'NOTSET': logging.NOTSET, } return opt[if_is] if is_sub_task() else opt[if_isnt] - diff --git a/setup.py b/setup.py index d90ac1922..14a6187f8 100644 --- a/setup.py +++ b/setup.py @@ -24,12 +24,12 @@ install_requires=[ 'numpy >= 1.13.0', 'openmatrix >= 0.2.4', - #'orca >= 1.1', 'pandas >= 0.20.3', 'pyyaml >= 3.0', 'tables >= 3.3.0', - 'toolz >= 0.7', + 'toolz >= 0.8.1', 'zbox >= 1.2', - 'psutil >= 4.1' + 'psutil >= 4.1', + 'future >= 0.16.0' ] ) From b1eb10313555c86263a5e0e115d924edd8ff4602 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Sat, 13 Oct 2018 00:29:47 -0400 Subject: [PATCH 022/122] pycodestyle --- activitysim/core/inject.py | 4 +--- activitysim/core/pipeline.py | 2 +- activitysim/core/test/test_inject_defaults.py | 1 - activitysim/core/test/test_pipeline.py | 3 --- example_mp/tasks.py | 1 - 5 files changed, 2 insertions(+), 9 deletions(-) diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index da31db36e..fbe1f44e2 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -87,7 +87,7 @@ def add_table(table_name, table, cache=False): return orca.add_table(table_name, table, cache=cache) -#fixme remove? +# fixme remove? def add_column(table_name, column_name, column, cache=False): return orca.add_column(table_name, column_name, column, cache=cache) @@ -120,8 +120,6 @@ def get_injectable(name, default=_NO_DEFAULT): def remove_injectable(name): - #fixme - #del orca.orca._INJECTABLES[name] orca.orca._INJECTABLES.pop(name, None) diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index abaf435c9..c2840bf63 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -227,7 +227,7 @@ def rewrap(table_name, df=None): for column_name in orca.list_columns_for_table(table_name): # logger.debug("pop %s.%s: %s" % (table_name, column_name, t.column_type(column_name))) - #fixme + # fixme orca.orca._COLUMNS.pop((table_name, column_name), None) # remove from orca's table list diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index c8bd73083..366760f56 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -44,4 +44,3 @@ def test_defaults(): data_dir = os.path.join(os.path.dirname(__file__), 'data') inject.add_injectable("data_dir", data_dir) - diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index a0274e38b..b851c637e 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -12,7 +12,6 @@ from activitysim.core import pipeline from activitysim.core import inject -#from . import extensions from .extensions import steps # set the max households for all tests (this is to limit memory use on travis) @@ -58,7 +57,6 @@ def test_pipeline_run(): setup() - #fixme inject.add_step('step1', steps.step1) inject.add_step('step2', steps.step2) inject.add_step('step3', steps.step3) @@ -106,7 +104,6 @@ def test_pipeline_checkpoint_drop(): setup() - #fixme inject.add_step('step1', steps.step1) inject.add_step('step2', steps.step2) inject.add_step('step3', steps.step3) diff --git a/example_mp/tasks.py b/example_mp/tasks.py index b1ed1bd68..4886d0898 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -765,7 +765,6 @@ def print_run_list(run_list, output_file=None): print(" - ", m, file=output_file) - def print_journal(journal, output_file=None): if output_file is None: From 2d6fc318a352f66e9dc6c5586a46c35e0c19df1d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Sat, 13 Oct 2018 22:07:22 -0400 Subject: [PATCH 023/122] futurize stage2 --- activitysim/abm/models/__init__.py | 58 ++++++++++--------- activitysim/abm/models/accessibility.py | 4 +- .../abm/models/joint_tour_destination.py | 9 +-- .../abm/models/joint_tour_participation.py | 6 +- activitysim/abm/models/school_location.py | 8 ++- activitysim/abm/models/trip_destination.py | 7 ++- activitysim/abm/models/trip_mode_choice.py | 4 +- activitysim/abm/models/trip_scheduling.py | 11 +++- activitysim/abm/models/util/cdap.py | 15 +++-- activitysim/abm/models/util/logsums.py | 10 ++-- activitysim/abm/models/util/mode.py | 16 +++-- activitysim/abm/models/util/test/test_cdap.py | 1 + activitysim/abm/models/util/tour_frequency.py | 24 ++++---- .../models/util/vectorize_tour_scheduling.py | 4 -- activitysim/abm/tables/__init__.py | 24 ++++---- activitysim/abm/tables/households.py | 10 ++-- activitysim/abm/tables/landuse.py | 4 +- activitysim/abm/tables/persons.py | 7 +-- activitysim/abm/tables/skims.py | 20 ++++--- activitysim/abm/test/test_misc.py | 1 + activitysim/abm/test/test_pipeline.py | 22 +++---- activitysim/core/assign.py | 12 +++- activitysim/core/chunk.py | 1 + activitysim/core/inject.py | 15 +++-- activitysim/core/interaction_sample.py | 7 ++- .../core/interaction_sample_simulate.py | 1 + activitysim/core/interaction_simulate.py | 2 + activitysim/core/logit.py | 6 +- activitysim/core/orca/utils/testing.py | 5 +- activitysim/core/pipeline.py | 33 +++++++---- activitysim/core/random.py | 13 +++-- activitysim/core/simulate.py | 11 ++-- activitysim/core/skim.py | 16 +++-- activitysim/core/test/test_assign.py | 18 +++--- activitysim/core/test/test_inject_defaults.py | 7 +-- activitysim/core/test/test_logit.py | 1 + activitysim/core/test/test_pipeline.py | 10 ++-- activitysim/core/test/test_random.py | 9 +-- activitysim/core/test/test_timetable.py | 8 +-- activitysim/core/test/test_tracing.py | 20 ++++--- activitysim/core/test/test_util.py | 1 + activitysim/core/timetable.py | 11 ++-- activitysim/core/tracing.py | 18 ++++-- activitysim/core/util.py | 3 + docs/howitworks.rst | 2 +- example/simulation.py | 10 ++-- example_mp/tasks.py | 29 ++++++---- 47 files changed, 314 insertions(+), 220 deletions(-) diff --git a/activitysim/abm/models/__init__.py b/activitysim/abm/models/__init__.py index 717ee5554..080fee5e9 100644 --- a/activitysim/abm/models/__init__.py +++ b/activitysim/abm/models/__init__.py @@ -1,35 +1,37 @@ # ActivitySim # See full license in LICENSE.txt. -import initialize -import accessibility -import auto_ownership -import mandatory_tour_frequency -import mandatory_scheduling -import joint_tour_frequency -import joint_tour_composition -import joint_tour_participation -import joint_tour_destination -import joint_tour_scheduling -import non_mandatory_tour_frequency -import non_mandatory_destination -import non_mandatory_scheduling -import school_location -import workplace_location -import tour_mode_choice -import cdap +from __future__ import absolute_import -import atwork_subtour_frequency -import atwork_subtour_destination -import atwork_subtour_scheduling -import atwork_subtour_mode_choice +from . import initialize +from . import accessibility +from . import auto_ownership +from . import mandatory_tour_frequency +from . import mandatory_scheduling +from . import joint_tour_frequency +from . import joint_tour_composition +from . import joint_tour_participation +from . import joint_tour_destination +from . import joint_tour_scheduling +from . import non_mandatory_tour_frequency +from . import non_mandatory_destination +from . import non_mandatory_scheduling +from . import school_location +from . import workplace_location +from . import tour_mode_choice +from . import cdap -import stop_frequency -import trip_purpose -import trip_destination -import trip_purpose_and_destination -import trip_scheduling -import trip_mode_choice +from . import atwork_subtour_frequency +from . import atwork_subtour_destination +from . import atwork_subtour_scheduling +from . import atwork_subtour_mode_choice + +from . import stop_frequency +from . import trip_purpose +from . import trip_destination +from . import trip_purpose_and_destination +from . import trip_scheduling +from . import trip_mode_choice # parameterized models -import annotate_table +from . import annotate_table diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 22c4acd8a..f0f4adf28 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import range +from builtins import object import logging import os @@ -57,7 +59,7 @@ def __init__(self, skim_dict, orig_zones, dest_zones, transpose=False): # data = data[orig_map, :][:, dest_map] # <- RIGHT # data = data[np.ix_(orig_map, dest_map)] # <- ALSO RIGHT - skim_index = range(omx_shape[0]) + skim_index = list(range(omx_shape[0])) orig_map = np.isin(skim_index, skim_dict.offset_mapper.map(orig_zones)) dest_map = np.isin(skim_index, skim_dict.offset_mapper.map(dest_zones)) diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index 7979ec5bb..a192e0566 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -1,7 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from future.utils import iteritems + import logging from collections import OrderedDict @@ -138,7 +139,7 @@ def joint_tour_destination_sample( choices_list = [] # segment by trip type and pick the right spec for each person type # for tour_type, choosers_segment in choosers.groupby('tour_type'): - for tour_type, tour_type_id in TOUR_TYPE_ID.iteritems(): + for tour_type, tour_type_id in iteritems(TOUR_TYPE_ID): locals_d['segment'] = tour_type @@ -229,7 +230,7 @@ def joint_tour_destination_logsums( logsum.filter_chooser_columns(joint_tours_merged, logsum_settings, model_settings) logsums_list = [] - for tour_type, tour_type_id in TOUR_TYPE_ID.iteritems(): + for tour_type, tour_type_id in iteritems(TOUR_TYPE_ID): choosers = destination_sample[destination_sample['tour_type_id'] == tour_type_id] @@ -334,7 +335,7 @@ def joint_tour_destination_simulate( choices_list = [] # segment by trip type and pick the right spec for each person type # for tour_type, choosers_segment in choosers.groupby('tour_type'): - for tour_type, tour_type_id in TOUR_TYPE_ID.iteritems(): + for tour_type, tour_type_id in iteritems(TOUR_TYPE_ID): locals_d['segment'] = tour_type diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 13214ebf4..5691be088 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -1,10 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import print_function + import logging -import numpy as np import pandas as pd from activitysim.core import simulate @@ -159,7 +159,7 @@ def participants_chooser(probs, choosers, spec, trace_label): unsatisfied_candidates = candidates[diagnostic_cols].join(probs) tracing.write_csv(unsatisfied_candidates, file_name='%s.UNSATISFIED' % trace_label, transpose=False) - print unsatisfied_candidates.head(20) + print(unsatisfied_candidates.head(20)) assert False choices, rands = logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 3d0ec1dd6..b10717ea4 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +from future.utils import iteritems + import logging from collections import OrderedDict @@ -89,7 +91,7 @@ def school_location_sample( locals_d.update(constants) choices_list = [] - for school_type, school_type_id in SCHOOL_TYPE_ID.iteritems(): + for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): locals_d['segment'] = school_type @@ -179,7 +181,7 @@ def school_location_logsums( persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) logsums_list = [] - for school_type, school_type_id in SCHOOL_TYPE_ID.iteritems(): + for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): tour_purpose = 'univ' if school_type == 'university' else 'school' @@ -265,7 +267,7 @@ def school_location_simulate(persons_merged, persons, choosers = choosers[chooser_columns] choices_list = [] - for school_type, school_type_id in SCHOOL_TYPE_ID.iteritems(): + for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): locals_d['segment'] = school_type diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 7d29119ae..f02250727 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -1,7 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import print_function + +from builtins import range import logging import numpy as np @@ -18,7 +20,6 @@ from activitysim.core.util import reindex from activitysim.core.util import assign_in_place -from .util import logsums from .util import expressions from .util.mode import annotate_preprocessors @@ -560,7 +561,7 @@ def trip_destination( pipeline.replace_table("trips", trips_df) - print "trips_df\n", trips_df.shape + print("trips_df\n", trips_df.shape) if trace_hh_id: tracing.trace_df(trips_df, diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 247f42b69..db0ed6ad1 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -2,6 +2,8 @@ # See full license in LICENSE.txt. +from builtins import zip +from builtins import range import logging import pandas as pd @@ -119,7 +121,7 @@ def trip_mode_choice( trace_choice_name='trip_mode_choice') alts = model_spec.columns - choices = choices.map(dict(zip(range(len(alts)), alts))) + choices = choices.map(dict(list(zip(list(range(len(alts))), alts)))) # tracing.print_summary('trip_mode_choice %s choices' % primary_purpose, # choices, value_counts=True) diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 9a65378c9..7dbb09879 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -1,7 +1,11 @@ +from __future__ import division # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import division + +from builtins import range + import logging import numpy as np @@ -371,7 +375,7 @@ def trip_scheduling_rpc(chunk_size, choosers, spec, trace_label=None): chooser_row_size = choosers.shape[1] + extra_columns # scale row_size by average number of chooser rows per chunk_id - rows_per_chunk_id = choosers.shape[0] / float(num_choosers) + rows_per_chunk_id = choosers.shape[0] / num_choosers row_size = (rows_per_chunk_id * chooser_row_size) # print "num_choosers", num_choosers @@ -504,7 +508,8 @@ def trip_scheduling( tours = tours.to_frame() # add tour-based chunk_id so we can chunk all trips in tour together - trips_df['chunk_id'] = reindex(pd.Series(range(tours.shape[0]), tours.index), trips_df.tour_id) + trips_df['chunk_id'] = \ + reindex(pd.Series(list(range(tours.shape[0])), tours.index), trips_df.tour_id) max_iterations = model_settings.get('MAX_ITERATIONS', 1) assert max_iterations > 0 diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index a6df0cbc1..81a8dd840 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -1,6 +1,9 @@ +from __future__ import division # ActivitySim # See full license in LICENSE.txt. +from builtins import range + import logging import itertools @@ -322,7 +325,7 @@ def build_cdap_spec(interaction_coefficients, hhsize, # list of alternative columns where person pnum has expression activity # e.g. for M_p1 we want the columns where activity M is in position p1 - alternative_columns = filter(lambda alt: alt[pnum - 1] == activity, alternatives) + alternative_columns = [alt for alt in alternatives if alt[pnum - 1] == activity] spec.loc[new_row_index, alternative_columns] = 1 # ignore rows whose cardinality exceeds hhsize @@ -348,13 +351,13 @@ def build_cdap_spec(interaction_coefficients, hhsize, continue - if row.cardinality not in range(1, MAX_INTERACTION_CARDINALITY+1): + if not (0 <= row.cardinality <= MAX_INTERACTION_CARDINALITY): raise RuntimeError("Bad row cardinality %d for %s" % (row.cardinality, row.slug)) # for all other interaction rules, we need to generate a row in the spec for each # possible combination of interacting persons # e.g. for (1, 2), (1,3), (2,3) for a coefficient with cardinality 2 in hhsize 3 - for tup in itertools.combinations(range(1, hhsize+1), row.cardinality): + for tup in itertools.combinations(list(range(1, hhsize+1)), row.cardinality): # determine the name of the chooser column with the ptypes for this interaction if row.cardinality == 1: @@ -369,8 +372,10 @@ def build_cdap_spec(interaction_coefficients, hhsize, # create list of columns with names matching activity for each of the persons in tup # e.g. ['MMM', 'MMN', 'MMH'] for an interaction between p1 and p3 with activity 'M' + # alternative_columns = \ + # filter(lambda alt: all([alt[p - 1] == row.activity for p in tup]), alternatives) alternative_columns = \ - filter(lambda alt: all([alt[p - 1] == row.activity for p in tup]), alternatives) + [alt for alt in alternatives if all([alt[p - 1] == row.activity for p in tup])] # a row for this interaction may already exist, # e.g. if there are rules for both HH13 and MM13, we don't need to add rows for both @@ -535,7 +540,7 @@ def hh_choosers(indiv_utils, hhsize): # add interaction columns for all 2 and 3 person interactions for i in range(2, min(hhsize, MAX_INTERACTION_CARDINALITY)+1): - for tup in itertools.combinations(range(1, hhsize+1), i): + for tup in itertools.combinations(list(range(1, hhsize+1)), i): add_interaction_column(choosers, tup) return choosers diff --git a/activitysim/abm/models/util/logsums.py b/activitysim/abm/models/util/logsums.py index 4100367c5..eb1d59025 100644 --- a/activitysim/abm/models/util/logsums.py +++ b/activitysim/abm/models/util/logsums.py @@ -1,11 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -import os -import logging +from __future__ import absolute_import -import numpy as np -import pandas as pd +import logging from activitysim.core import simulate from activitysim.core import tracing @@ -13,8 +11,8 @@ from activitysim.core.assign import evaluate_constants -from mode import tour_mode_choice_spec -from mode import tour_mode_choice_coeffecients_spec +from .mode import tour_mode_choice_spec +from .mode import tour_mode_choice_coeffecients_spec from . import expressions diff --git a/activitysim/abm/models/util/mode.py b/activitysim/abm/models/util/mode.py index 782fcbba9..236a49ca0 100644 --- a/activitysim/abm/models/util/mode.py +++ b/activitysim/abm/models/util/mode.py @@ -1,21 +1,19 @@ # ActivitySim # See full license in LICENSE.txt. -import os -import copy -import string +from __future__ import absolute_import + +from builtins import zip +from builtins import range + import pandas as pd -import numpy as np -from activitysim.core import tracing -from activitysim.core import inject from activitysim.core import simulate from activitysim.core import config - from activitysim.core.assign import evaluate_constants from activitysim.core.util import assign_in_place -import expressions +from . import expressions """ @@ -82,7 +80,7 @@ def run_tour_mode_choice_simulate( trace_choice_name=trace_choice_name) alts = spec.columns - choices = choices.map(dict(zip(range(len(alts)), alts))) + choices = choices.map(dict(list(zip(list(range(len(alts))), alts)))) return choices diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 8d1267d86..5dc0995ef 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -1,6 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str import os.path from itertools import product diff --git a/activitysim/abm/models/util/tour_frequency.py b/activitysim/abm/models/util/tour_frequency.py index bce49f078..fce37b8cc 100644 --- a/activitysim/abm/models/util/tour_frequency.py +++ b/activitysim/abm/models/util/tour_frequency.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str +from builtins import range +from future.utils import iteritems + import logging import numpy as np @@ -16,7 +20,7 @@ def enumerate_tour_types(tour_flavors): # tour_flavors: {'eat': 1, 'business': 2, 'maint': 1} # channels: ['eat1', 'business1', 'business2', 'maint1'] channels = [tour_type + str(tour_num) - for tour_type, max_count in tour_flavors.iteritems() + for tour_type, max_count in iteritems(tour_flavors) for tour_num in range(1, max_count + 1)] return channels @@ -110,7 +114,7 @@ def set_tour_index(tours, parent_tour_num_col=None, is_joint=False): # map recognized strings to ints tours.tour_id = tours.tour_id.replace(to_replace=possible_tours, - value=range(possible_tours_count)) + value=list(range(possible_tours_count))) # convert to numeric - shouldn't be any NaNs - this will raise error if there are tours.tour_id = pd.to_numeric(tours.tour_id, errors='coerce').astype(int) @@ -149,17 +153,17 @@ def process_tours(tour_frequency, tour_frequency_alts, tour_category, parent_col Returns ------- - tours : DataFrame + tours : pandas.DataFrame An example of a tours DataFrame is supplied as a comment in the source code - it has an index which is a unique tour identifier, a person_id column, and a tour type column which comes from the column names of the alternatives DataFrame supplied above. - tours.tour_type - tour type (e.g. school, work, shopping, eat) - tours.tour_type_num - if there are two 'school' type tours, they will be numbered 1 and 2 - tours.tour_type_count - number of tours of tour_type parent has (parent's max tour_type_num) - tours.tour_num - index of tour (of any type) for parent - tours.tour_count - number of tours of any type) for parent (parent's max tour_num) + tours.tour_type - tour type (e.g. school, work, shopping, eat) + tours.tour_type_num - if there are two 'school' type tours, they will be numbered 1 and 2 + tours.tour_type_count - number of tours of tour_type parent has (parent's max tour_type_num) + tours.tour_num - index of tour (of any type) for parent + tours.tour_count - number of tours of any type) for parent (parent's max tour_num) """ # FIXME - document requirement to ensure adjacent tour_type_nums in tour_num order @@ -257,7 +261,7 @@ def process_mandatory_tours(persons, mandatory_tour_frequency_alts): depends on the is_worker column: work tours first for workers, second for non-workers """ - PERSON_COLUMNS = ['mandatory_tour_frequency', 'is_worker', + person_columns = ['mandatory_tour_frequency', 'is_worker', 'school_taz', 'workplace_taz', 'home_taz', 'household_id'] assert not persons.mandatory_tour_frequency.isnull().any() @@ -266,7 +270,7 @@ def process_mandatory_tours(persons, mandatory_tour_frequency_alts): tour_category='mandatory') tours_merged = pd.merge(tours[['person_id', 'tour_type']], - persons[PERSON_COLUMNS], + persons[person_columns], left_on='person_id', right_index=True) # by default work tours are first for work_and_school tours diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index f9165f483..b251f59b5 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -9,11 +9,7 @@ from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core import tracing from activitysim.core import inject - from activitysim.core import timetable as tt - -from activitysim.core.util import memory_info -from activitysim.core.util import df_size from activitysim.core.util import force_garbage_collect from activitysim.core.util import reindex diff --git a/activitysim/abm/tables/__init__.py b/activitysim/abm/tables/__init__.py index cf8bd5bb8..7d95ba658 100644 --- a/activitysim/abm/tables/__init__.py +++ b/activitysim/abm/tables/__init__.py @@ -1,16 +1,18 @@ # ActivitySim # See full license in LICENSE.txt. -import input_store +from __future__ import absolute_import -import households -import persons -import landuse -import skims -import tours -import size_terms -import trips -import time_windows +from . import input_store -import constants -import table_dict +from . import households +from . import persons +from . import landuse +from . import skims +from . import tours +from . import size_terms +from . import trips +from . import time_windows + +from . import constants +from . import table_dict diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index 32b2dfbcd..700979fad 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -1,18 +1,18 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import absolute_import + +from builtins import range import logging -import os import pandas as pd -from activitysim.core import simulate as asim from activitysim.core import tracing from activitysim.core import pipeline - from activitysim.core import inject -from input_store import read_input_table +from .input_store import read_input_table logger = logging.getLogger(__name__) @@ -80,7 +80,7 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): # FIXME - pathological knowledge of name of chunk_id column used by chunked_choosers_by_chunk_id assert 'chunk_id' not in df.columns - df['chunk_id'] = pd.Series(range(len(df)), df.index) + df['chunk_id'] = pd.Series(list(range(len(df))), df.index) # replace table function with dataframe inject.add_table('households', df) diff --git a/activitysim/abm/tables/landuse.py b/activitysim/abm/tables/landuse.py index d12e915db..34fabf81d 100644 --- a/activitysim/abm/tables/landuse.py +++ b/activitysim/abm/tables/landuse.py @@ -1,10 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import absolute_import + import logging from activitysim.core import inject -from input_store import read_input_table +from .input_store import read_input_table logger = logging.getLogger(__name__) diff --git a/activitysim/abm/tables/persons.py b/activitysim/abm/tables/persons.py index abec59f65..c5bb81f3b 100644 --- a/activitysim/abm/tables/persons.py +++ b/activitysim/abm/tables/persons.py @@ -1,16 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. -import logging +from __future__ import absolute_import -import pandas as pd +import logging from activitysim.core import pipeline from activitysim.core import inject from activitysim.core import tracing -from activitysim.core.util import other_than, reindex -from input_store import read_input_table +from .input_store import read_input_table logger = logging.getLogger(__name__) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index f4d8df9f4..928f107e2 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -3,6 +3,10 @@ from __future__ import print_function +from builtins import map +from builtins import range +from future.utils import iteritems + import sys import os import logging @@ -29,7 +33,7 @@ def get_skim_info(omx_file_path, tags_to_load=None): # this is sys.maxint for p2.7 but no limit for p3 # MAX_BLOCK_BYTES = 28880000 - MAX_BLOCK_BYTES = sys.maxint + MAX_BLOCK_BYTES = sys.maxsize # Note: we load all skims except those with key2 not in tags_to_load # Note: we require all skims to be of same dtype so they can share buffer - is that ok? @@ -55,14 +59,14 @@ def get_skim_info(omx_file_path, tags_to_load=None): skim_key = (key1, key2) if sep else key1 omx_keys[skim_key] = skim_name - num_skims = len(omx_keys.keys()) + num_skims = len(omx_keys) skim_data_shape = omx_shape + (num_skims, ) # - key1_subkeys dict maps key1 to dict of subkeys with that key1 # DIST: {'DIST': 0} # DRV_COM_WLK_BOARDS: {'MD': 1, 'AM': 0, 'PM': 2}, ... key1_subkeys = OrderedDict() - for skim_key, omx_key in omx_keys.iteritems(): + for skim_key, omx_key in iteritems(omx_keys): if isinstance(skim_key, tuple): key1, key2 = skim_key else: @@ -89,7 +93,7 @@ def block_name(block): key1_block_offsets = OrderedDict() blocks = OrderedDict() block = offset = 0 - for key1, v in key1_subkeys.iteritems(): + for key1, v in iteritems(key1_subkeys): num_subkeys = len(v) if offset + num_subkeys > max_skims_per_block: # next block blocks[block_name(block)] = offset @@ -142,7 +146,7 @@ def buffer_for_skims(skim_info, shared=False): blocks = skim_info['blocks'] skim_buffer = {} - for block_name, block_size in blocks.iteritems(): + for block_name, block_size in iteritems(blocks): buffer_size = np.prod(omx_shape) * block_size @@ -175,7 +179,7 @@ def skim_data_from_buffer(skim_buffer, skim_info): blocks = skim_info['blocks'] skim_data = [] - for block_name, block_size in blocks.iteritems(): + for block_name, block_size in iteritems(blocks): skims_shape = omx_shape + (block_size,) block_buffer = skim_buffer[block_name] assert len(block_buffer) == int(np.prod(skims_shape)) @@ -194,7 +198,7 @@ def load_skims(omx_file_path, skim_info, skim_buffer): # read skims into skim_data with omx.open_file(omx_file_path) as omx_file: - for skim_key, omx_key in omx_keys.iteritems(): + for skim_key, omx_key in iteritems(omx_keys): omx_data = omx_file[omx_key] assert np.issubdtype(omx_data.dtype, np.floating) @@ -234,7 +238,7 @@ def skim_dict(data_dir, settings): skim_data = skim_data_from_buffer(skim_buffer, skim_info) - block_names = skim_info['blocks'].keys() + block_names = list(skim_info['blocks'].keys()) for i in range(len(skim_data)): block_name = block_names[i] block_data = skim_data[i] diff --git a/activitysim/abm/test/test_misc.py b/activitysim/abm/test/test_misc.py index 380b43a3b..870eb8fd7 100644 --- a/activitysim/abm/test/test_misc.py +++ b/activitysim/abm/test/test_misc.py @@ -1,6 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str import os import tempfile diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 2da6c204b..a32302725 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -1,20 +1,18 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +from builtins import str import os -import tempfile import logging -import numpy as np import pandas as pd import pandas.util.testing as pdt import pytest import yaml -import openmatrix as omx - -from activitysim.abm import __init__ -from activitysim.abm.tables import size_terms +from activitysim.core import random from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import inject @@ -89,6 +87,8 @@ def test_rng_access(): rng = pipeline.get_rn_generator() + assert isinstance(rng, random.Random) + pipeline.close_pipeline() inject.clear_cache() @@ -103,7 +103,7 @@ def regress_mini_auto(): expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), name='auto_ownership') - print "auto_choice\n", auto_choice.head(10) + print("auto_choice\n", auto_choice.head(10)) """ auto_choice household_id @@ -132,7 +132,7 @@ def regress_mini_mtf(): expected_choice = pd.Series(choices, index=pd.Index(per_ids, name='person_id'), name='mandatory_tour_frequency') - print "mtf_choice\n", mtf_choice.dropna().head(5) + print("mtf_choice\n", mtf_choice.dropna().head(5)) """ mtf_choice 26986 school1 @@ -315,7 +315,7 @@ def regress_tour_modes(tours_df): tours_df = tours_df[tours_df.household_id == HH_ID] tours_df = tours_df.sort_values(by=['person_id', 'tour_category', 'tour_num']) - print "mode_df\n", tours_df[mode_cols] + print("mode_df\n", tours_df[mode_cols]) """ tour_id tour_mode person_id tour_type tour_num tour_category @@ -395,7 +395,7 @@ def test_full_run1(): tour_count = full_run(trace_hh_id=HH_ID, check_for_variability=True, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - print "tour_count", tour_count + print("tour_count", tour_count) assert(tour_count == EXPECT_TOUR_COUNT) @@ -455,6 +455,6 @@ def test_full_run_stability(): if __name__ == "__main__": - print "running test_full_run1" + print("running test_full_run1") test_full_run1() # teardown_function(None) diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index f8eb79072..81b9778f6 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -1,6 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from future.utils import iteritems + +from builtins import zip +from builtins import object + import logging import os from collections import OrderedDict @@ -57,7 +62,7 @@ def evaluate_constants(expressions, constants): # FIXME why copy? d = {} - for k, v in expressions.iteritems(): + for k, v in iteritems(expressions): d[k] = eval(str(v), d.copy(), constants) return d @@ -220,7 +225,7 @@ def to_series(x): _locals_dict[df_alias] = df else: _locals_dict['df'] = df - local_keys = _locals_dict.keys() + local_keys = list(_locals_dict.keys()) # build a dataframe of eval results for non-temp targets # since we allow targets to be recycled, we want to only keep the last usage @@ -232,7 +237,8 @@ def to_series(x): target, expression = e assert isinstance(target, str), \ - "expected target '%s' for expression '%s' to be string" % (target, expression) + "expected target '%s' for expression '%s' to be string not %s" % \ + (target, expression, type(target)) if target in local_keys: logger.warn("assign_variables target obscures local_d name '%s'" % str(target)) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index e87ffc5a2..58e3fa7ce 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -1,3 +1,4 @@ +from __future__ import division # ActivitySim # See full license in LICENSE.txt. diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index fbe1f44e2..e86b6668a 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -1,9 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +from future.utils import iteritems + import logging -import pandas as pd from . import orca _DECORATED_STEPS = {} @@ -136,16 +139,16 @@ def reinject_decorated_tables(): orca.orca._TABLE_CACHE.clear() orca.orca._COLUMN_CACHE.clear() - for name, func in _DECORATED_TABLES.iteritems(): + for name, func in iteritems(_DECORATED_TABLES): logger.debug("reinject decorated table %s" % name) orca.add_table(name, func) - for column_key, args in _DECORATED_COLUMNS.iteritems(): + for column_key, args in iteritems(_DECORATED_COLUMNS): table_name, column_name = column_key logger.debug("reinject decorated column %s.%s" % (table_name, column_name)) orca.add_column(table_name, column_name, args['func'], cache=args['cache']) - for name, args in _DECORATED_INJECTABLES.iteritems(): + for name, args in iteritems(_DECORATED_INJECTABLES): logger.debug("reinject decorated injectable %s" % name) orca.add_injectable(name, args['func'], cache=args['cache']) @@ -173,5 +176,5 @@ def get_step_arg(arg_name, default=_NO_DEFAULT): def dump_state(): - print "_DECORATED_STEPS", _DECORATED_STEPS.keys() - print "orca._STEPS", orca.orca._STEPS.keys() + print("_DECORATED_STEPS", list(_DECORATED_STEPS.keys())) + print("orca._STEPS", list(orca.orca._STEPS.keys())) diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 995670d6e..9ee7e60cb 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -1,6 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import absolute_import +from __future__ import division + +from builtins import range + import logging from math import ceil @@ -16,7 +21,7 @@ from .interaction_simulate import eval_interaction_utilities -import pipeline +from . import pipeline logger = logging.getLogger(__name__) diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index 95d49f4f5..bf3d9bd33 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -1,3 +1,4 @@ +from __future__ import division # ActivitySim # See full license in LICENSE.txt. diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 8ff76bd62..bff1258bb 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import zip +from builtins import str import logging import numpy as np diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 10b49b160..a10888877 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -2,14 +2,16 @@ # See full license in LICENSE.txt. from __future__ import division +from __future__ import absolute_import +from builtins import object import logging import numpy as np import pandas as pd -import tracing -import pipeline +from . import tracing +from . import pipeline logger = logging.getLogger(__name__) diff --git a/activitysim/core/orca/utils/testing.py b/activitysim/core/orca/utils/testing.py index 448bed40e..4beb0fe01 100644 --- a/activitysim/core/orca/utils/testing.py +++ b/activitysim/core/orca/utils/testing.py @@ -6,6 +6,9 @@ Utilities used in testing of Orca. """ + +from future.utils import iteritems + import numpy as np import numpy.testing as npt import pandas as pd @@ -42,7 +45,7 @@ def assert_frames_equal(actual, expected, use_close=False): act_row = actual.loc[i] - for j, exp_item in exp_row.iteritems(): + for j, exp_item in iteritems(exp_row): assert j in act_row.index, \ 'Expected column {!r} not found.'.format(j) diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index c2840bf63..8ad2c62c7 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,6 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import + +from future.utils import iteritems + +from builtins import next +from builtins import map +from builtins import object import os import datetime as dt @@ -8,14 +17,14 @@ from . import orca import logging -import inject -import config -from util import memory_info -from util import df_size +from . import inject +from . import config +from .util import memory_info +from .util import df_size -import random -import tracing -from tracing import print_elapsed_time +from . import random +from . import tracing +from .tracing import print_elapsed_time logger = logging.getLogger(__name__) @@ -76,8 +85,8 @@ def close_on_exit(file, name): def close_open_files(): - for name, file in _PIPELINE.open_files.iteritems(): - print "Closing %s" % name + for name, file in iteritems(_PIPELINE.open_files): + print("Closing %s" % name) file.close() _PIPELINE.open_files.clear() @@ -307,7 +316,7 @@ def checkpointed_tables(): Return a list of the names of all checkpointed tables """ - return [name for name, checkpoint_name in _PIPELINE.last_checkpoint.iteritems() + return [name for name, checkpoint_name in iteritems(_PIPELINE.last_checkpoint) if checkpoint_name and name not in NON_TABLE_COLUMNS] @@ -346,7 +355,7 @@ def load_checkpoint(checkpoint_name): # drop tables with empty names for checkpoint in checkpoints: - for key in checkpoint.keys(): + for key in list(checkpoint.keys()): if key not in NON_TABLE_COLUMNS and not checkpoint[key]: del checkpoint[key] @@ -392,7 +401,7 @@ def split_arg(s, sep, default=''): split str s in two at first sep, returning empty string as second result if no sep """ r = s.split(sep, 2) - r = map(str.strip, r) + r = list(map(str.strip, r)) arg = r[0] diff --git a/activitysim/core/random.py b/activitysim/core/random.py index c2bd84e39..53829862a 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -1,14 +1,17 @@ -import collections +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import absolute_import + +from builtins import range +from builtins import object +import logging import numpy as np import pandas as pd -import inject from .tracing import print_elapsed_time -import logging - logger = logging.getLogger(__name__) # one more than 0xFFFFFFFF so we can wrap using: int64 % _MAX_SEED @@ -429,7 +432,7 @@ def set_base_seed(self, seed=None): if self.step_name is not None or self.channels: raise RuntimeError("Can only call set_base_seed before the first step.") - assert len(self.channels.keys()) == 0 + assert len(list(self.channels.keys())) == 0 if seed is None: self.base_seed = np.random.RandomState().randint(_MAX_SEED) diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 8e870d018..459b92b0f 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -2,11 +2,14 @@ # See full license in LICENSE.txt. from __future__ import print_function +from __future__ import division + +from future.utils import listvalues +from builtins import range import sys import os import logging -import time from collections import OrderedDict import numpy as np @@ -48,7 +51,7 @@ def uniquify_spec_index(spec): # bug prev_index_name = spec.index.name - spec.index = dict.keys() + spec.index = list(dict.keys()) spec.index.name = prev_index_name assert spec.index.is_unique @@ -271,7 +274,7 @@ def set_skim_wrapper_targets(df, skims): elif isinstance(skims, dict): # it it is a dict, then check for known types, ignore anything we don't recognize as a skim # (this allows putting skim column names in same dict as skims for use in locals_dicts) - for skim in skims.values(): + for skim in listvalues(skims): if isinstance(skim, SkimDictWrapper) or isinstance(skim, SkimStackWrapper): skim.set_df(df) else: @@ -409,7 +412,7 @@ def compute_base_probabilities(nested_probabilities, nests, spec): ---------- nested_probabilities : pandas.DataFrame dataframe with the nested probabilities for nest leafs and nodes - nest_spec : dict + nests : dict Nest tree dict from the model spec yaml file spec : pandas.Dataframe simple simulate spec so we can return columns in appropriate order diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index b2b1276ce..50160dac8 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -1,7 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from future.utils import iteritems +from future.utils import listvalues +from builtins import range +from builtins import object import logging from collections import OrderedDict @@ -28,14 +32,14 @@ def set_offset_list(self, offset_list): # for performance, check if this is a simple int-based series first_offset = offset_list[0] - if (offset_list == range(first_offset, len(offset_list)+first_offset)): + if (offset_list == list(range(first_offset, len(offset_list)+first_offset))): offset_int = -1 * first_offset # print "set_offset_list substituting offset_int of %s" % offset_int self.set_offset_int(offset_int) return if self.offset_series is None: - self.offset_series = pd.Series(data=range(len(offset_list)), index=offset_list) + self.offset_series = pd.Series(data=list(range(len(offset_list))), index=offset_list) else: # make sure it offsets are the same assert (offset_list == self.offset_series.index).all() @@ -43,7 +47,7 @@ def set_offset_list(self, offset_list): def set_offset_int(self, offset_int): # should be some kind of integer - assert long(offset_int) == offset_int + assert int(offset_int) == offset_int assert self.offset_series is None if self.offset_int is None: @@ -64,7 +68,7 @@ def map(self, zone_ids): elif self.offset_int: # should be some kind of integer - assert long(self.offset_int) == self.offset_int + assert int(self.offset_int) == self.offset_int assert (self.offset_series is None) offsets = zone_ids + self.offset_int else: @@ -318,7 +322,7 @@ def __init__(self, skim_dict): # DISTWALK: 0, # DRV_COM_WLK_BOARDS: 0, ... key1_block_offsets = skim_dict.skim_info['key1_block_offsets'] - self.key1_blocks = {k: v[0] for k, v in key1_block_offsets.iteritems()} + self.key1_blocks = {k: v[0] for k, v in iteritems(key1_block_offsets)} # - skim_dim3 dict maps key1 to dict of key2 absolute offsets into block # DRV_COM_WLK_BOARDS: {'MD': 4, 'AM': 3, 'PM': 5}, ... @@ -340,7 +344,7 @@ def __init__(self, skim_dict): logger.info("SkimStack.__init__ loaded %s keys with %s total skims" % (len(self.skim_dim3), - sum([len(d) for d in self.skim_dim3.values()]))) + sum([len(d) for d in listvalues(self.skim_dim3)]))) self.usage = set() diff --git a/activitysim/core/test/test_assign.py b/activitysim/core/test/test_assign.py index f073bf236..0db77e1e4 100644 --- a/activitysim/core/test/test_assign.py +++ b/activitysim/core/test/test_assign.py @@ -1,15 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. -import os.path +from __future__ import print_function +from builtins import str +import os.path import logging import logging.config -import numpy.testing as npt import numpy as np import pandas as pd -import pandas.util.testing as pdt import pytest from .. import assign @@ -75,7 +75,7 @@ def test_assign_variables(capsys, spec_name, data): results, trace_results, trace_assigned_locals \ = assign.assign_variables(spec, data, locals_d, trace_rows=None) - print results + print(results) assert list(results.columns) == ['target1', 'target2', 'target3'] assert list(results.target1) == [True, False, False] @@ -93,7 +93,7 @@ def test_assign_variables(capsys, spec_name, data): assert list(results.target3) == [530, 530, 550] # should assign trace_results for second row in data - print trace_results + print(trace_results) assert trace_results is not None assert '_scalar' in trace_results.columns @@ -104,7 +104,7 @@ def test_assign_variables(capsys, spec_name, data): assert list(trace_results['_temp']) == [9] assert list(trace_results['target3']) == [530] - print "trace_assigned_locals", trace_assigned_locals + print("trace_assigned_locals", trace_assigned_locals) assert trace_assigned_locals['_DF_COL_NAME'] == 'thing2' # shouldn't have been changed even though it was a target @@ -128,7 +128,7 @@ def test_assign_variables_aliased(capsys, data): = assign.assign_variables(spec, data, locals_d, df_alias='aliased_df', trace_rows=trace_rows) - print results + print(results) assert list(results.columns) == ['target1', 'target2', 'target3'] assert list(results.target1) == [True, False, False] @@ -136,7 +136,7 @@ def test_assign_variables_aliased(capsys, data): assert list(results.target3) == [530, 530, 550] # should assign trace_results for second row in data - print trace_results + print(trace_results) assert trace_results is not None assert '_scalar' in trace_results.columns @@ -177,7 +177,7 @@ def test_assign_variables_failing(capsys, data): out, err = capsys.readouterr() # don't consume output - print out + print(out) # undefined variable should raise error assert "'undefined_variable' is not defined" in str(excinfo.value) diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index 366760f56..5486b6a05 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -1,12 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function +from builtins import str import os -import tempfile -import numpy as np import pytest -import yaml from .. import inject @@ -33,7 +32,7 @@ def test_defaults(): with pytest.raises(RuntimeError) as excinfo: output_dir = inject.get_injectable("output_dir") - print "output_dir", output_dir + print("output_dir", output_dir) assert "directory does not exist" in str(excinfo.value) configs_dir = os.path.join(os.path.dirname(__file__), 'configs_test_defaults') diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index c23d24acb..991ccea29 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -1,6 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str import os.path import numpy as np diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index b851c637e..2842a742d 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -1,11 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +from builtins import str import os -import tempfile import logging - -import pandas.util.testing as pdt import pytest from activitysim.core import tracing @@ -73,7 +73,7 @@ def test_pipeline_run(): pipeline.run(models=_MODELS, resume_after=None) checkpoints = pipeline.get_checkpoints() - print "checkpoints\n", checkpoints + print("checkpoints\n", checkpoints) c2 = pipeline.get_table("table2").c2 @@ -121,7 +121,7 @@ def test_pipeline_checkpoint_drop(): pipeline.run(models=_MODELS, resume_after=None) checkpoints = pipeline.get_checkpoints() - print "checkpoints\n", checkpoints + print("checkpoints\n", checkpoints) pipeline.get_table("table1") diff --git a/activitysim/core/test/test_random.py b/activitysim/core/test/test_random.py index 0ef475d64..bc38d5561 100644 --- a/activitysim/core/test/test_random.py +++ b/activitysim/core/test/test_random.py @@ -1,14 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +from builtins import str import numpy as np import pandas as pd import numpy.testing as npt -import pandas.util.testing as pdt import pytest from activitysim.core import random -from activitysim.core import pipeline def test_basic(): @@ -59,7 +60,7 @@ def test_channel(): rands = rng.random_for_df(persons) - print "rands", np.asanyarray(rands).flatten() + print("rands", np.asanyarray(rands).flatten()) assert rands.shape == (5, 1) test1_expected_rands = [0.9060891, 0.4576382, 0.2154094, 0.2801035, 0.6196645] @@ -105,7 +106,7 @@ def test_channel(): rands = rng.random_for_df(persons) - print "rands", np.asanyarray(rands).flatten() + print("rands", np.asanyarray(rands).flatten()) npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands) rands = rng.random_for_df(persons) diff --git a/activitysim/core/test/test_timetable.py b/activitysim/core/test/test_timetable.py index ae4e2bc09..5d3262ed4 100644 --- a/activitysim/core/test/test_timetable.py +++ b/activitysim/core/test/test_timetable.py @@ -1,9 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import range import numpy as np import pandas as pd -import numpy.testing as npt import pandas.util.testing as pdt import pytest @@ -14,7 +14,7 @@ def persons(): df = pd.DataFrame( - index=range(6) + index=list(range(6)) ) return df @@ -70,8 +70,8 @@ def test_basic(persons, tdd_alts): num_alts = len(tdd_alts.index) num_persons = len(persons.index) - person_ids = pd.Series(range(num_persons)*num_alts) - tdds = pd.Series(np.repeat(range(num_alts), num_persons)) + person_ids = pd.Series(list(range(num_persons))*num_alts) + tdds = pd.Series(np.repeat(list(range(num_alts)), num_persons)) assert timetable.tour_available(person_ids, tdds).all() diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index 285df8862..c33aa9dc1 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -1,9 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + +from builtins import str import os.path import logging - import pytest import pandas as pd @@ -45,7 +47,7 @@ def test_config_logger(capsys): assert len(file_handlers) == 1 asim_logger_baseFilename = file_handlers[0].baseFilename - print "handlers:", logger.handlers + print("handlers:", logger.handlers) logger.info('test_config_logger') logger.info('log_info') @@ -54,7 +56,7 @@ def test_config_logger(capsys): out, err = capsys.readouterr() # don't consume output - print out + print(out) assert "could not find conf file" not in out assert 'log_warn1' in out @@ -67,7 +69,7 @@ def test_config_logger(capsys): with open(asim_logger_baseFilename, 'r') as content_file: content = content_file.read() - print content + print(content) assert 'log_warn1' in content assert 'log_warn2' not in content @@ -78,12 +80,12 @@ def test_print_summary(capsys): tracing.config_logger() - tracing.print_summary('label', df=None, describe=False, value_counts=False) + tracing.print_summary('label', df=pd.DataFrame(), describe=False, value_counts=False) out, err = capsys.readouterr() # don't consume output - print out + print(out) assert 'print_summary neither value_counts nor describe' in out @@ -156,7 +158,7 @@ def test_register_tours(capsys): tracing.register_traceable_table('tours', tours_df) out, err = capsys.readouterr() - print out # don't consume output + print(out) # don't consume output # should be tracing tour with tour_id 3 assert inject.get_injectable('trace_tours') == [12] @@ -175,7 +177,7 @@ def test_write_csv(capsys): out, err = capsys.readouterr() - print out # don't consume output + print(out) # don't consume output assert "unexpected type" in out @@ -229,7 +231,7 @@ def test_basic(capsys): out, err = capsys.readouterr() # don't consume output - print out + print(out) assert 'log_warn' in out assert 'log_info' in out diff --git a/activitysim/core/test/test_util.py b/activitysim/core/test/test_util.py index 188d85e9f..1c38c14f2 100644 --- a/activitysim/core/test/test_util.py +++ b/activitysim/core/test/test_util.py @@ -1,6 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str import numpy as np import pandas as pd import pandas.util.testing as pdt diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index 370e46242..4399d3149 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -1,6 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. +from builtins import str +from builtins import range +from builtins import object import logging import numpy as np @@ -79,7 +82,7 @@ def tour_map(persons, tours, tdd_alts, persons_id_col='person_id'): agenda = agenda.reshape(n_persons, n_periods) scheduled = np.zeros_like(agenda, dtype=int) - row_ix_map = pd.Series(range(n_persons), index=persons.index) + row_ix_map = pd.Series(list(range(n_persons)), index=persons.index) # construct with strings so we can create runs of strings using char * int w_strings = [ @@ -149,7 +152,7 @@ def create_timetable_windows(rows, tdd_alts): assert rows.index is not None # pad windows at both ends of day - windows = range(tdd_alts.start.min() - 1, tdd_alts.end.max() + 2) + windows = list(range(tdd_alts.start.min() - 1, tdd_alts.end.max() + 2)) # hdf5 store converts these to strs, se we conform window_cols = [str(w) for w in windows] @@ -192,10 +195,10 @@ def __init__(self, windows_df, tdd_alts_df, table_name=None): self.windows = self.windows_df.values # series to map window row index value to window row's ordinal index - self.window_row_ix = pd.Series(range(len(windows_df.index)), index=windows_df.index) + self.window_row_ix = pd.Series(list(range(len(windows_df.index))), index=windows_df.index) int_time_periods = [int(c) for c in windows_df.columns.values] - self.time_ix = pd.Series(range(len(windows_df.columns)), index=int_time_periods) + self.time_ix = pd.Series(list(range(len(windows_df.columns))), index=int_time_periods) # - pre-compute window state footprints for every tdd_alt min_period = min(int_time_periods) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index bd11fc8a5..aecbfb8b8 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -1,6 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from builtins import next +from builtins import str +from builtins import range + import os import logging import logging.config @@ -15,7 +23,7 @@ from activitysim.core import inject -import config +from . import config # Configurations @@ -76,7 +84,7 @@ def delete_output_files(file_type, ignore=None): if ignore: ignore = [os.path.realpath(p) for p in ignore] - print "delete_output_files ignoring", ignore + print("delete_output_files ignoring", ignore) logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) @@ -136,7 +144,7 @@ def config_logger(basic=False): if log_config_file: logger.info("Read logging configuration from: %s" % log_config_file) else: - print "Configured logging using basicConfig" + print("Configured logging using basicConfig") logger.info("Configured logging using basicConfig") @@ -244,7 +252,7 @@ def register_traceable_table(table_name, df): # update traceable_table_refs with this traceable_table's ref_con if idx_name not in traceable_table_refs: traceable_table_refs[idx_name] = table_name - print "adding table %s.%s to traceable_table_refs" % (table_name, idx_name) + print("adding table %s.%s to traceable_table_refs" % (table_name, idx_name)) inject.add_injectable('traceable_table_refs', traceable_table_refs) # update the list of trace_ids for this table @@ -594,7 +602,7 @@ def interaction_trace_rows(interaction_df, choosers, sample_size=None): slicer_column_name = 'person_id' targets = inject.get_injectable('trace_persons', []) else: - print choosers.columns + print(choosers.columns) raise RuntimeError("interaction_trace_rows don't know how to slice index '%s'" % choosers.index.name) diff --git a/activitysim/core/util.py b/activitysim/core/util.py index 697c6d362..c60e9aaf2 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -1,3 +1,6 @@ +from __future__ import division +from builtins import zip + import os import psutil import gc diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 4292f6412..dcec90703 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -342,7 +342,7 @@ and the model is calculating and adding the mode choice logsums using the logsum :: - for school_type, school_type_id in SCHOOL_TYPE_ID.iteritems(): + for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): segment = 'university' if school_type == 'university' else 'school' logsum_spec = get_segment_and_unstack(omnibus_logsum_spec, segment) diff --git a/example/simulation.py b/example/simulation.py index 89b5e8ae5..69602265d 100644 --- a/example/simulation.py +++ b/example/simulation.py @@ -1,9 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function + import logging +# activitysim.abm imported for its side-effects (dependency injection) from activitysim import abm + from activitysim.core import tracing from activitysim.core.config import handle_standard_args from activitysim.core.config import setting @@ -22,17 +26,15 @@ def run(): tracing.config_logger() tracing.delete_csv_files() - MODELS = setting('models') - # If you provide a resume_after argument to pipeline.run # the pipeline manager will attempt to load checkpointed tables from the checkpoint store # and resume pipeline processing on the next submodel step after the specified checkpoint resume_after = setting('resume_after', None) if resume_after: - print "resume_after", resume_after + print("resume_after", resume_after) - pipeline.run(models=MODELS, resume_after=resume_after) + pipeline.run(models=setting('models'), resume_after=resume_after) # tables will no longer be available after pipeline is closed pipeline.close_pipeline() diff --git a/example_mp/tasks.py b/example_mp/tasks.py index 4886d0898..af9255faf 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -2,7 +2,11 @@ # See full license in LICENSE.txt. from __future__ import print_function +from __future__ import division +from builtins import zip +from builtins import next +from builtins import range from future.utils import iteritems import sys @@ -24,7 +28,9 @@ from activitysim.core.config import setting from activitysim.core.config import handle_standard_args +# activitysim.abm imported for its side-effects (dependency injection) from activitysim import abm + from activitysim.abm.tables.skims import get_skim_info from activitysim.abm.tables.skims import buffer_for_skims from activitysim.abm.tables.skims import load_skims @@ -70,7 +76,7 @@ def build_slice_rules(slice_info, tables): if primary_slicer not in tables: raise RuntimeError("primary slice table '%s' not in pipeline" % primary_slicer) - logger.debug("build_slice_rules tables %s", tables.keys()) + logger.debug("build_slice_rules tables %s", list(tables.keys())) logger.debug("build_slice_rules primary_slicer %s", primary_slicer) logger.debug("build_slice_rules slicer_table_names %s", slicer_table_names) logger.debug("build_slice_rules slicer_table_exceptions %s", slicer_table_exceptions) @@ -154,7 +160,7 @@ def apportion_pipeline(sub_job_names, slice_info): # keep only the last row of checkpoints and patch the last checkpoint name checkpoints_df = checkpoints_df.tail(1).copy() - checkpoints_df[tables.keys()] = checkpoint_name + checkpoints_df[list(tables.keys())] = checkpoint_name # build slice rules for loaded tables slice_rules = build_slice_rules(slice_info, tables) @@ -184,7 +190,7 @@ def apportion_pipeline(sub_job_names, slice_info): # slice primary apportion table by num_sub_jobs strides # this hopefully yields a more random distribution # (e.g.) households are ordered by size in input store - primary_df = df[np.asanyarray(range(df.shape[0])) % num_sub_jobs == i] + primary_df = df[np.asanyarray(list(range(df.shape[0]))) % num_sub_jobs == i] sliced_tables[table_name] = primary_df elif rule['slice_by'] == 'index': # slice a table with same index name as a known slicer @@ -416,7 +422,7 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name queues.append(q) def log_queued_messages(): - for i, process, queue in zip(range(num_simulations), procs, queues): + for i, process, queue in zip(list(range(num_simulations)), procs, queues): while not queue.empty(): msg = queue.get(block=False) logger.info("%s %s : %s", @@ -541,10 +547,10 @@ def get_resume_journal(run_list): raise RuntimeError("empty journal for resume_after '%s'" % resume_after) if resume_after == '_': - resume_step_name = previous_journal.keys()[-1] + resume_step_name = list(previous_journal.keys())[-1] else: - previous_steps = previous_journal.keys() + previous_steps = list(previous_journal.keys()) # run_list step resume_after is in resume_step_name = next((step['name'] for step in run_list['multiprocess_steps'] @@ -574,8 +580,9 @@ def get_resume_journal(run_list): previous_journal[resume_step_name]['coalesce'] = None multiprocess_step_names = [step['name'] for step in run_list['multiprocess_steps']] - if previous_journal.keys() != multiprocess_step_names[:len(previous_journal)]: - raise RuntimeError("last run steps don't match run list: %s" % previous_journal.keys()) + if list(previous_journal.keys()) != multiprocess_step_names[:len(previous_journal)]: + raise RuntimeError("last run steps don't match run list: %s" % + list(previous_journal.keys())) return previous_journal @@ -661,7 +668,7 @@ def get_run_list(): chunk_size = step.get('chunk_size', None) if chunk_size is None: if global_chunk_size > 0 and num_processes > 1: - chunk_size = int(round(global_chunk_size / float(num_processes))) + chunk_size = int(round(global_chunk_size / num_processes)) chunk_size = max(chunk_size, 1) else: chunk_size = global_chunk_size @@ -748,7 +755,7 @@ def print_run_list(run_list, output_file=None): for step in run_list['multiprocess_steps']: print(" step:", step['name'], file=output_file) for k in step: - if isinstance(step[k], (list, )): + if isinstance(step[k], list): print(" ", k, file=output_file) for v in step[k]: print(" -", v, file=output_file) @@ -799,7 +806,7 @@ def read_journal(file_name=None): def save_journal(journal, file_name=None): with open(journal_file_path(file_name), 'w') as f: - journal = [step for step in journal.values()] + journal = [step for step in list(journal.values())] yaml.dump(journal, f) From 5c769a017177ea1d8ec372728cb33448bb374fb9 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 16 Oct 2018 03:32:46 -0400 Subject: [PATCH 024/122] p3.5 --- activitysim/abm/__init__.py | 8 +- activitysim/abm/misc.py | 8 +- activitysim/abm/models/accessibility.py | 8 +- activitysim/abm/models/initialize.py | 2 +- .../abm/models/joint_tour_composition.py | 7 +- .../abm/models/joint_tour_frequency.py | 2 +- .../abm/models/joint_tour_participation.py | 4 +- .../abm/models/mandatory_tour_frequency.py | 2 +- .../models/non_mandatory_tour_frequency.py | 10 +- activitysim/abm/models/trip_destination.py | 19 ++-- .../models/trip_purpose_and_destination.py | 7 +- activitysim/abm/models/trip_scheduling.py | 8 +- .../abm/models/util/tour_destination.py | 6 +- .../test/configs/annotate_households_cdap.csv | 2 +- activitysim/abm/test/output/.gitignore | 1 + activitysim/abm/test/test_misc.py | 1 + activitysim/abm/test/test_pipeline.py | 100 ++++++++---------- activitysim/core/assign.py | 2 +- activitysim/core/chunk.py | 2 +- activitysim/core/config.py | 7 +- activitysim/core/inject.py | 2 +- activitysim/core/interaction_simulate.py | 4 +- activitysim/core/orca/orca.py | 6 +- activitysim/core/orca/tests/test_orca.py | 4 +- activitysim/core/orca/utils/logutil.py | 4 +- activitysim/core/pipeline.py | 23 ++-- activitysim/core/random.py | 32 ++++-- activitysim/core/simulate.py | 8 +- activitysim/core/steps/output.py | 93 ++++++++-------- .../core/test/configs/custom_logging.yaml | 16 +-- activitysim/core/test/configs/logging.yaml | 16 +-- activitysim/core/test/extensions/__init__.py | 3 +- activitysim/core/test/extensions/steps.py | 5 + activitysim/core/test/test_assign.py | 9 +- activitysim/core/test/test_inject_defaults.py | 9 +- activitysim/core/test/test_pipeline.py | 8 +- activitysim/core/test/test_random.py | 18 ++-- activitysim/core/test/test_tracing.py | 16 ++- activitysim/core/tracing.py | 21 ++-- activitysim/core/util.py | 8 +- example/configs/logging.yaml | 14 +-- example/configs/settings.yaml | 2 +- example_mp/__init__.py | 0 example_mp/configs/logging.yaml | 20 ++-- example_mp/simulation.py | 17 ++- example_mp/tasks.py | 35 +++--- example_multi/extensions/models.py | 2 +- setup.py | 1 + 48 files changed, 342 insertions(+), 260 deletions(-) create mode 100644 example_mp/__init__.py diff --git a/activitysim/abm/__init__.py b/activitysim/abm/__init__.py index 1b837cf3c..7cb1d16ba 100644 --- a/activitysim/abm/__init__.py +++ b/activitysim/abm/__init__.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. -import misc -import tables -import models +from __future__ import absolute_import + +from . import misc +from . import tables +from . import models diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index 4f34bce23..f96370b83 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -1,6 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, unicode_literals) + import os import warnings import logging @@ -13,7 +15,7 @@ from activitysim.core import inject # FIXME -warnings.filterwarnings('ignore', category=pd.io.pytables.PerformanceWarning) +# warnings.filterwarnings('ignore', category=pd.io.pytables.PerformanceWarning) pd.options.mode.chained_assignment = None logger = logging.getLogger(__name__) @@ -64,7 +66,7 @@ def trace_hh_id(settings): id = settings.get('trace_hh_id', None) if id and not isinstance(id, int): - logger.warn("setting trace_hh_id is wrong type, should be an int, but was %s" % type(id)) + logger.warning("setting trace_hh_id is wrong type, should be an int, but was %s" % type(id)) id = None return id @@ -76,7 +78,7 @@ def trace_od(settings): od = settings.get('trace_od', None) if od and not (isinstance(od, list) and len(od) == 2 and all(isinstance(x, int) for x in od)): - logger.warn("setting trace_od is wrong type, should be a list of length 2, but was %s" % od) + logger.warning("setting trace_od should be a list of length 2, but was %s" % od) od = None return od diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index f0f4adf28..1bf42d2da 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -1,10 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import range -from builtins import object +from __future__ import (absolute_import, division, print_function, unicode_literals) +from builtins import * + import logging -import os import pandas as pd import numpy as np @@ -183,7 +183,7 @@ def compute_accessibility(accessibility, skim_dict, land_use, trace_od): if trace_od: if not trace_od_rows.any(): - logger.warn("trace_od not found origin = %s, dest = %s" % (trace_orig, trace_dest)) + logger.warning("trace_od not found origin = %s, dest = %s" % (trace_orig, trace_dest)) else: # add OD columns to trace results diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index ae33fa64b..38eee2601 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -28,7 +28,7 @@ def annotate_tables(model_settings, trace_label): annotate_tables = model_settings.get('annotate_tables', []) if not annotate_tables: - logger.warn("annotate_tables setting is empty - nothing to do!") + logger.warning("annotate_tables setting is empty - nothing to do!") t0 = tracing.print_elapsed_time() diff --git a/activitysim/abm/models/joint_tour_composition.py b/activitysim/abm/models/joint_tour_composition.py index 4c5faec91..58f73d0da 100644 --- a/activitysim/abm/models/joint_tour_composition.py +++ b/activitysim/abm/models/joint_tour_composition.py @@ -25,7 +25,7 @@ def add_null_results(trace_label, tours): logger.info("Skipping %s: add_null_results" % trace_label) - tours['composition'] = np.nan + tours['composition'] = '' pipeline.replace_table("tours", tours) @@ -94,10 +94,11 @@ def joint_tour_composition( # convert indexes to alternative names choices = pd.Series(model_spec.columns[choices.values], index=choices.index) - # add composition column to tours + # add composition column to tours for tracing joint_tours['composition'] = choices - assign_in_place(tours, joint_tours[['composition']]) + # reindex since we ran model on a subset of households + tours['composition'] = choices.reindex(tours.index).fillna('').astype(str) pipeline.replace_table("tours", tours) tracing.print_summary('joint_tour_composition', joint_tours.composition, diff --git a/activitysim/abm/models/joint_tour_frequency.py b/activitysim/abm/models/joint_tour_frequency.py index 55b8cc48f..a7814f882 100644 --- a/activitysim/abm/models/joint_tour_frequency.py +++ b/activitysim/abm/models/joint_tour_frequency.py @@ -103,7 +103,7 @@ def joint_tour_frequency( # - annotate households # add joint_tour_frequency and num_hh_joint_tours columns to households # reindex since we ran model on a subset of households - households['joint_tour_frequency'] = choices.reindex(households.index) + households['joint_tour_frequency'] = choices.reindex(households.index).fillna('').astype(str) households['num_hh_joint_tours'] = joint_tours.groupby('household_id').size().\ reindex(households.index).fillna(0).astype(np.int8) diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 5691be088..c8f029989 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -55,7 +55,7 @@ def joint_tour_participation_candidates(joint_tours, persons_merged): MAX_PNUM = 100 if candidates.PNUM.max() > MAX_PNUM: # if this happens, channel random seeds will overlap at MAX_PNUM (not probably a big deal) - logger.warn("max persons.PNUM (%s) > MAX_PNUM (%s)" % (candidates.PNUM.max(), MAX_PNUM)) + logger.warning("max persons.PNUM (%s) > MAX_PNUM (%s)" % (candidates.PNUM.max(), MAX_PNUM)) candidates['participant_id'] = (candidates[joint_tours.index.name] * MAX_PNUM) + candidates.PNUM candidates.set_index('participant_id', drop=True, inplace=True, verify_integrity=True) @@ -154,7 +154,7 @@ def participants_chooser(probs, choosers, spec, trace_label): iter += 1 if iter > MAX_ITERATIONS: - logger.warn('%s max iterations exceeded (%s).' % (trace_label, MAX_ITERATIONS)) + logger.warning('%s max iterations exceeded (%s).' % (trace_label, MAX_ITERATIONS)) diagnostic_cols = ['tour_id', 'household_id', 'composition', 'adult'] unsatisfied_candidates = candidates[diagnostic_cols].join(probs) tracing.write_csv(unsatisfied_candidates, diff --git a/activitysim/abm/models/mandatory_tour_frequency.py b/activitysim/abm/models/mandatory_tour_frequency.py index 8f5db1564..51eb40c7f 100644 --- a/activitysim/abm/models/mandatory_tour_frequency.py +++ b/activitysim/abm/models/mandatory_tour_frequency.py @@ -114,7 +114,7 @@ def mandatory_tour_frequency(persons_merged, persons = inject.get_table('persons').to_frame() # need to reindex as we only handled persons with cdap_activity == 'M' - persons['mandatory_tour_frequency'] = choices.reindex(persons.index) + persons['mandatory_tour_frequency'] = choices.reindex(persons.index).fillna('').astype(str) expressions.assign_columns( df=persons, diff --git a/activitysim/abm/models/non_mandatory_tour_frequency.py b/activitysim/abm/models/non_mandatory_tour_frequency.py index 06e4f2f3d..6a7b0bb49 100644 --- a/activitysim/abm/models/non_mandatory_tour_frequency.py +++ b/activitysim/abm/models/non_mandatory_tour_frequency.py @@ -4,6 +4,7 @@ import os import logging +import numpy as np import pandas as pd from activitysim.core.interaction_simulate import interaction_simulate @@ -47,8 +48,11 @@ def non_mandatory_tour_frequency(persons, persons_merged, choosers = persons_merged.to_frame() # FIXME kind of tacky both that we know to add this here and del it below + # 'tot_tours' is used in model_spec expressions alternatives['tot_tours'] = alternatives.sum(axis=1) + no_tours_alt = list((alternatives.sum(axis=1) == 0).values).index(True) + # - preprocessor preprocessor_settings = model_settings.get('preprocessor', None) if preprocessor_settings: @@ -103,7 +107,8 @@ def non_mandatory_tour_frequency(persons, persons_merged, persons = persons.to_frame() # need to reindex as we only handled persons with cdap_activity in ['M', 'N'] - persons['non_mandatory_tour_frequency'] = choices.reindex(persons.index) + persons['non_mandatory_tour_frequency'] = \ + choices.reindex(persons.index).fillna(no_tours_alt).astype(np.int8) """ We have now generated non-mandatory tours, but they are attributes of the person table @@ -112,11 +117,12 @@ def non_mandatory_tour_frequency(persons, persons_merged, """ del alternatives['tot_tours'] # del tot_tours column we added above non_mandatory_tours = process_non_mandatory_tours( - persons[~persons.mandatory_tour_frequency.isnull()], + persons[persons.mandatory_tour_frequency != no_tours_alt], alternatives, ) tours = pipeline.extend_table("tours", non_mandatory_tours) + tracing.register_traceable_table('tours', tours) pipeline.get_rn_generator().add_channel(non_mandatory_tours, 'tours') diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index f02250727..ee2a3ce2f 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -306,8 +306,9 @@ def choose_trip_destination( dropped_trips = ~trips.index.isin(destination_sample.index.unique()) if dropped_trips.any(): - logger.warn("%s trip_destination_ample %s trips without viable destination alternatives" - % (trace_label, dropped_trips.sum())) + logger.warning("%s trip_destination_ample %s trips " + "without viable destination alternatives" % + (trace_label, dropped_trips.sum())) trips = trips[~dropped_trips] t0 = print_elapsed_time("%s.trip_destination_sample" % trace_label, t0) @@ -340,8 +341,9 @@ def choose_trip_destination( dropped_trips = ~trips.index.isin(destinations.index) if dropped_trips.any(): - logger.warn("%s trip_destination_simulate %s trips without viable destination alternatives" - % (trace_label, dropped_trips.sum())) + logger.warning("%s trip_destination_simulate %s trips " + "without viable destination alternatives" % + (trace_label, dropped_trips.sum())) t0 = print_elapsed_time("%s.trip_destination_simulate" % trace_label, t0) @@ -503,8 +505,8 @@ def run_trip_destination( failed_trip_ids = nth_trips.index.difference(destinations.index) if failed_trip_ids.any(): - logger.warn("%s sidelining %s trips without viable destination alternatives" - % (nth_trace_label, failed_trip_ids.shape[0])) + logger.warning("%s sidelining %s trips without viable destination alternatives" % + (nth_trace_label, failed_trip_ids.shape[0])) next_trip_ids = nth_trips.next_trip_id.reindex(failed_trip_ids) trips.loc[failed_trip_ids, 'failed'] = True trips.loc[failed_trip_ids, 'destination'] = -1 @@ -549,7 +551,7 @@ def trip_destination( trace_label) if trips_df.failed.any(): - logger.warn("%s %s failed trips" % (trace_label, trips_df.failed.sum())) + logger.warning("%s %s failed trips" % (trace_label, trips_df.failed.sum())) file_name = "%s_failed_trips" % trace_label logger.info("writing failed trips to %s" % file_name) tracing.write_csv(trips_df[trips_df.failed], file_name=file_name, transpose=False) @@ -557,7 +559,8 @@ def trip_destination( if CLEANUP: trips_df = cleanup_failed_trips(trips_df) elif trips_df.failed.any(): - logger.warn("%s keeping %s sidelined failed trips" % (trace_label, trips_df.failed.sum())) + logger.warning("%s keeping %s sidelined failed trips" % + (trace_label, trips_df.failed.sum())) pipeline.replace_table("trips", trips_df) diff --git a/activitysim/abm/models/trip_purpose_and_destination.py b/activitysim/abm/models/trip_purpose_and_destination.py index 8ab678f80..328cdaa5d 100644 --- a/activitysim/abm/models/trip_purpose_and_destination.py +++ b/activitysim/abm/models/trip_purpose_and_destination.py @@ -109,7 +109,7 @@ def trip_purpose_and_destination( results.append(trips_df[RESULT_COLUMNS]) break - logger.warn("%s %s failed trips in iteration %s" % (trace_label, num_failed_trips, i)) + logger.warning("%s %s failed trips in iteration %s" % (trace_label, num_failed_trips, i)) file_name = "%s_i%s_failed_trips" % (trace_label, i) logger.info("writing failed trips to %s" % file_name) tracing.write_csv(trips_df[trips_df.failed], file_name=file_name, transpose=False) @@ -117,7 +117,7 @@ def trip_purpose_and_destination( # if max iterations reached, add remaining trips to results and give up # note that we do this BEFORE failing leg_mates so resulting trip legs are complete if i >= MAX_ITERATIONS: - logger.warn("%s too many iterations %s" % (trace_label, i)) + logger.warning("%s too many iterations %s" % (trace_label, i)) results.append(trips_df[RESULT_COLUMNS]) break @@ -142,7 +142,8 @@ def trip_purpose_and_destination( if CLEANUP: trips_df = cleanup_failed_trips(trips_df) elif trips_df.failed.any(): - logger.warn("%s keeping %s sidelined failed trips" % (trace_label, trips_df.failed.sum())) + logger.warning("%s keeping %s sidelined failed trips" % + (trace_label, trips_df.failed.sum())) pipeline.replace_table("trips", trips_df) diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 7dbb09879..8ceffb4a0 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -337,8 +337,8 @@ def schedule_trips_in_leg( # most initial departure (when no choice was made because all probs were zero) if last_iteration and (failfix == FAILFIX_CHOOSE_MOST_INITIAL): choices = choices.reindex(nth_trips.index) - logger.warn("%s coercing %s depart choices to most initial" % - (nth_trace_label, choices.isna().sum())) + logger.warning("%s coercing %s depart choices to most initial" % + (nth_trace_label, choices.isna().sum())) choices = choices.fillna(trips[ADJUST_NEXT_DEPART_COL]) # adjust allowed depart range of next trip @@ -552,8 +552,8 @@ def trip_scheduling( choices = pd.concat(choices_list) choices = choices.reindex(trips_df.index) if choices.isnull().any(): - logger.warn("%s of %s trips could not be scheduled after %s iterations" % - (choices.isnull().sum(), trips_df.shape[0], i)) + logger.warning("%s of %s trips could not be scheduled after %s iterations" % + (choices.isnull().sum(), trips_df.shape[0], i)) if failfix != FAILFIX_DROP_AND_CLEANUP: raise RuntimeError("%s setting '%' not enabled in settings" % diff --git a/activitysim/abm/models/util/tour_destination.py b/activitysim/abm/models/util/tour_destination.py index 8a42b85fc..c81c2b429 100644 --- a/activitysim/abm/models/util/tour_destination.py +++ b/activitysim/abm/models/util/tour_destination.py @@ -40,9 +40,9 @@ def size_term(land_use, destination_choice_coeffs): missing = coeffs[~coeffs.index.isin(land_use.columns)] if len(missing) > 0: - logger.warn("%s missing columns in land use" % len(missing.index)) + logger.warning("%s missing columns in land use" % len(missing.index)) for v in missing.index.values: - logger.warn("missing: %s" % v) + logger.warning("missing: %s" % v) return land_use[coeffs.index].dot(coeffs) @@ -84,7 +84,7 @@ def tour_destination_size_terms(land_use, size_terms, selector): df.index.name = 'TAZ' if not (df.dtypes == 'float64').all(): - logger.warn('Surprised to find that not all size_terms were float64!') + logger.warning('Surprised to find that not all size_terms were float64!') # - #NARROW # float16 has 3.3 decimal digits of precision, float32 has 7.2 diff --git a/activitysim/abm/test/configs/annotate_households_cdap.csv b/activitysim/abm/test/configs/annotate_households_cdap.csv index 240357fa1..c3233f861 100644 --- a/activitysim/abm/test/configs/annotate_households_cdap.csv +++ b/activitysim/abm/test/configs/annotate_households_cdap.csv @@ -1,6 +1,6 @@ Description,Target,Expression #,, annotate households table after cdap model has run -num_under16_not_at_school,num_under16_not_at_school,"persons.under16_not_at_school.groupby(persons.household_id).sum().reindex(households.index).fillna(0)" +num_under16_not_at_school,num_under16_not_at_school,"persons.under16_not_at_school.astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0)" num_travel_active,num_travel_active,"persons.travel_active.groupby(persons.household_id).sum().reindex(households.index).fillna(0)" num_travel_active_adults,num_travel_active_adults,"(persons.adult & persons.travel_active).groupby(persons.household_id).sum().reindex(households.index).fillna(0)" num_travel_active_children,num_travel_active_children,"num_travel_active - num_travel_active_adults" diff --git a/activitysim/abm/test/output/.gitignore b/activitysim/abm/test/output/.gitignore index d98bf24ad..b987779f4 100644 --- a/activitysim/abm/test/output/.gitignore +++ b/activitysim/abm/test/output/.gitignore @@ -1,5 +1,6 @@ *.csv *.log +*.prof *.h5 *.txt *.yaml diff --git a/activitysim/abm/test/test_misc.py b/activitysim/abm/test/test_misc.py index 870eb8fd7..aea46ba49 100644 --- a/activitysim/abm/test/test_misc.py +++ b/activitysim/abm/test/test_misc.py @@ -1,5 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import str import os diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index a32302725..d02643a13 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -1,9 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -from builtins import str import os import logging @@ -21,13 +25,12 @@ HOUSEHOLDS_SAMPLE_SIZE = 100 # household with mandatory, non mandatory, atwork_subtours, and joint tours -HH_ID = 1528374 +HH_ID = 1482966 -# test households with all tour types -# [ 897097 921736 934309 1263203 1528374 1577292 1685008 1809710 2123173 2124138] +# [1062107 1098395 1115294 1482578 1482698 1482715 1482920 +# 1482966 1809757 2022863 2123123 2123582 2591312] - -SKIP_FULL_RUN = True +# SKIP_FULL_RUN = True SKIP_FULL_RUN = False @@ -98,28 +101,21 @@ def regress_mini_auto(): auto_choice = pipeline.get_table("households").auto_ownership # regression test: these are among the first 10 households in households table - hh_ids = [961042, 238146, 23730] - choices = [1, 1, 0] + hh_ids = [961042, 608031, 93713] + choices = [0, 0, 1] expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), name='auto_ownership') - print("auto_choice\n", auto_choice.head(10)) + print("auto_choice\n", auto_choice.head(3)) """ auto_choice - household_id - 961042 1 - 238146 1 - 701954 1 - 644046 1 - 23730 0 - 460343 1 - 25354 1 - 92615 1 - 1950284 0 - 2122448 0 + household_id + 961042 0 + 608031 0 + 93713 1 Name: auto_ownership, dtype: int64 """ - pdt.assert_series_equal(auto_choice[hh_ids], expected_choice) + pdt.assert_series_equal(auto_choice.reindex(hh_ids), expected_choice) def regress_mini_mtf(): @@ -127,22 +123,22 @@ def regress_mini_mtf(): mtf_choice = pipeline.get_table("persons").mandatory_tour_frequency # these choices are for pure regression - their appropriateness has not been checked - per_ids = [26986, 92615, 93332] - choices = ['school1', 'work1', 'work1'] + per_ids = [23757, 24180, 268182] + choices = ['school1', 'school1', 'work1'] expected_choice = pd.Series(choices, index=pd.Index(per_ids, name='person_id'), name='mandatory_tour_frequency') print("mtf_choice\n", mtf_choice.dropna().head(5)) """ mtf_choice - 26986 school1 - 92615 work1 - 93332 work1 - 93390 work1 - 93898 work1 + 23757 school1 + 24180 school1 + 25067 school1 + 268181 school1 + 268182 work1 Name: mandatory_tour_frequency, dtype: object """ - pdt.assert_series_equal(mtf_choice[per_ids], expected_choice) + pdt.assert_series_equal(mtf_choice.reindex(per_ids), expected_choice) def test_mini_pipeline_run(): @@ -305,7 +301,7 @@ def get_trace_csv(file_name): return df -EXPECT_TOUR_COUNT = 158 +EXPECT_TOUR_COUNT = 305 def regress_tour_modes(tours_df): @@ -320,47 +316,39 @@ def regress_tour_modes(tours_df): """ tour_id tour_mode person_id tour_type tour_num tour_category tour_id - 96881402 WALK 3340738 business 1 atwork - 96881411 SHARED2FREE 3340738 eatout 1 joint - 96881429 WALK_LOC 3340738 work 1 mandatory - 96881427 WALK_LOC 3340738 shopping 1 non_mandatory - 96881454 WALK_LOC 3340739 school 1 mandatory - 96881456 DRIVEALONEFREE 3340739 shopping 1 non_mandatory - 96881437 SHARED2FREE 3340739 eatout 2 non_mandatory - 96881457 WALK_LOC 3340739 social 3 non_mandatory + 94247751 SHARED2FREE 3249922 othmaint 1 joint + 94247765 WALK_LOC 3249922 work 1 mandatory + 94247744 WALK 3249922 eatout 1 non_mandatory + 94247771 SHARED3FREE 3249923 eat 1 atwork + 94247794 WALK_LOC 3249923 work 1 mandatory + 94247773 DRIVEALONEFREE 3249923 eatout 1 non_mandatory """ EXPECT_PERSON_IDS = [ - 3340738, - 3340738, - 3340738, - 3340738, - 3340739, - 3340739, - 3340739, - 3340739 + 3249922, + 3249922, + 3249922, + 3249923, + 3249923, + 3249923, ] EXPECT_TOUR_TYPES = [ - 'business', + 'othmaint', + 'work', 'eatout', + 'eat', 'work', - 'shopping', - 'school', - 'shopping', 'eatout', - 'social' ] EXPECT_MODES = [ - 'WALK', 'SHARED2FREE', 'WALK_LOC', - 'WALK_LOC', + 'WALK', + 'SHARED3FREE', 'WALK_LOC', 'DRIVEALONEFREE', - 'SHARED2FREE', - 'WALK_LOC' ] assert (tours_df.person_id.values == EXPECT_PERSON_IDS).all() diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 81b9778f6..fb8536932 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -241,7 +241,7 @@ def to_series(x): (target, expression, type(target)) if target in local_keys: - logger.warn("assign_variables target obscures local_d name '%s'" % str(target)) + logger.warning("assign_variables target obscures local_d name '%s'" % str(target)) if is_temp_scalar(target) or is_throwaway(target): x = eval(expression, globals(), _locals_dict) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 58e3fa7ce..5e40503d6 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -121,7 +121,7 @@ def chunked_choosers_and_alts(choosers, alternatives, rows_per_chunk): """ # if not choosers.index.is_monotonic_increasing: - # logger.warn('chunked_choosers_and_alts sorting choosers because not monotonic increasing') + # logger.warning('sorting choosers because not monotonic increasing') # choosers = choosers.sort_index() # alternatives index should match choosers (except with duplicate repeating alt rows) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 26f17e138..0fe6e8b4c 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -1,5 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import argparse import os @@ -252,7 +258,6 @@ def config_file_path(file_name, mandatory=True): if isinstance(configs_dir, str): configs_dir = [configs_dir] - assert isinstance(configs_dir, list) file_path = None diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index e86b6668a..b406e91b4 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -85,7 +85,7 @@ def add_step(name, func): def add_table(table_name, table, cache=False): if orca.is_table(table_name): - logger.warn("inject add_table replacing existing table %s" % table_name) + logger.warning("inject add_table replacing existing table %s" % table_name) return orca.add_table(table_name, table, cache=cache) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index bff1258bb..79787f500 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -128,10 +128,10 @@ def to_series(x): raise err if no_variability > 0: - logger.warn("%s: %s columns have no variability" % (trace_label, no_variability)) + logger.warning("%s: %s columns have no variability" % (trace_label, no_variability)) if has_missing_vals > 0: - logger.warn("%s: %s columns have missing values" % (trace_label, has_missing_vals)) + logger.warning("%s: %s columns have missing values" % (trace_label, has_missing_vals)) if trace_eval_results is not None: trace_eval_results['total utility'] = utilities.utility[trace_rows] diff --git a/activitysim/core/orca/orca.py b/activitysim/core/orca/orca.py index 20514c9a6..b8dab93e4 100644 --- a/activitysim/core/orca/orca.py +++ b/activitysim/core/orca/orca.py @@ -1967,7 +1967,8 @@ def run(steps, iter_vars=None, data_out=None, out_interval=1, # write out the base (inputs) if data_out: add_injectable('iter_var', iter_vars[0]) - write_tables(data_out, out_base_tables, 'base', compress=compress, local=out_base_local) + write_tables(data_out, out_base_tables, + prefix='base', compress=compress, local=out_base_local) # run the steps for i, var in enumerate(iter_vars, start=1): @@ -1990,7 +1991,8 @@ def run(steps, iter_vars=None, data_out=None, out_interval=1, # write out the results for the current iteration if data_out: if (i - 1) % out_interval == 0 or i == max_i: - write_tables(data_out, out_run_tables, var, compress=compress, local=out_run_local) + write_tables(data_out, out_run_tables, + prefix=var, compress=compress, local=out_run_local) clear_cache(scope=_CS_ITER) diff --git a/activitysim/core/orca/tests/test_orca.py b/activitysim/core/orca/tests/test_orca.py index aae3c3093..7a3f07ab2 100644 --- a/activitysim/core/orca/tests/test_orca.py +++ b/activitysim/core/orca/tests/test_orca.py @@ -915,12 +915,12 @@ def step(table): step_tables = orca.get_step_table_names(['step']) - orca.write_tables(store_name, step_tables, None) + orca.write_tables(store_name, step_tables) with pd.HDFStore(store_name, mode='r') as store: assert 'table' in store pdt.assert_frame_equal(store['table'], df) - orca.write_tables(store_name, step_tables, 1969) + orca.write_tables(store_name, step_tables, prefix=1969) with pd.HDFStore(store_name, mode='r') as store: assert '1969/table' in store diff --git a/activitysim/core/orca/utils/logutil.py b/activitysim/core/orca/utils/logutil.py index 90755ce02..258a24f0a 100644 --- a/activitysim/core/orca/utils/logutil.py +++ b/activitysim/core/orca/utils/logutil.py @@ -26,9 +26,9 @@ def log_start_finish(msg, logger, level=logging.DEBUG): Level at which to log, passed to ``logger.log``. """ - logger.log(level, 'start: ' + msg) + # logger.log(level, 'start: ' + msg) yield - logger.log(level, 'finish: ' + msg) + # logger.log(level, 'finish: ' + msg) def set_log_level(level): diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 8ad2c62c7..954267eb0 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,9 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function -from __future__ import division -from __future__ import absolute_import + +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from future.utils import iteritems @@ -113,7 +117,7 @@ def open_pipeline_store(overwrite=False): os.unlink(pipeline_file_path) except Exception as e: print(e) - logger.warn("Error removing %s: %s" % (pipeline_file_path, e)) + logger.warning("Error removing %s: %s" % (pipeline_file_path, e)) _PIPELINE.pipeline_store = pd.HDFStore(pipeline_file_path, mode='a') @@ -718,15 +722,22 @@ def extend_table(table_name, df, axis=0): if axis == 0: # don't expect indexes to overlap assert len(table_df.index.intersection(df.index)) == 0 + missing_df_str_columns = [c for c in table_df.columns + if c not in df.columns and table_df[c].dtype == 'O'] else: # expect indexes be same assert table_df.index.equals(df.index) - new_columns = [c for c in df.columns if c not in table_df.columns] - df = df[new_columns] + new_df_columns = [c for c in df.columns if c not in table_df.columns] + df = df[new_df_columns] # preserve existing column order df = pd.concat([table_df, df], sort=False, axis=axis) + # backfill missing df columns that were str (object) type in table_df + if axis == 0: + for c in missing_df_str_columns: + df[c] = df[c].fillna('') + replace_table(table_name, df) return df diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 53829862a..f0bfdc835 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -5,7 +5,10 @@ from builtins import range from builtins import object + import logging +import hashlib + import numpy as np import pandas as pd @@ -16,6 +19,23 @@ # one more than 0xFFFFFFFF so we can wrap using: int64 % _MAX_SEED _MAX_SEED = (1 << 32) +_SEED_MASK = 0xffffffff + + +def hash32(s): + """ + + Parameters + ---------- + s: str + + Returns + ------- + 32 bit unsigned hash + """ + s = s.encode('utf8') + h = hashlib.md5(s).hexdigest() + return int(h, base=16) & _SEED_MASK class SimpleChannel(object): @@ -54,7 +74,7 @@ def __init__(self, channel_name, base_seed, domain_df, step_name): # ensure that every channel is different, even for the same df index values and max_steps self.channel_name = channel_name - self.channel_seed = hash(self.channel_name) % _MAX_SEED + self.channel_seed = hash32(self.channel_name) self.step_name = None self.step_seed = None @@ -109,7 +129,7 @@ def extend_domain(self, domain_df): """ if domain_df.empty: - logger.warn("extend_domain for channel %s for empty domain_df" % self.channel_name) + logger.warning("extend_domain for channel %s for empty domain_df" % self.channel_name) # dataframe to hold state for every df row row_states = pd.DataFrame(columns=['row_seed', 'offset'], index=domain_df.index) @@ -138,7 +158,7 @@ def begin_step(self, step_name): assert self.step_name is None self.step_name = step_name - self.step_seed = hash(self.step_name) % _MAX_SEED + self.step_seed = hash32(self.step_name) self.init_row_states_for_step(self.row_states) @@ -269,7 +289,7 @@ def choice_for_df(self, df, step_name, a, size, replace): if not self.multi_choice_offset: # FIXME - if replace, should we estimate rands_consumed? if replace: - logger.warn("choice_for_df MULTI_CHOICE_FF with replace") + logger.warning("choice_for_df MULTI_CHOICE_FF with replace") # update offset for rows we handled self.row_states.loc[df.index, 'offset'] += size @@ -324,7 +344,7 @@ def begin_step(self, step_name): self.step_name = step_name - self.step_seed = hash(step_name) % _MAX_SEED + self.step_seed = hash32(step_name) seed = [self.base_seed, self.step_seed] self.global_rng = np.random.RandomState(seed) @@ -470,7 +490,7 @@ def get_external_rng(self, one_off_step_name): exists to allow sampling of input tables consistent no matter what step they are called in """ - seed = [self.base_seed, hash(one_off_step_name) % _MAX_SEED] + seed = [self.base_seed, hash32(one_off_step_name)] return np.random.RandomState(seed) def random_for_df(self, df, n=1): diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 459b92b0f..ddd8922de 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -312,10 +312,10 @@ def _check_for_variability(expression_values, trace_label): has_missing_vals += 1 if no_variability > 0: - logger.warn("%s: %s columns have no variability" % (trace_label, no_variability)) + logger.warning("%s: %s columns have no variability" % (trace_label, no_variability)) if has_missing_vals > 0: - logger.warn("%s: %s columns have missing values" % (trace_label, has_missing_vals)) + logger.warning("%s: %s columns have missing values" % (trace_label, has_missing_vals)) def compute_nested_exp_utilities(raw_utilities, nest_spec): @@ -977,8 +977,8 @@ def simple_simulate_logsums_rpc(chunk_size, choosers, spec, nest_spec, trace_lab # expression_values for each spec row # utilities for each alt extra_columns = spec.shape[0] + spec.shape[1] - logger.warn("simple_simulate_logsums_rpc rows_per_chunk not validated for mnl" - " so chunk sizing might be a bit off") + logger.warning("simple_simulate_logsums_rpc rows_per_chunk not validated for mnl" + " so chunk sizing might be a bit off") else: # expression_values for each spec row # raw_utilities for each alt diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index ebdd1f046..a826e9eb3 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -1,8 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging -import os +import sys import pandas as pd from collections import OrderedDict @@ -36,37 +41,39 @@ def track_skim_usage(output_dir): skim_dict = inject.get_injectable('skim_dict') skim_stack = inject.get_injectable('skim_stack', None) - with open(config.output_file_path('skim_usage.txt'), 'w') as file: + with open(config.output_file_path('skim_usage.txt'), 'wb') as file: - print >> file, "\n### skim_dict usage" + print("\n### skim_dict usage", file=file) for key in skim_dict.usage: - print>> file, key + print(key, file=file) if skim_stack is None: - unused_keys = {k for k in skim_dict.skim_key_to_skim_num} - {k for k in skim_dict.usage} - print >> file, "\n### unused skim keys" + unused_keys = {k for k in skim_dict.skim_info['omx_keys']} - \ + {k for k in skim_dict.usage} + + print("\n### unused skim keys", file=file) for key in unused_keys: - print>> file, key + print(key, file=file) else: - print >> file, "\n### skim_stack usage" + print("\n### skim_stack usage", file=file) for key in skim_stack.usage: - print>> file, key + print(key, file=file) - unused = {k for k in skim_dict.skim_key_to_skim_num if not isinstance(k, tuple)} - \ + unused = {k for k in skim_dict.skim_info['omx_keys'] if not isinstance(k, tuple)} - \ {k for k in skim_dict.usage if not isinstance(k, tuple)} - print >> file, "\n### unused skim str keys" + print("\n### unused skim str keys", file=file) for key in unused: - print>> file, key + print(key, file=file) - unused = {k[0] for k in skim_dict.skim_key_to_skim_num if isinstance(k, tuple)} - \ + unused = {k[0] for k in skim_dict.skim_info['omx_keys'] if isinstance(k, tuple)} - \ {k[0] for k in skim_dict.usage if isinstance(k, tuple)} - \ {k for k in skim_stack.usage} - print >> file, "\n### unused skim dim3 keys" + print("\n### unused skim dim3 keys", file=file) for key in unused: - print>> file, key + print(key, file=file) def write_data_dictionary(output_dir): @@ -85,16 +92,17 @@ def write_data_dictionary(output_dir): # write data dictionary for all checkpointed_tables - with open(config.output_file_path('data_dict.txt'), 'w') as file: + mode = 'wb' if sys.version_info < (3,) else 'w' + with open(config.output_file_path('data_dict.txt'), mode) as file: for table_name in output_tables: df = inject.get_table(table_name, None).to_frame() - print >> file, "\n### %s %s" % (table_name, df.shape) - print >> file, 'index:', df.index.name, df.index.dtype - print >> file, df.dtypes + print("\n### %s %s" % (table_name, df.shape), file=file) + print('index:', df.index.name, df.index.dtype, file=file) + print(df.dtypes, file=file) -# def write_data_dictionary(output_dir): +# def xwrite_data_dictionary(output_dir): # """ # Write table_name, number of rows, columns, and bytes for each checkpointed table # @@ -111,20 +119,20 @@ def write_data_dictionary(output_dir): # # table_names = [c for c in checkpoints if c not in pipeline.NON_TABLE_COLUMNS] # -# with open(config.output_file_path('data_dict.txt'), 'w') as file: +# with open(config.output_file_path('data_dict.txt'), 'wb') as file: # # for index, row in checkpoints.iterrows(): # # checkpoint = row[pipeline.CHECKPOINT_NAME] # -# print >> file, "\n##########################################" -# print >> file, "# %s" % checkpoint -# print >> file, "##########################################" +# print("\n##########################################", file=file) +# print("# %s" % checkpoint, file=file) +# print("##########################################", file=file) # # for table_name in table_names: # # if row[table_name] == '' and table_name in tables: -# print >> file, "\n### %s dropped %s" % (checkpoint, table_name, ) +# print("\n### %s dropped %s" % (checkpoint, table_name, ), file=file) # del tables[table_name] # # if row[table_name] == checkpoint: @@ -132,11 +140,11 @@ def write_data_dictionary(output_dir): # info = tables.get(table_name, None) # if info is None: # -# print >> file, "\n### %s created %s %s\n" % \ -# (checkpoint, table_name, df.shape) +# print("\n### %s created %s %s\n" % +# (checkpoint, table_name, df.shape), file=file) # -# print >> file, df.dtypes -# print >> file, 'index:', df.index.name, df.index.dtype +# print(df.dtypes, file=file) +# print('index:', df.index.name, df.index.dtype, file=file) # # else: # new_cols = [c for c in df.columns.values if c not in info['columns']] @@ -144,25 +152,26 @@ def write_data_dictionary(output_dir): # new_rows = df.shape[0] - info['num_rows'] # if new_cols: # -# print >> file, "\n### %s added %s columns to %s %s\n" % \ -# (checkpoint, len(new_cols), table_name, df.shape) -# print >> file, df[new_cols].dtypes +# print("\n### %s added %s columns to %s %s\n" % +# (checkpoint, len(new_cols), table_name, df.shape), file=file) +# print(df[new_cols].dtypes, file=file) # # if dropped_cols: -# print >> file, "\n### %s dropped %s columns from %s %s\n" % \ -# (checkpoint, len(dropped_cols), table_name, df.shape) -# print >> file, dropped_cols +# print("\n### %s dropped %s columns from %s %s\n" % +# (checkpoint, len(dropped_cols), table_name, df.shape), +# file=file) +# print(dropped_cols, file=file) # # if new_rows > 0: -# print >> file, "\n### %s added %s rows to %s %s" % \ -# (checkpoint, new_rows, table_name, df.shape) +# print("\n### %s added %s rows to %s %s" % +# (checkpoint, new_rows, table_name, df.shape), file=file) # elif new_rows < 0: -# print >> file, "\n### %s dropped %s rows from %s %s" % \ -# (checkpoint, new_rows, table_name, df.shape) +# print("\n### %s dropped %s rows from %s %s" % +# (checkpoint, new_rows, table_name, df.shape), file=file) # else: # if not new_cols and not dropped_cols: -# print >> file, "\n### %s modified %s %s" % \ -# (checkpoint, table_name, df.shape) +# print("\n### %s modified %s %s" % +# (checkpoint, table_name, df.shape), file=file) # # tables[table_name] = { # 'checkpoint_name': checkpoint, @@ -232,7 +241,7 @@ def write_tables(output_dir): df = pipeline.get_checkpoints() else: if table_name not in checkpointed_tables: - logger.warn("Skipping '%s': Table not found." % table_name) + logger.warning("Skipping '%s': Table not found." % table_name) continue df = pipeline.get_table(table_name) diff --git a/activitysim/core/test/configs/custom_logging.yaml b/activitysim/core/test/configs/custom_logging.yaml index 92335d307..71f1e6d4f 100644 --- a/activitysim/core/test/configs/custom_logging.yaml +++ b/activitysim/core/test/configs/custom_logging.yaml @@ -9,18 +9,18 @@ logging: # Configuring the default (root) logger is highly recommended root: - level: !!python/name:logging.NOTSET + level: NOTSET handlers: [console] loggers: activitysim: - level: !!python/name:logging.DEBUG + level: DEBUG handlers: [console, logfile] propagate: false orca: - level: !!python/name:logging.INFO + level: INFO handlers: [console, logfile] propagate: false @@ -31,24 +31,24 @@ logging: filename: !!python/object/apply:activitysim.core.config.log_file_path ['xasim.log'] mode: w formatter: simpleFormatter - level: !!python/name:logging.NOTSET + level: NOTSET console: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - #level: !!python/name:logging.NOTSET - level: !!python/name:logging.WARN + #level: NOTSET + level: WARNING formatters: simpleFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' diff --git a/activitysim/core/test/configs/logging.yaml b/activitysim/core/test/configs/logging.yaml index 5e78d3824..35067d008 100644 --- a/activitysim/core/test/configs/logging.yaml +++ b/activitysim/core/test/configs/logging.yaml @@ -9,18 +9,18 @@ logging: # Configuring the default (root) logger is highly recommended root: - level: !!python/name:logging.NOTSET + level: NOTSET handlers: [console] loggers: activitysim: - level: !!python/name:logging.DEBUG + level: DEBUG handlers: [console, logfile] propagate: false orca: - level: !!python/name:logging.INFO + level: INFO handlers: [console, logfile] propagate: false @@ -31,24 +31,24 @@ logging: filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] mode: w formatter: simpleFormatter - level: !!python/name:logging.NOTSET + level: NOTSET console: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - #level: !!python/name:logging.NOTSET - level: !!python/name:logging.WARN + #level: NOTSET + level: WARNING formatters: simpleFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' diff --git a/activitysim/core/test/extensions/__init__.py b/activitysim/core/test/extensions/__init__.py index ba5d55cf5..4512a2508 100644 --- a/activitysim/core/test/extensions/__init__.py +++ b/activitysim/core/test/extensions/__init__.py @@ -1 +1,2 @@ -import steps +from __future__ import absolute_import +from . import steps diff --git a/activitysim/core/test/extensions/steps.py b/activitysim/core/test/extensions/steps.py index 00341685d..73a4f252c 100644 --- a/activitysim/core/test/extensions/steps.py +++ b/activitysim/core/test/extensions/steps.py @@ -1,9 +1,14 @@ +from __future__ import (absolute_import, division, print_function, unicode_literals) +from builtins import * import pandas as pd from activitysim.core import inject from activitysim.core import pipeline from activitysim.core import tracing +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + @inject.step() def step1(): diff --git a/activitysim/core/test/test_assign.py b/activitysim/core/test/test_assign.py index 0db77e1e4..f52e5997c 100644 --- a/activitysim/core/test/test_assign.py +++ b/activitysim/core/test/test_assign.py @@ -1,9 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + -from builtins import str import os.path import logging import logging.config diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index 5486b6a05..c9503c608 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -1,8 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function -from builtins import str +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import os import pytest diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index 2842a742d..d5a79966e 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -1,9 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -from builtins import str import os import logging import pytest diff --git a/activitysim/core/test/test_random.py b/activitysim/core/test/test_random.py index bc38d5561..341b57d8b 100644 --- a/activitysim/core/test/test_random.py +++ b/activitysim/core/test/test_random.py @@ -22,11 +22,11 @@ def test_basic(): global_rng = rng.get_global_rng() - npt.assert_almost_equal(global_rng.rand(1), [0.09237]) + npt.assert_almost_equal(global_rng.rand(1), [0.8994663]) # second call should return something different with pytest.raises(AssertionError) as excinfo: - npt.assert_almost_equal(global_rng.rand(1), [0.09237]) + npt.assert_almost_equal(global_rng.rand(1), [0.8994663]) assert "Arrays are not almost equal" in str(excinfo.value) # second call should return something different @@ -63,12 +63,12 @@ def test_channel(): print("rands", np.asanyarray(rands).flatten()) assert rands.shape == (5, 1) - test1_expected_rands = [0.9060891, 0.4576382, 0.2154094, 0.2801035, 0.6196645] + test1_expected_rands = [0.1733218, 0.1255693, 0.7384256, 0.3485183, 0.9012387] npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands) # second call should return something different rands = rng.random_for_df(persons) - test1_expected_rands2 = [0.5991157, 0.5516594, 0.5529548, 0.3586653, 0.5844314] + test1_expected_rands2 = [0.9105223, 0.5718418, 0.7222742, 0.9062284, 0.3929369] npt.assert_almost_equal(np.asanyarray(rands).flatten(), test1_expected_rands2) rng.end_step('test_step') @@ -76,16 +76,16 @@ def test_channel(): rng.begin_step('test_step2') rands = rng.random_for_df(households) - expected_rands = [0.7970902, 0.2633469, 0.7662205, 0.7544782, 0.129741] + expected_rands = [0.417278, 0.2994774, 0.8653719, 0.4429748, 0.5101697] npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) choices = rng.choice_for_df(households, [1, 2, 3, 4], 2, replace=True) - expected_choices = [2, 1, 3, 1, 3, 1, 3, 4, 4, 2] + expected_choices = [2, 1, 3, 3, 4, 2, 4, 1, 4, 1] npt.assert_almost_equal(choices, expected_choices) # should be DIFFERENT the second time choices = rng.choice_for_df(households, [1, 2, 3, 4], 2, replace=True) - expected_choices = [1, 1, 3, 2, 3, 2, 2, 3, 2, 3] + expected_choices = [3, 1, 4, 3, 3, 2, 2, 1, 4, 2] npt.assert_almost_equal(choices, expected_choices) rng.end_step('test_step2') @@ -94,8 +94,8 @@ def test_channel(): rands = rng.random_for_df(households, n=2) - expected_rands = [0.8635927, 0.3258157, 0.7970902, 0.365523, 0.2633469, 0.5388047, - 0.7662205, 0.8067344, 0.7544782, 0.024577] + expected_rands = [0.3157928, 0.3321823, 0.5194067, 0.9340083, 0.9002048, 0.8754209, + 0.3898816, 0.4101094, 0.7351484, 0.1741092] npt.assert_almost_equal(np.asanyarray(rands).flatten(), expected_rands) diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index c33aa9dc1..d554f8e58 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -1,9 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -from builtins import str import os.path import logging import pytest @@ -51,7 +57,7 @@ def test_config_logger(capsys): logger.info('test_config_logger') logger.info('log_info') - logger.warn('log_warn1') + logger.warning('log_warn1') out, err = capsys.readouterr() @@ -65,7 +71,7 @@ def test_config_logger(capsys): close_handlers() logger = logging.getLogger(__name__) - logger.warn('log_warn2') + logger.warning('log_warn2') with open(asim_logger_baseFilename, 'r') as content_file: content = content_file.read() @@ -226,7 +232,7 @@ def test_basic(capsys): logger.info('test_basic') logger.debug('log_debug') logger.info('log_info') - logger.warn('log_warn') + logger.warning('log_warn') out, err = capsys.readouterr() diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index aecbfb8b8..6006addd1 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -1,14 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division +from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import next from builtins import str from builtins import range +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import os import logging import logging.config @@ -222,7 +223,7 @@ def register_traceable_table(table_name, df): if table_name == 'households': if trace_hh_id not in df.index: - logger.warn("trace_hh_id %s not in dataframe" % trace_hh_id) + logger.warning("trace_hh_id %s not in dataframe" % trace_hh_id) new_traced_ids = [] else: logger.info("tracing household id %s in %s households" % (trace_hh_id, len(df.index))) @@ -246,8 +247,8 @@ def register_traceable_table(table_name, df): traced_df = df[df[ref_con].isin(ref_con_traced_ids)] new_traced_ids = traced_df.index.tolist() if len(new_traced_ids) == 0: - logger.warn("register %s: no rows with %s in %s." % - (table_name, ref_con, ref_con_traced_ids)) + logger.warning("register %s: no rows with %s in %s." % + (table_name, ref_con, ref_con_traced_ids)) # update traceable_table_refs with this traceable_table's ref_con if idx_name not in traceable_table_refs: @@ -339,11 +340,9 @@ def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, Nothing """ - file_name = file_name.encode('ascii', 'ignore') - assert len(file_name) > 0 - if not file_name.endswith(".%s" % CSV_FILE_TYPE): + if not file_name.endswith('.%s' % CSV_FILE_TYPE): file_name = '%s.%s' % (file_name, CSV_FILE_TYPE) file_path = config.trace_file_path(file_name) @@ -555,8 +554,8 @@ def trace_df(df, label, slicer=None, columns=None, if warn_if_empty and df.shape[0] == 0 and target_ids != []: column_name = column or slicer - logger.warn("slice_canonically: no rows in %s with %s == %s" - % (label, column_name, target_ids)) + logger.warning("slice_canonically: no rows in %s with %s == %s" + % (label, column_name, target_ids)) if df.shape[0] > 0: write_csv(df, file_name=label, index_label=(index_label or slicer), columns=columns, diff --git a/activitysim/core/util.py b/activitysim/core/util.py index c60e9aaf2..39ccf98a1 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -287,8 +287,8 @@ def assign_in_place(df, df2): try: df[c] = df[c].astype(old_dtype) except ValueError: - logger.warn("assign_in_place changed dtype %s of column %s to %s" % - (old_dtype, c, df[c].dtype)) + logger.warning("assign_in_place changed dtype %s of column %s to %s" % + (old_dtype, c, df[c].dtype)) # if both df and df2 column were ints, but result is not if np.issubdtype(old_dtype, np.integer) \ @@ -297,8 +297,8 @@ def assign_in_place(df, df2): try: df[c] = df[c].astype(old_dtype) except ValueError: - logger.warn("assign_in_place changed dtype %s of column %s to %s" % - (old_dtype, c, df[c].dtype)) + logger.warning("assign_in_place changed dtype %s of column %s to %s" % + (old_dtype, c, df[c].dtype)) # add new columns (in order they appear in df2) new_columns = [c for c in df2.columns if c not in df.columns] diff --git a/example/configs/logging.yaml b/example/configs/logging.yaml index 29a9f6fb8..7ab585e84 100644 --- a/example/configs/logging.yaml +++ b/example/configs/logging.yaml @@ -9,18 +9,18 @@ logging: # Configuring the default (root) logger is highly recommended root: - level: !!python/name:logging.NOTSET + level: NOTSET handlers: [console] loggers: activitysim: - level: !!python/name:logging.INFO + level: INFO handlers: [console, logfile] propagate: false orca: - level: !!python/name:logging.WARN + level: WARN handlers: [console, logfile] propagate: false @@ -31,24 +31,24 @@ logging: filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] mode: w formatter: fileFormatter - level: !!python/name:logging.NOTSET + level: NOTSET console: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - level: !!python/name:logging.NOTSET + level: NOTSET formatters: simpleFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter # format: '%(levelname)s - %(name)s - %(message)s' format: '%(levelname)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 755525ceb..2cbd7a985 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -7,7 +7,7 @@ households_sample_size: 100 #trace household id; comment out for no trace # household with all tour categories -trace_hh_id: 1269102 +trace_hh_id: 1482966 # trace origin, destination in accessibility calculation #trace_od: [5, 11] diff --git a/example_mp/__init__.py b/example_mp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index 71bf056b6..ba05c0fdb 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -9,23 +9,23 @@ logging: # Configuring the default (root) logger is highly recommended root: - level: !!python/name:logging.DEBUG + level: DEBUG handlers: [console, logfile] loggers: tasks: - level: !!python/name:logging.DEBUG + level: DEBUG handlers: [mp_console, logfile] propagate: false activitysim: - level: !!python/name:logging.DEBUG + level: DEBUG handlers: [console, logfile] propagate: false orca: - level: !!python/name:logging.WARN + level: WARNING handlers: [console, logfile] propagate: false @@ -36,32 +36,32 @@ logging: filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] mode: w formatter: fileFormatter - level: !!python/name:logging.NOTSET + level: NOTSET console: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - #level: !!python/name:logging.WARN - level: !!python/object/apply:tasks.if_sub_task_opt [INFO, NOTSET] + #level: WARNING + level: !!python/object/apply:tasks.if_sub_task [INFO, NOTSET] mp_console: class: logging.StreamHandler stream: ext://sys.stdout formatter: simpleFormatter - level: !!python/name:logging.NOTSET + level: NOTSET formatters: simpleFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' # format: !!python/object/apply:tasks.if_sub_task ['%(processName)-10s %(levelname)s - %(name)s - %(message)s', # '%(levelname)s - %(name)s - %(message)s'] datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: - class: !!python/name:logging.Formatter + class: logging.Formatter format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' datefmt: '%d/%m/%Y %H:%M:%S' diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 3d8789cf8..f9536ebc9 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -1,9 +1,16 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging +from io import open +import sys from activitysim.core import inject from activitysim.core import tracing @@ -48,11 +55,13 @@ def cleanup_output_files(): cleanup_output_files() run_list = tasks.get_run_list() - with open(config.output_file_path('run_list.txt'), 'w') as f: + + mode = 'wb' if sys.version_info < (3,) else 'w' + with open(config.output_file_path('run_list.txt'), mode) as f: tasks.print_run_list(run_list, f) - # tasks.print_run_list(run_list) - # bug + tasks.print_run_list(run_list) + bug if run_list['multiprocess']: logger.info("run multiprocess simulation") diff --git a/example_mp/tasks.py b/example_mp/tasks.py index af9255faf..d9fcee6f9 100644 --- a/example_mp/tasks.py +++ b/example_mp/tasks.py @@ -1,12 +1,16 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import absolute_import from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -from builtins import zip -from builtins import next -from builtins import range from future.utils import iteritems import sys @@ -345,7 +349,7 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): # if they specified a resume_after model, check to make sure it is checkpointed if resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: # if not checkpointed, then fall back to last checkpoint - logger.warn("resume_after checkpoint '%s' not in pipeline", resume_after) + logger.warning("resume_after checkpoint '%s' not in pipeline", resume_after) resume_after = '_' pipeline.open_pipeline(resume_after) @@ -596,7 +600,7 @@ def get_run_list(): multiprocess_steps = setting('multiprocess_steps', []) if multiprocess and mp.cpu_count() == 1: - logger.warn("Can't multiprocess because there is only 1 cpu") + logger.warning("Can't multiprocess because there is only 1 cpu") multiprocess = False run_list = { @@ -653,8 +657,8 @@ def get_run_list(): raise RuntimeError("num_processes = 1 but found slice info for step %s" " in multiprocess_steps" % name) if num_processes > mp.cpu_count(): - logger.warn("num_processes setting (%s) greater than cpu count (%s", - num_processes, mp.cpu_count()) + logger.warning("num_processes setting (%s) greater than cpu count (%s", + num_processes, mp.cpu_count()) else: if num_processes == 0: num_processes = 1 @@ -743,6 +747,9 @@ def print_run_list(run_list, output_file=None): if output_file is None: output_file = sys.stdout + s = 'print_run_list' + print(s, file=output_file) + print("resume_after:", run_list['resume_after'], file=output_file) print("multiprocess:", run_list['multiprocess'], file=output_file) @@ -818,15 +825,3 @@ def is_sub_task(): def if_sub_task(if_is, if_isnt): return if_is if is_sub_task() else if_isnt - - -def if_sub_task_opt(if_is, if_isnt): - - opt = { - 'ERROR': logging.ERROR, - 'WARN': logging.WARN, - 'INFO': logging.INFO, - 'DEBUG': logging.DEBUG, - 'NOTSET': logging.NOTSET, - } - return opt[if_is] if is_sub_task() else opt[if_isnt] diff --git a/example_multi/extensions/models.py b/example_multi/extensions/models.py index e49aff9cc..2446e52c4 100644 --- a/example_multi/extensions/models.py +++ b/example_multi/extensions/models.py @@ -98,7 +98,7 @@ def best_transit_path(set_random_seed, if trace_od: if not trace_oabd_rows.any(): - logger.warn("trace_od not found origin = %s, dest = %s" % (trace_orig, trace_dest)) + logger.warning("trace_od not found origin = %s, dest = %s" % (trace_orig, trace_dest)) else: tracing.trace_df(atap_btap_df, diff --git a/setup.py b/setup.py index 14a6187f8..f17f3daae 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ classifiers=[ 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', 'License :: OSI Approved :: BSD License' ], long_description=long_description, From 2a75ed780cc2f2a93da0dc1093012d81b91942a6 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 16 Oct 2018 17:17:48 -0400 Subject: [PATCH 025/122] mp_tasks and p3.5 tests --- .travis.yml | 9 +- activitysim/abm/tables/skims.py | 15 ++- activitysim/abm/test/configs_mp/settings.yaml | 45 ++++++++ activitysim/abm/test/run_mp.py | 100 ++++++++++++++++++ activitysim/abm/test/test_mp_pipeline.py | 24 +++++ activitysim/core/interaction_simulate.py | 3 +- .../tasks.py => activitysim/core/mp_tasks.py | 56 +++++----- activitysim/core/simulate.py | 15 +-- activitysim/core/steps/output.py | 3 +- activitysim/core/test/test_simulate.py | 4 +- activitysim/core/tracing.py | 4 - example/configs/settings.yaml | 1 + example_mp/__init__.py | 0 example_mp/configs/logging.yaml | 4 +- example_mp/simulation.py | 25 ++--- 15 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 activitysim/abm/test/configs_mp/settings.yaml create mode 100644 activitysim/abm/test/run_mp.py create mode 100644 activitysim/abm/test/test_mp_pipeline.py rename example_mp/tasks.py => activitysim/core/mp_tasks.py (94%) delete mode 100644 example_mp/__init__.py diff --git a/.travis.yml b/.travis.yml index 114187ad2..d4438cbdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: python sudo: false python: -- '2.7' + - '2.7' + - '3.5' install: -- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget http://repo.continuum.io/miniconda/Miniconda-3.7.0-Linux-x86_64.sh - -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.7.0-Linux-x86_64.sh - -O miniconda.sh; fi +- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; + then wget http://repo.continuum.io/miniconda/Miniconda-3.7.0-Linux-x86_64.sh -O miniconda.sh; + else wget http://repo.continuum.io/miniconda/Miniconda3-3.7.0-Linux-x86_64.sh -O miniconda.sh; fi - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 928f107e2..d51ca8f3b 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -1,21 +1,25 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import map from builtins import range +from builtins import int + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + from future.utils import iteritems import sys import os import logging +import multiprocessing from collections import OrderedDict -import multiprocessing as mp import numpy as np - import openmatrix as omx from activitysim.core import skim @@ -148,7 +152,8 @@ def buffer_for_skims(skim_info, shared=False): skim_buffer = {} for block_name, block_size in iteritems(blocks): - buffer_size = np.prod(omx_shape) * block_size + # buffer_size must be int (or p2.7 long), not np.int64 + buffer_size = int(np.prod(omx_shape) * block_size) logger.info("allocating shared buffer %s for %s (%s) matrices" % (block_name, buffer_size, omx_shape, )) @@ -161,7 +166,7 @@ def buffer_for_skims(skim_info, shared=False): else: raise RuntimeError("buffer_for_skims unrecognized dtype %s" % skim_dtype) - buffer = mp.RawArray(typecode, buffer_size) + buffer = multiprocessing.RawArray(typecode, buffer_size) else: buffer = np.zeros(buffer_size, dtype=skim_dtype) diff --git a/activitysim/abm/test/configs_mp/settings.yaml b/activitysim/abm/test/configs_mp/settings.yaml new file mode 100644 index 000000000..9e80f88da --- /dev/null +++ b/activitysim/abm/test/configs_mp/settings.yaml @@ -0,0 +1,45 @@ + +inherit_settings: True + +multiprocess: True + + +models: + - initialize_landuse + - compute_accessibility + - initialize_households + - school_location_sample + - school_location_logsums + - school_location_simulate + - workplace_location_sample + - workplace_location_logsums + - workplace_location_simulate + - auto_ownership_simulate + - write_data_dictionary + - write_tables + + +multiprocess_steps: + - name: mp_initialize_landuse + begin: initialize_landuse + - name: mp_accessibility + begin: compute_accessibility + num_processes: 2 + slice: + tables: + - accessibility + except: + - land_use + - name: mp_initialize_households + begin: initialize_households + - name: mp_households + begin: school_location_sample + num_processes: 2 + stagger: 4 + #chunk_size: 1000000000 + slice: + tables: + - households + - persons + - name: mp_summarize + begin: write_data_dictionary diff --git a/activitysim/abm/test/run_mp.py b/activitysim/abm/test/run_mp.py new file mode 100644 index 000000000..b2ec21716 --- /dev/null +++ b/activitysim/abm/test/run_mp.py @@ -0,0 +1,100 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +import os +import logging + +import pandas as pd +import pandas.util.testing as pdt + +from activitysim.core import tracing +from activitysim.core import pipeline +from activitysim.core import inject +from activitysim.core import mp_tasks + +# set the max households for all tests (this is to limit memory use on travis) +HOUSEHOLDS_SAMPLE_SIZE = 100 + +# household with mandatory, non mandatory, atwork_subtours, and joint tours +HH_ID = 1482966 + +# [1062107 1098395 1115294 1482578 1482698 1482715 1482920 +# 1482966 1809757 2022863 2123123 2123582 2591312] + + +# def teardown_function(func): +# inject.clear_cache() +# inject.reinject_decorated_tables() +# +# +# def close_handlers(): +# +# loggers = logging.Logger.manager.loggerDict +# for name in loggers: +# logger = logging.getLogger(name) +# logger.handlers = [] +# logger.propagate = True +# logger.setLevel(logging.NOTSET) + + +def regress_mini_auto(): + + auto_choice = pipeline.get_table("households").auto_ownership + + # regression test: these are among the first 10 households in households table + hh_ids = [961042, 608031, 93713] + choices = [0, 0, 1] + expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), + name='auto_ownership') + + print("auto_choice\n", auto_choice.head(3)) + """ + auto_choice + household_id + 961042 0 + 608031 0 + 93713 1 + Name: auto_ownership, dtype: int64 + """ + pdt.assert_series_equal(auto_choice.reindex(hh_ids), expected_choice) + + +def test_mp_run(): + + mp_configs_dir = os.path.join(os.path.dirname(__file__), 'configs_mp') + configs_dir = os.path.join(os.path.dirname(__file__), 'configs') + inject.add_injectable('configs_dir', [mp_configs_dir, configs_dir]) + + output_dir = os.path.join(os.path.dirname(__file__), 'output') + inject.add_injectable("output_dir", output_dir) + + data_dir = os.path.join(os.path.dirname(__file__), 'data') + inject.add_injectable("data_dir", data_dir) + + tracing.config_logger() + + run_list = mp_tasks.get_run_list() + mp_tasks.print_run_list(run_list) + + # do this after config.handle_standard_args, as command line args may override injectables + injectables = ['data_dir', 'configs_dir', 'output_dir'] + injectables = {k: inject.get_injectable(k) for k in injectables} + + # pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) + + mp_tasks.run_multiprocess(run_list, injectables) + pipeline.open_pipeline('_') + regress_mini_auto() + pipeline.close_pipeline() + + +if __name__ == '__main__': + + test_mp_run() diff --git a/activitysim/abm/test/test_mp_pipeline.py b/activitysim/abm/test/test_mp_pipeline.py new file mode 100644 index 000000000..6ad871d19 --- /dev/null +++ b/activitysim/abm/test/test_mp_pipeline.py @@ -0,0 +1,24 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, unicode_literals) + +from builtins import * + +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +import os +import subprocess + + +def test_mp_run(): + + file_path = os.path.join(os.path.dirname(__file__), 'run_mp.py') + + subprocess.check_call(['coverage', 'run', file_path]) + + +if __name__ == '__main__': + + test_mp_run() diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 79787f500..31f25e144 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -11,6 +11,7 @@ from . import logit from . import tracing +from . import config from .simulate import set_skim_wrapper_targets from . import chunk @@ -85,7 +86,7 @@ def to_series(x): else: trace_eval_results = None - check_for_variability = tracing.check_for_variability() + check_for_variability = config.setting('check_for_variability') # need to be able to identify which variables causes an error, which keeps # this from being expressed more parsimoniously diff --git a/example_mp/tasks.py b/activitysim/core/mp_tasks.py similarity index 94% rename from example_mp/tasks.py rename to activitysim/core/mp_tasks.py index d9fcee6f9..e34f5c073 100644 --- a/example_mp/tasks.py +++ b/activitysim/core/mp_tasks.py @@ -17,7 +17,7 @@ import os import time import logging -import multiprocessing as mp +import multiprocessing from collections import OrderedDict import yaml @@ -90,7 +90,7 @@ def build_slice_rules(slice_info, tables): slicer_ref_cols = OrderedDict() # build slice rules for loaded tables - slice_rules = {} + slice_rules = OrderedDict() for table_name, df in iteritems(tables): rule = {} @@ -313,7 +313,7 @@ def setup_injectables_and_logging(injectables): inject.add_injectable("is_sub_task", True) - process_name = mp.current_process().name + process_name = multiprocessing.current_process().name inject.add_injectable("log_file_prefix", process_name) tracing.config_logger() @@ -330,7 +330,7 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): handle_standard_args() # do this before config_logger so log file is named appropriately - process_name = mp.current_process().name + process_name = multiprocessing.current_process().name if num_processes > 1: pipeline_prefix = process_name logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) @@ -418,10 +418,10 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name queues = [] for process_name in process_names: - q = mp.Queue() - p = mp.Process(target=mp_run_simulation, name=process_name, - args=(q, injectables, step_info, resume_after), - kwargs=shared_skim_buffer) + q = multiprocessing.Queue() + p = multiprocessing.Process(target=mp_run_simulation, name=process_name, + args=(q, injectables, step_info, resume_after), + kwargs=shared_skim_buffer) procs.append(p) queues.append(q) @@ -449,7 +449,7 @@ def idle(seconds): logger.info("start process %s", p.name) p.start() - while mp.active_children(): + while multiprocessing.active_children(): idle(1) log_queued_messages() @@ -468,6 +468,10 @@ def idle(seconds): def run_multiprocess(run_list, injectables): + if not run_list['multiprocess']: + raise RuntimeError("run_multiprocess called but multiprocess flag is %s" % + run_list['multiprocess']) + resume_journal = run_list.get('resume_journal', {}) run_status = OrderedDict() @@ -487,9 +491,9 @@ def update_journal(step_name, key, value): # - mp_setup_skims run_sub_task( - mp.Process(target=mp_setup_skims, name='mp_setup_skims', - args=(injectables,), - kwargs=shared_skim_buffer) + multiprocessing.Process( + target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), + kwargs=shared_skim_buffer) ) for step_info in run_list['multiprocess_steps']: @@ -513,8 +517,9 @@ def update_journal(step_name, key, value): if num_processes > 1: logger.info('apportioning households to sub_processes') run_sub_task( - mp.Process(target=mp_apportion_pipeline, name='%s_apportion' % step_name, - args=(injectables, sub_proc_names, slice_info)) + multiprocessing.Process( + target=mp_apportion_pipeline, name='%s_apportion' % step_name, + args=(injectables, sub_proc_names, slice_info)) ) update_journal(step_name, 'apportion', True) @@ -533,8 +538,9 @@ def update_journal(step_name, key, value): if num_processes > 1: logger.info('coalescing sub_process pipelines') run_sub_task( - mp.Process(target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, - args=(injectables, sub_proc_names, slice_info)) + multiprocessing.Process( + target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, + args=(injectables, sub_proc_names, slice_info)) ) update_journal(step_name, 'coalesce', True) @@ -599,7 +605,7 @@ def get_run_list(): global_chunk_size = setting('chunk_size', 0) multiprocess_steps = setting('multiprocess_steps', []) - if multiprocess and mp.cpu_count() == 1: + if multiprocess and multiprocessing.cpu_count() == 1: logger.warning("Can't multiprocess because there is only 1 cpu") multiprocess = False @@ -652,13 +658,13 @@ def get_run_list(): if num_processes == 0: logger.info("Setting num_processes = %s for step %s", num_processes, name) - num_processes = mp.cpu_count() + num_processes = multiprocessing.cpu_count() if num_processes == 1: raise RuntimeError("num_processes = 1 but found slice info for step %s" " in multiprocess_steps" % name) - if num_processes > mp.cpu_count(): + if num_processes > multiprocessing.cpu_count(): logger.warning("num_processes setting (%s) greater than cpu count (%s", - num_processes, mp.cpu_count()) + num_processes, multiprocessing.cpu_count()) else: if num_processes == 0: num_processes = 1 @@ -739,6 +745,11 @@ def get_run_list(): istep = len(resume_journal) - 1 multiprocess_steps[istep]['resume_after'] = resume_after + # - write run list to output dir + mode = 'wb' if sys.version_info < (3,) else 'w' + with open(config.output_file_path('run_list.txt'), mode) as f: + print_run_list(run_list, f) + return run_list @@ -773,11 +784,6 @@ def print_run_list(run_list, output_file=None): print("\nresume_journal:", file=output_file) print_journal(run_list['resume_journal'], output_file) - else: - print("models", file=output_file) - for m in run_list['models']: - print(" - ", m, file=output_file) - def print_journal(journal, output_file=None): diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index ddd8922de..fe80b4b70 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -482,11 +482,9 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, trace_label = tracing.extend_trace_label(trace_label, 'mnl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - check_for_variability = tracing.check_for_variability() - expression_values = eval_variables(spec.index, choosers, locals_d) - if check_for_variability: + if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) # matrix product of spec expression_values with utility coefficients of alternatives @@ -571,12 +569,10 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, trace_label = tracing.extend_trace_label(trace_label, 'nl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - check_for_variability = tracing.check_for_variability() - # column names of expression_values match spec index values expression_values = eval_variables(spec.index, choosers, locals_d) - if check_for_variability: + if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) # raw utilities of all the leaves @@ -797,8 +793,6 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): trace_label = tracing.extend_trace_label(trace_label, 'mnl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - check_for_variability = tracing.check_for_variability() - logger.debug("running eval_mnl_logsums") t00 = t0 = print_elapsed_time() @@ -810,7 +804,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): expression_values = eval_variables(spec.index, choosers, locals_d) t0 = print_elapsed_time("eval_variables", t0, debug=True) - if check_for_variability: + if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) # utility values @@ -871,7 +865,6 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): trace_label = tracing.extend_trace_label(trace_label, 'nl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - check_for_variability = tracing.check_for_variability() t00 = t0 = print_elapsed_time("begin eval_nl_logsums") @@ -885,7 +878,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): expression_values = eval_variables(spec.index, choosers, locals_d) t0 = print_elapsed_time("eval_variables", t0, debug=True) - if check_for_variability: + if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) t0 = print_elapsed_time("_check_for_variability", t0, debug=True) diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index a826e9eb3..e6103223a 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -41,7 +41,8 @@ def track_skim_usage(output_dir): skim_dict = inject.get_injectable('skim_dict') skim_stack = inject.get_injectable('skim_stack', None) - with open(config.output_file_path('skim_usage.txt'), 'wb') as file: + mode = 'wb' if sys.version_info < (3,) else 'w' + with open(config.output_file_path('skim_usage.txt'), mode) as file: print("\n### skim_dict usage", file=file) for key in skim_dict.usage: diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 4bd7d3551..4f9141d73 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -71,7 +71,7 @@ def test_eval_variables(spec, data): def test_simple_simulate(data, spec): - inject.add_injectable("check_for_variability", False) + inject.add_injectable("settings", {'check_for_variability': False}) choices = simulate.simple_simulate(choosers=data, spec=spec, nest_spec=None) expected = pd.Series([1, 1, 1], index=data.index) @@ -80,7 +80,7 @@ def test_simple_simulate(data, spec): def test_simple_simulate_chunked(data, spec): - inject.add_injectable("check_for_variability", False) + inject.add_injectable("settings", {'check_for_variability': False}) choices = simulate.simple_simulate(choosers=data, spec=spec, nest_spec=None, chunk_size=2) expected = pd.Series([1, 1, 1], index=data.index) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 6006addd1..c3f0d96ac 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -41,10 +41,6 @@ def log_file_path(file_name): return config.log_file_path(file_name) -def check_for_variability(): - return inject.get_injectable('check_for_variability', False) - - def extend_trace_label(trace_label, extension): if trace_label: trace_label = "%s.%s" % (trace_label, extension) diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 2cbd7a985..c0e53b68e 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -57,6 +57,7 @@ models: - trip_scheduling - trip_mode_choice - write_data_dictionary + - track_skim_usage - write_tables # to resume after last successful checkpoint, specify resume_after: _ diff --git a/example_mp/__init__.py b/example_mp/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index ba05c0fdb..e5bba3916 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -43,7 +43,7 @@ logging: stream: ext://sys.stdout formatter: simpleFormatter #level: WARNING - level: !!python/object/apply:tasks.if_sub_task [INFO, NOTSET] + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [DEBUG, NOTSET] mp_console: class: logging.StreamHandler @@ -56,7 +56,7 @@ logging: simpleFormatter: class: logging.Formatter format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' -# format: !!python/object/apply:tasks.if_sub_task ['%(processName)-10s %(levelname)s - %(name)s - %(message)s', +# format: !!python/object/apply:mp_tasks.if_sub_task ['%(processName)-10s %(levelname)s - %(name)s - %(message)s', # '%(levelname)s - %(name)s - %(message)s'] datefmt: '%d/%m/%Y %H:%M:%S' diff --git a/example_mp/simulation.py b/example_mp/simulation.py index f9536ebc9..e8506e3c4 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -16,9 +16,9 @@ from activitysim.core import tracing from activitysim.core import config from activitysim.core import pipeline +from activitysim.core import mp_tasks # from activitysim import abm -import tasks logger = logging.getLogger('activitysim') @@ -40,41 +40,30 @@ def cleanup_output_files(): # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') inject.add_injectable('data_dir', '../example/data') inject.add_injectable('configs_dir', ['configs', '../example/configs']) - # inject.add_injectable('configs_dir', '../example/configs') config.handle_standard_args() tracing.config_logger() t0 = tracing.print_elapsed_time() - injectables = ['data_dir', 'configs_dir', 'output_dir'] - injectables = {k: inject.get_injectable(k) for k in injectables} - # cleanup if not resuming if not config.setting('resume_after', False): cleanup_output_files() - run_list = tasks.get_run_list() - - mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path('run_list.txt'), mode) as f: - tasks.print_run_list(run_list, f) - - tasks.print_run_list(run_list) - bug + run_list = mp_tasks.get_run_list() + # mp_tasks.print_run_list(run_list) if run_list['multiprocess']: logger.info("run multiprocess simulation") - tasks.run_multiprocess(run_list, injectables) + # do this after config.handle_standard_args, as command line args may override injectables + injectables = ['data_dir', 'configs_dir', 'output_dir'] + injectables = {k: inject.get_injectable(k) for k in injectables} + mp_tasks.run_multiprocess(run_list, injectables) else: logger.info("run single process simulation") - - # tasks.run_simulation(run_list['models'], run_list['resume_after']) pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) - - # tables will no longer be available after pipeline is closed pipeline.close_pipeline() t0 = tracing.print_elapsed_time("everything", t0) From 4b6f85916b6e44d91a457d2a6db43f4072b6b10d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 17 Oct 2018 15:44:14 -0400 Subject: [PATCH 026/122] reorg mp_tasks for profiling --- activitysim/core/mp_tasks.py | 196 ++++++++++++++++--------------- example_mp/configs/logging.yaml | 7 +- example_mp/configs/settings.yaml | 11 +- 3 files changed, 111 insertions(+), 103 deletions(-) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index e34f5c073..957ffe840 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -43,6 +43,11 @@ logger = logging.getLogger('activitysim') +""" + child process methods (called within sub process) +""" + + def pipeline_table_keys(pipeline_store, checkpoint_name=None): checkpoints = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] @@ -318,38 +323,23 @@ def setup_injectables_and_logging(injectables): tracing.config_logger() -def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): - - skim_buffer = kwargs +def run_simulation(queue, injectables, step_info, resume_after, skim_buffer): step_label = step_info['name'] models = step_info['models'] - num_processes = step_info['num_processes'] chunk_size = step_info['chunk_size'] - handle_standard_args() - - # do this before config_logger so log file is named appropriately - process_name = multiprocessing.current_process().name - if num_processes > 1: - pipeline_prefix = process_name - logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) - inject.add_injectable("pipeline_file_prefix", pipeline_prefix) - - setup_injectables_and_logging(injectables) + inject.add_injectable('skim_buffer', skim_buffer) + inject.add_injectable("chunk_size", chunk_size) - logger.info("mp_run_simulation %s num_processes %s", process_name, num_processes) if resume_after: logger.info('resume_after %s', resume_after) - inject.add_injectable('skim_buffer', skim_buffer) - inject.add_injectable("chunk_size", chunk_size) - - if resume_after and resume_after != '_': # if they specified a resume_after model, check to make sure it is checkpointed - if resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: + if resume_after != '_' \ + and resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: # if not checkpointed, then fall back to last checkpoint - logger.warning("resume_after checkpoint '%s' not in pipeline", resume_after) + logger.info("resume_after checkpoint '%s' not in pipeline.", resume_after) resume_after = '_' pipeline.open_pipeline(resume_after) @@ -376,6 +366,26 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): pipeline.close_pipeline() +""" + multiprocessing entry points +""" + + +def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): + + skim_buffer = kwargs + handle_standard_args() + setup_injectables_and_logging(injectables) + + process_name = multiprocessing.current_process().name + if step_info['num_processes'] > 1: + pipeline_prefix = process_name + logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) + inject.add_injectable("pipeline_file_prefix", pipeline_prefix) + + run_simulation(queue, injectables, step_info, resume_after, skim_buffer) + + def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): setup_injectables_and_logging(injectables) apportion_pipeline(sub_job_proc_names, slice_info) @@ -392,15 +402,9 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): coalesce_pipelines(sub_job_proc_names, slice_info) -def run_sub_task(p): - logger.info("running sub_process %s", p.name) - p.start() - p.join() - # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) - - if p.exitcode: - logger.error("Process %s returned exitcode %s", p.name, p.exitcode) - raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) +""" + main (parent) process methods +""" def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_names, resume_after): @@ -466,25 +470,35 @@ def idle(seconds): return error_count +def run_sub_task(p): + logger.info("running sub_process %s", p.name) + p.start() + p.join() + # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) + + if p.exitcode: + logger.error("Process %s returned exitcode %s", p.name, p.exitcode) + raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) + + def run_multiprocess(run_list, injectables): if not run_list['multiprocess']: raise RuntimeError("run_multiprocess called but multiprocess flag is %s" % run_list['multiprocess']) - resume_journal = run_list.get('resume_journal', {}) - run_status = OrderedDict() + old_breadcrumbs = run_list.get('breadcrumbs', {}) + new_breadcrumbs = OrderedDict() - def skip(step_name, key): - already_did_this = resume_journal and resume_journal.get(step_name, {}).get(key, False) - if already_did_this: - logger.info("Skipping %s %s", step_name, key) - time.sleep(1) - return already_did_this + def skip_phase(phase): + skip = old_breadcrumbs and old_breadcrumbs.get(step_name, {}).get(phase, False) + if skip: + logger.info("Skipping %s %s", step_name, phase) + return skip - def update_journal(step_name, key, value): - run_status.setdefault(step_name, {'name': step_name})[key] = value - save_journal(run_status) + def drop_breadcrumb(phase): + new_breadcrumbs.setdefault(step_name, {'name': step_name})[phase] = True + write_breadcrumbs(new_breadcrumbs) logger.info('setup shared skim data') shared_skim_buffer = allocate_shared_skim_buffer() @@ -508,12 +522,10 @@ def update_journal(step_name, key, value): else: sub_proc_names = ["%s_%s" % (step_name, i) for i in range(num_processes)] - update_journal(step_name, 'sub_proc_names', sub_proc_names) - logger.info('running step %s with %s processes', step_name, num_processes,) # - mp_apportion_pipeline - if not skip(step_name, 'apportion'): + if not skip_phase('apportion'): if num_processes > 1: logger.info('apportioning households to sub_processes') run_sub_task( @@ -521,20 +533,19 @@ def update_journal(step_name, key, value): target=mp_apportion_pipeline, name='%s_apportion' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - update_journal(step_name, 'apportion', True) + drop_breadcrumb('apportion') # - run_sub_simulations - if not skip(step_name, 'simulate'): + if not skip_phase('simulate'): error_count = \ run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, resume_after=step_info.get('resume_after', None)) if error_count: raise RuntimeError("%s processes failed in step %s" % (error_count, step_name)) - - update_journal(step_name, 'simulate', True) + drop_breadcrumb('simulate') # - mp_coalesce_pipelines - if not skip(step_name, 'coalesce'): + if not skip_phase('coalesce'): if num_processes > 1: logger.info('coalescing sub_process pipelines') run_sub_task( @@ -542,59 +553,58 @@ def update_journal(step_name, key, value): target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - update_journal(step_name, 'coalesce', True) + drop_breadcrumb('coalesce') -def get_resume_journal(run_list): +def get_breadcrumbs(run_list): resume_after = run_list['resume_after'] assert resume_after is not None - previous_journal = read_journal() + breadcrumbs = read_breadcrumbs() - if not previous_journal: - logger.error("empty journal for resume_after '%s'", resume_after) - raise RuntimeError("empty journal for resume_after '%s'" % resume_after) + if not breadcrumbs: + logger.error("empty breadcrumbs for resume_after '%s'", resume_after) + raise RuntimeError("empty breadcrumbs for resume_after '%s'" % resume_after) if resume_after == '_': - resume_step_name = list(previous_journal.keys())[-1] + resume_step_name = list(breadcrumbs.keys())[-1] else: - previous_steps = list(previous_journal.keys()) + previous_steps = list(breadcrumbs.keys()) # run_list step resume_after is in resume_step_name = next((step['name'] for step in run_list['multiprocess_steps'] if resume_after in step['models']), None) if resume_step_name not in previous_steps: - logger.error("resume_after model '%s' not in journal", resume_after) - raise RuntimeError("resume_after model '%s' not in journal" % resume_after) + logger.error("resume_after model '%s' not in breadcrumbs", resume_after) + raise RuntimeError("resume_after model '%s' not in breadcrumbs" % resume_after) - # drop any previous_journal steps after resume_step + # drop any previous_breadcrumbs steps after resume_step for step in previous_steps[previous_steps.index(resume_step_name) + 1:]: - del previous_journal[step] + del breadcrumbs[step] multiprocess_step = next((step for step in run_list['multiprocess_steps'] - if step['name'] == resume_step_name), []) + if step['name'] == resume_step_name), []) # type: dict - print("resume_step_models", multiprocess_step['models']) if resume_after in multiprocess_step['models'][:-1]: # if resume_after is specified by name, and is not the last model in the step # then we need to rerun the simulations, even if they succeeded - if previous_journal[resume_step_name].get('simulate', None): - previous_journal[resume_step_name]['simulate'] = None + if breadcrumbs[resume_step_name].get('simulate', None): + breadcrumbs[resume_step_name]['simulate'] = None - if previous_journal[resume_step_name].get('coalesce', None): - previous_journal[resume_step_name]['coalesce'] = None + if breadcrumbs[resume_step_name].get('coalesce', None): + breadcrumbs[resume_step_name]['coalesce'] = None multiprocess_step_names = [step['name'] for step in run_list['multiprocess_steps']] - if list(previous_journal.keys()) != multiprocess_step_names[:len(previous_journal)]: + if list(breadcrumbs.keys()) != multiprocess_step_names[:len(breadcrumbs)]: raise RuntimeError("last run steps don't match run list: %s" % - list(previous_journal.keys())) + list(breadcrumbs.keys())) - return previous_journal + return breadcrumbs def get_run_list(): @@ -736,13 +746,13 @@ def get_run_list(): run_list['multiprocess_steps'] = multiprocess_steps - # - add resume_journal + # - add resume_breadcrumbs if resume_after: - resume_journal = get_resume_journal(run_list) - if resume_journal: - run_list['resume_journal'] = resume_journal + breadcrumbs = get_breadcrumbs(run_list) + if breadcrumbs: + run_list['breadcrumbs'] = breadcrumbs # - add resume_after to resume_step - istep = len(resume_journal) - 1 + istep = len(breadcrumbs) - 1 multiprocess_steps[istep]['resume_after'] = resume_after # - write run list to output dir @@ -780,18 +790,18 @@ def print_run_list(run_list, output_file=None): else: print(" %s: %s" % (k, step[k]), file=output_file) - if run_list.get('resume_journal'): - print("\nresume_journal:", file=output_file) - print_journal(run_list['resume_journal'], output_file) + if run_list.get('breadcrumbs'): + print("\nbreadcrumbs:", file=output_file) + print_breadcrumbs(run_list['breadcrumbs'], output_file) -def print_journal(journal, output_file=None): +def print_breadcrumbs(breadcrumbs, output_file=None): if output_file is None: output_file = sys.stdout - for step_name in journal: - step = journal[step_name] + for step_name in breadcrumbs: + step = breadcrumbs[step_name] print(" step:", step_name, file=output_file) for k in step: if isinstance(k, str): @@ -802,25 +812,25 @@ def print_journal(journal, output_file=None): print(" ", v, file=output_file) -def journal_file_path(file_name=None): - return config.build_output_file_path(file_name or 'journal.yaml') +def breadcrumbs_file_path(): + return config.build_output_file_path('breadcrumbs.yaml') -def read_journal(file_name=None): - file_path = journal_file_path(file_name) +def read_breadcrumbs(): + file_path = breadcrumbs_file_path() if not os.path.exists(file_path): - raise IOError("Could not find saved journal file '%s'" % file_path) + raise IOError("Could not find saved breadcrumbs file '%s'" % file_path) with open(file_path, 'r') as f: - journal = yaml.load(f) + breadcrumbs = yaml.load(f) - journal = OrderedDict([(step['name'], step) for step in journal]) - return journal + breadcrumbs = OrderedDict([(step['name'], step) for step in breadcrumbs]) + return breadcrumbs -def save_journal(journal, file_name=None): - with open(journal_file_path(file_name), 'w') as f: - journal = [step for step in list(journal.values())] - yaml.dump(journal, f) +def write_breadcrumbs(breadcrumbs): + with open(breadcrumbs_file_path(), 'w') as f: + breadcrumbs = [step for step in list(breadcrumbs.values())] + yaml.dump(breadcrumbs, f) def is_sub_task(): diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index e5bba3916..d69998f6e 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -55,9 +55,10 @@ logging: simpleFormatter: class: logging.Formatter - format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' -# format: !!python/object/apply:mp_tasks.if_sub_task ['%(processName)-10s %(levelname)s - %(name)s - %(message)s', -# '%(levelname)s - %(name)s - %(message)s'] + #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' + format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ + '%(processName)-10s %(levelname)s - %(name)s - %(message)s', + '%(levelname)s - %(name)s - %(message)s'] datefmt: '%d/%m/%Y %H:%M:%S' fileFormatter: diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index af0a03539..7243a07e3 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,8 +1,11 @@ inherit_settings: True -#number of households to simulate households_sample_size: 100 +multiprocess: False +chunk_size: 3000000000 + +check_for_variability: False #trace household id; comment out for no trace # household with all tour categories @@ -11,9 +14,6 @@ trace_hh_id: 1269102 # trace origin, destination in accessibility calculation trace_od: [5, 11] -# comment out or set false to disable variability check in simple_simulate and interaction_simulate -check_for_variability: False - models: - initialize_landuse - compute_accessibility @@ -61,9 +61,6 @@ models: -# comment out to run single-threaded -multiprocess: False -chunk_size: 4000000000 # 160 GB #chunk_size: 10000000000 From e159840cf5f38a788c8a52adfe5aca55c3ba8dfb Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 18 Oct 2018 14:22:39 -0400 Subject: [PATCH 027/122] mp_tasks profiling hooks --- .../abm/models/atwork_subtour_destination.py | 6 +- .../abm/models/atwork_subtour_frequency.py | 4 +- .../abm/models/atwork_subtour_scheduling.py | 2 +- activitysim/abm/models/auto_ownership.py | 2 +- activitysim/abm/models/cdap.py | 2 +- .../abm/models/joint_tour_destination.py | 10 +- .../abm/models/joint_tour_participation.py | 10 +- .../abm/models/joint_tour_scheduling.py | 2 +- .../abm/models/mandatory_scheduling.py | 2 +- .../abm/models/mandatory_tour_frequency.py | 4 +- .../abm/models/non_mandatory_scheduling.py | 6 +- .../models/non_mandatory_tour_frequency.py | 4 +- activitysim/abm/models/school_location.py | 4 +- activitysim/abm/models/trip_destination.py | 18 +-- activitysim/abm/models/trip_mode_choice.py | 2 +- activitysim/abm/models/trip_purpose.py | 14 +-- activitysim/abm/models/trip_scheduling.py | 4 +- activitysim/abm/models/util/cdap.py | 58 ++++++++- activitysim/abm/models/util/test/test_cdap.py | 2 +- activitysim/abm/tables/households.py | 4 +- .../abm/test/configs/override_hh_ids.csv | 11 ++ activitysim/abm/test/test_pipeline.py | 119 ++++++++++-------- activitysim/core/assign.py | 10 +- activitysim/core/config.py | 13 +- activitysim/core/mp_tasks.py | 80 +++++++----- activitysim/core/pipeline.py | 2 + activitysim/core/tracing.py | 32 ++++- docs/abmexample.rst | 8 +- example/configs/logging.yaml | 2 +- example_mp/configs/logging.yaml | 2 +- example_mp/configs/settings.yaml | 8 +- example_mp/output/.gitignore | 1 + example_mp/simulation.py | 28 +++-- example_multi/configs/logging.yaml | 2 +- example_multi/extensions/models.py | 15 +-- 35 files changed, 322 insertions(+), 171 deletions(-) create mode 100644 activitysim/abm/test/configs/override_hh_ids.csv diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index f8bd306e9..fd575cabe 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -64,7 +64,7 @@ def atwork_subtour_destination_sample(tours, sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - logger.info("Running atwork_subtour_location_sample with %d tours" % len(choosers)) + logger.info("Running atwork_subtour_location_sample with %d tours", len(choosers)) # create wrapper with keys for this lookup - in this case there is a workplace_taz # in the choosers and a TAZ in the alternatives which get merged during interaction @@ -143,7 +143,7 @@ def atwork_subtour_destination_logsums(persons_merged, right_index=True, how="left") - logger.info("Running %s with %s rows" % (trace_label, len(choosers))) + logger.info("Running %s with %s rows", trace_label, len(choosers)) tracing.dump_df(DUMP, persons_merged, trace_label, 'persons_merged') tracing.dump_df(DUMP, choosers, trace_label, 'choosers') @@ -217,7 +217,7 @@ def atwork_subtour_destination_simulate(tours, constants = config.get_model_constants(model_settings) - logger.info("Running atwork_subtour_destination_simulate with %d persons" % len(choosers)) + logger.info("Running atwork_subtour_destination_simulate with %d persons", len(choosers)) # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers # and a TAZ in the alternatives which get merged during interaction diff --git a/activitysim/abm/models/atwork_subtour_frequency.py b/activitysim/abm/models/atwork_subtour_frequency.py index 0f93679a1..40598709e 100644 --- a/activitysim/abm/models/atwork_subtour_frequency.py +++ b/activitysim/abm/models/atwork_subtour_frequency.py @@ -23,7 +23,7 @@ def add_null_results(trace_label, tours): - logger.info("Skipping %s: add_null_results" % trace_label) + logger.info("Skipping %s: add_null_results", trace_label) tours['atwork_subtour_frequency'] = np.nan pipeline.replace_table("tours", tours) @@ -61,7 +61,7 @@ def atwork_subtour_frequency(tours, # merge persons into work_tours work_tours = pd.merge(work_tours, persons_merged, left_on='person_id', right_index=True) - logger.info("Running atwork_subtour_frequency with %d work tours" % len(work_tours)) + logger.info("Running atwork_subtour_frequency with %d work tours", len(work_tours)) nest_spec = config.get_logit_model_settings(model_settings) constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index b8850fb45..b2d06e055 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -49,7 +49,7 @@ def atwork_subtour_scheduling( tracing.no_results(trace_label) return - logger.info("Running %s with %d tours" % (trace_label, len(subtours))) + logger.info("Running %s with %d tours", trace_label, len(subtours)) # preprocessor constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/models/auto_ownership.py b/activitysim/abm/models/auto_ownership.py index a8e0d3143..a4d1f16aa 100644 --- a/activitysim/abm/models/auto_ownership.py +++ b/activitysim/abm/models/auto_ownership.py @@ -26,7 +26,7 @@ def auto_ownership_simulate(households, trace_label = 'auto_ownership_simulate' model_settings = config.read_model_settings('auto_ownership.yaml') - logger.info("Running %s with %d households" % (trace_label, len(households_merged))) + logger.info("Running %s with %d households", trace_label, len(households_merged)) model_spec = simulate.read_model_spec(file_name='auto_ownership.csv') diff --git a/activitysim/abm/models/cdap.py b/activitysim/abm/models/cdap.py index eb8f761f7..76d06d9f1 100644 --- a/activitysim/abm/models/cdap.py +++ b/activitysim/abm/models/cdap.py @@ -73,7 +73,7 @@ def cdap_simulate(persons_merged, persons, households, constants = config.get_model_constants(model_settings) - logger.info("Running cdap_simulate with %d persons" % len(persons_merged.index)) + logger.info("Running cdap_simulate with %d persons", len(persons_merged.index)) choices = run_cdap(persons=persons_merged, cdap_indiv_spec=cdap_indiv_spec, diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index a192e0566..b4162c976 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -134,7 +134,7 @@ def joint_tour_destination_sample( if constants is not None: locals_d.update(constants) - logger.info("Running joint_tour_destination_sample with %d joint_tours" % len(choosers)) + logger.info("Running joint_tour_destination_sample with %d joint_tours", len(choosers)) choices_list = [] # segment by trip type and pick the right spec for each person type @@ -146,7 +146,7 @@ def joint_tour_destination_sample( choosers_segment = choosers[choosers.tour_type == tour_type] if choosers_segment.shape[0] == 0: - logger.info("%s skipping tour_type %s: no tours" % (trace_label, tour_type)) + logger.info("%s skipping tour_type %s: no tours", trace_label, tour_type) continue # alts indexed by taz with one column containing size_term for this tour_type @@ -235,7 +235,7 @@ def joint_tour_destination_logsums( choosers = destination_sample[destination_sample['tour_type_id'] == tour_type_id] if choosers.shape[0] == 0: - logger.info("%s skipping tour_type %s: no tours" % (trace_label, tour_type)) + logger.info("%s skipping tour_type %s: no tours", trace_label, tour_type) continue # sample is sorted by TOUR_TYPE_ID, tour_id @@ -249,7 +249,7 @@ def joint_tour_destination_logsums( how="left", sort=False) - logger.info("%s running %s with %s rows" % (trace_label, tour_type, len(choosers))) + logger.info("%s running %s with %s rows", trace_label, tour_type, len(choosers)) tour_purpose = tour_type logsums = logsum.compute_logsums( @@ -343,7 +343,7 @@ def joint_tour_destination_simulate( # - skip empty segments if choosers_segment.shape[0] == 0: - logger.info("%s skipping tour_type %s: no tours" % (trace_label, tour_type)) + logger.info("%s skipping tour_type %s: no tours", trace_label, tour_type) continue alts_segment = destination_sample[destination_sample.tour_type_id == tour_type_id] diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index c8f029989..45e7036ca 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -55,7 +55,7 @@ def joint_tour_participation_candidates(joint_tours, persons_merged): MAX_PNUM = 100 if candidates.PNUM.max() > MAX_PNUM: # if this happens, channel random seeds will overlap at MAX_PNUM (not probably a big deal) - logger.warning("max persons.PNUM (%s) > MAX_PNUM (%s)" % (candidates.PNUM.max(), MAX_PNUM)) + logger.warning("max persons.PNUM (%s) > MAX_PNUM (%s)", candidates.PNUM.max(), MAX_PNUM) candidates['participant_id'] = (candidates[joint_tours.index.name] * MAX_PNUM) + candidates.PNUM candidates.set_index('participant_id', drop=True, inplace=True, verify_integrity=True) @@ -146,7 +146,7 @@ def participants_chooser(probs, choosers, spec, trace_label): rands_list = [] num_tours_remaining = len(candidates.tour_id.unique()) - logger.info('%s %s joint tours to satisfy.' % (trace_label, num_tours_remaining,)) + logger.info('%s %s joint tours to satisfy.', trace_label, num_tours_remaining,) iter = 0 while candidates.shape[0] > 0: @@ -154,7 +154,7 @@ def participants_chooser(probs, choosers, spec, trace_label): iter += 1 if iter > MAX_ITERATIONS: - logger.warning('%s max iterations exceeded (%s).' % (trace_label, MAX_ITERATIONS)) + logger.warning('%s max iterations exceeded (%s).', trace_label, MAX_ITERATIONS) diagnostic_cols = ['tour_id', 'household_id', 'composition', 'adult'] unsatisfied_candidates = candidates[diagnostic_cols].join(probs) tracing.write_csv(unsatisfied_candidates, @@ -194,7 +194,7 @@ def participants_chooser(probs, choosers, spec, trace_label): assert choices.index.equals(choosers.index) assert rands.index.equals(choosers.index) - logger.info('%s %s iterations to satisfy all joint tours.' % (trace_label, iter,)) + logger.info('%s %s iterations to satisfy all joint tours.', trace_label, iter,) return choices, rands @@ -211,7 +211,7 @@ def annotate_jtp(model_settings, trace_label): def add_null_results(model_settings, trace_label): - logger.info("Skipping %s: joint tours" % trace_label) + logger.info("Skipping %s: joint tours", trace_label) # participants table is used downstream in non-joint tour expressions participants = pd.DataFrame(columns=['person_id']) participants.index.name = 'participant_id' diff --git a/activitysim/abm/models/joint_tour_scheduling.py b/activitysim/abm/models/joint_tour_scheduling.py index 7febe3980..86436be97 100644 --- a/activitysim/abm/models/joint_tour_scheduling.py +++ b/activitysim/abm/models/joint_tour_scheduling.py @@ -47,7 +47,7 @@ def joint_tour_scheduling( persons_merged = persons_merged.to_frame() - logger.info("Running %s with %d joint tours" % (trace_label, joint_tours.shape[0])) + logger.info("Running %s with %d joint tours", trace_label, joint_tours.shape[0]) # it may seem peculiar that we are concerned with persons rather than households # but every joint tour is (somewhat arbitrarily) assigned a "primary person" diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index 7071521e9..39e97ce62 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -48,7 +48,7 @@ def mandatory_tour_scheduling(tours, model_constants = config.get_model_constants(model_settings) - logger.info("Running mandatory_tour_scheduling with %d tours" % len(tours)) + logger.info("Running mandatory_tour_scheduling with %d tours", len(tours)) tdd_choices = vectorize_tour_scheduling( mandatory_tours, persons_merged, tdd_alts, diff --git a/activitysim/abm/models/mandatory_tour_frequency.py b/activitysim/abm/models/mandatory_tour_frequency.py index 51eb40c7f..6f5f3e676 100644 --- a/activitysim/abm/models/mandatory_tour_frequency.py +++ b/activitysim/abm/models/mandatory_tour_frequency.py @@ -20,7 +20,7 @@ def add_null_results(trace_label, mandatory_tour_frequency_settings): - logger.info("Skipping %s: add_null_results" % trace_label) + logger.info("Skipping %s: add_null_results", trace_label) persons = inject.get_table('persons').to_frame() persons['mandatory_tour_frequency'] = '' @@ -58,7 +58,7 @@ def mandatory_tour_frequency(persons_merged, choosers = persons_merged.to_frame() # filter based on results of CDAP choosers = choosers[choosers.cdap_activity == 'M'] - logger.info("Running mandatory_tour_frequency with %d persons" % len(choosers)) + logger.info("Running mandatory_tour_frequency with %d persons", len(choosers)) # - if no mandatory tours if choosers.shape[0] == 0: diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index 15992b1f5..08340c785 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -1,12 +1,8 @@ # ActivitySim # See full license in LICENSE.txt. -import os import logging -import pandas as pd - -from activitysim.core import simulate as asim from activitysim.core import tracing from activitysim.core import config from activitysim.core import inject @@ -42,7 +38,7 @@ def non_mandatory_tour_scheduling(tours, non_mandatory_tours = tours[tours.tour_category == 'non_mandatory'] - logger.info("Running non_mandatory_tour_scheduling with %d tours" % len(tours)) + logger.info("Running non_mandatory_tour_scheduling with %d tours", len(tours)) constants = config.get_model_constants(model_settinsg) diff --git a/activitysim/abm/models/non_mandatory_tour_frequency.py b/activitysim/abm/models/non_mandatory_tour_frequency.py index 6a7b0bb49..108777cde 100644 --- a/activitysim/abm/models/non_mandatory_tour_frequency.py +++ b/activitysim/abm/models/non_mandatory_tour_frequency.py @@ -70,7 +70,7 @@ def non_mandatory_tour_frequency(persons, persons_merged, # filter based on results of CDAP choosers = choosers[choosers.cdap_activity.isin(['M', 'N'])] - logger.info("Running non_mandatory_tour_frequency with %d persons" % len(choosers)) + logger.info("Running non_mandatory_tour_frequency with %d persons", len(choosers)) constants = config.get_model_constants(model_settings) @@ -86,7 +86,7 @@ def non_mandatory_tour_frequency(persons, persons_merged, # drop any zero-valued rows spec = spec[spec[name] != 0] - logger.info("Running segment '%s' of size %d" % (name, len(segment))) + logger.info("Running segment '%s' of size %d", name, len(segment)) choices = interaction_simulate( segment, diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index b10717ea4..d3a048ff0 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -76,7 +76,7 @@ def school_location_sample( sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - logger.info("Running school_location_simulate with %d persons" % len(choosers)) + logger.info("Running school_location_simulate with %d persons", len(choosers)) # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers # and a TAZ in the alternatives which get merged during interaction @@ -98,7 +98,7 @@ def school_location_sample( choosers_segment = choosers[choosers["is_" + school_type]] if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) + logger.info("%s skipping school_type %s: no choosers", model_name, school_type) continue # alts indexed by taz with one column containing size_term for this tour_type diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index ee2a3ce2f..127213922 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -83,7 +83,7 @@ def trip_destination_sample( sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST"] - logger.info("Running %s with %d trips" % (trace_label, trips.shape[0])) + logger.info("Running %s with %d trips", trace_label, trips.shape[0]) locals_dict = config.get_model_constants(model_settings).copy() locals_dict.update({ @@ -165,7 +165,7 @@ def compute_logsums( adds od_logsum and dp_logsum columns to trips (in place) """ trace_label = tracing.extend_trace_label(trace_label, 'compute_logsums') - logger.info("Running %s with %d samples" % (trace_label, destination_sample.shape[0])) + logger.info("Running %s with %d samples", trace_label, destination_sample.shape[0]) # - trips_merged - merge trips and tours_merged trips_merged = pd.merge( @@ -251,7 +251,7 @@ def trip_destination_simulate( alt_dest_col_name = model_settings["ALT_DEST"] - logger.info("Running trip_destination_simulate with %d trips" % len(trips)) + logger.info("Running trip_destination_simulate with %d trips", len(trips)) locals_dict = config.get_model_constants(model_settings).copy() locals_dict.update({ @@ -273,7 +273,7 @@ def trip_destination_simulate( # drop any failed zero_prob destinations if (destinations == NO_DESTINATION).any(): - # logger.debug("dropping %s failed destinations" % (destinations == NO_DESTINATION).sum()) + # logger.debug("dropping %s failed destinations", destinations == NO_DESTINATION).sum() destinations = destinations[destinations != NO_DESTINATION] return destinations @@ -289,7 +289,7 @@ def choose_trip_destination( chunk_size, trace_hh_id, trace_label): - logger.info("choose_trip_destination %s with %d trips" % (trace_label, trips.shape[0])) + logger.info("choose_trip_destination %s with %d trips", trace_label, trips.shape[0]) # FIXME want timing? t0 = print_elapsed_time() @@ -484,7 +484,7 @@ def run_trip_destination( locals_dict=config.get_model_constants(model_settings), trace_label=nth_trace_label) - logger.info("Running %s with %d trips" % (nth_trace_label, nth_trips.shape[0])) + logger.info("Running %s with %d trips", nth_trace_label, nth_trips.shape[0]) # - choose destination for nth_trips, segmented by primary_purpose choices_list = [] @@ -542,7 +542,7 @@ def trip_destination( trips_df = trips.to_frame() tours_merged_df = tours_merged.to_frame() - logger.info("Running %s with %d trips" % (trace_label, trips_df.shape[0])) + logger.info("Running %s with %d trips", trace_label, trips_df.shape[0]) trips_df = run_trip_destination( trips_df, @@ -551,9 +551,9 @@ def trip_destination( trace_label) if trips_df.failed.any(): - logger.warning("%s %s failed trips" % (trace_label, trips_df.failed.sum())) + logger.warning("%s %s failed trips", trace_label, trips_df.failed.sum()) file_name = "%s_failed_trips" % trace_label - logger.info("writing failed trips to %s" % file_name) + logger.info("writing failed trips to %s", file_name) tracing.write_csv(trips_df[trips_df.failed], file_name=file_name, transpose=False) if CLEANUP: diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index db0ed6ad1..3b33e4919 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -49,7 +49,7 @@ def trip_mode_choice( assign.read_constant_spec(config.config_file_path(model_settings['COEFFS'])) trips_df = trips.to_frame() - logger.info("Running %s with %d trips" % (trace_label, trips_df.shape[0])) + logger.info("Running %s with %d trips", trace_label, trips_df.shape[0]) tours_merged = tours_merged.to_frame() tours_merged = tours_merged[model_settings['TOURS_MERGED_CHOOSER_COLUMNS']] diff --git a/activitysim/abm/models/trip_purpose.py b/activitysim/abm/models/trip_purpose.py index 2b9751aa7..3d7fc8d7f 100644 --- a/activitysim/abm/models/trip_purpose.py +++ b/activitysim/abm/models/trip_purpose.py @@ -45,9 +45,9 @@ def trip_purpose_rpc(chunk_size, choosers, spec, trace_label): row_size = chooser_row_size + extra_columns - # logger.debug("%s #chunk_calc choosers %s" % (trace_label, choosers.shape)) - # logger.debug("%s #chunk_calc spec %s" % (trace_label, spec.shape)) - # logger.debug("%s #chunk_calc extra_columns %s" % (trace_label, extra_columns)) + # logger.debug("%s #chunk_calc choosers %s", trace_label, choosers.shape) + # logger.debug("%s #chunk_calc spec %s", trace_label, spec.shape) + # logger.debug("%s #chunk_calc extra_columns %s", trace_label, extra_columns) return chunk.rows_per_chunk(chunk_size, row_size, num_choosers, trace_label) @@ -128,17 +128,17 @@ def run_trip_purpose( last_trip = (trips_df.trip_num == trips_df.trip_count) purpose = trips_df.primary_purpose[last_trip & trips_df.outbound] result_list.append(purpose) - logger.info("assign purpose to %s last outbound trips" % purpose.shape[0]) + logger.info("assign purpose to %s last outbound trips", purpose.shape[0]) # - last trip of inbound tour gets home (or work for atwork subtours) purpose = trips_df.primary_purpose[last_trip & ~trips_df.outbound] purpose = pd.Series(np.where(purpose == 'atwork', 'Work', 'Home'), index=purpose.index) result_list.append(purpose) - logger.info("assign purpose to %s last inbound trips" % purpose.shape[0]) + logger.info("assign purpose to %s last inbound trips", purpose.shape[0]) # - intermediate stops (non-last trips) purpose assigned by probability table trips_df = trips_df[~last_trip] - logger.info("assign purpose to %s intermediate trips" % trips_df.shape[0]) + logger.info("assign purpose to %s intermediate trips", trips_df.shape[0]) preprocessor_settings = model_settings.get('preprocessor', None) if preprocessor_settings: @@ -157,7 +157,7 @@ def run_trip_purpose( for i, num_chunks, trips_chunk in chunk.chunked_choosers(trips_df, rows_per_chunk): - logger.info("Running chunk %s of %s size %d" % (i, num_chunks, len(trips_chunk))) + logger.info("Running chunk %s of %s size %d", i, num_chunks, len(trips_chunk)) chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 8ceffb4a0..3c26c76e3 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -522,7 +522,7 @@ def trip_scheduling( last_iteration = (i == max_iterations) trace_label_i = tracing.extend_trace_label(trace_label, "i%s" % i) - logger.info("%s scheduling %s trips" % (trace_label_i, trips_df.shape[0])) + logger.info("%s scheduling %s trips", trace_label_i, trips_df.shape[0]) choices = \ run_trip_scheduling( @@ -537,7 +537,7 @@ def trip_scheduling( # boolean series of trips whose individual trip scheduling failed failed = choices.reindex(trips_df.index).isnull() - logger.info("%s %s failed" % (trace_label_i, failed.sum())) + logger.info("%s %s failed", trace_label_i, failed.sum()) if not last_iteration: # boolean series of trips whose leg scheduling failed diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 81a8dd840..5031fba56 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -6,6 +6,7 @@ import logging import itertools +import os import numpy as np import pandas as pd @@ -18,6 +19,8 @@ from activitysim.core import chunk from activitysim.core import logit from activitysim.core import tracing +from activitysim.core import inject +from activitysim.core import config logger = logging.getLogger(__name__) @@ -246,8 +249,48 @@ def preprocess_interaction_coefficients(interaction_coefficients): return coefficients +def cached_spec_name(hhsize): + return 'cdap_spec_%s.csv' % hhsize + + +def cached_spec_path(spec_name): + return config.output_file_path(spec_name) + + +def get_cached_spec(hhsize): + + spec_name = cached_spec_name(hhsize) + + spec = inject.get_injectable(spec_name, None) + if spec is not None: + logger.info("build_cdap_spec returning cached injectable spec %s", spec_name) + return spec + + # # try configs dir + # spec_path = config.config_file_path(spec_name, mandatory=False) + # if spec_path: + # logger.info("build_cdap_spec reading cached spec %s from %s", spec_name, spec_path) + # return pd.read_csv(spec_path, index_col='Expression') + + # try data dir + if os.path.exists(config.output_file_path(spec_name)): + spec_path = config.output_file_path(spec_name) + logger.info("build_cdap_spec reading cached spec %s from %s", spec_name, spec_path) + return pd.read_csv(spec_path, index_col='Expression') + + return None + + +def cache_spec(hhsize, spec): + spec_name = cached_spec_name(hhsize) + # cache as injectable + inject.add_injectable(spec_name, spec) + # cache as csv in output_dir + spec.to_csv(config.output_file_path(spec_name), index=True) + + def build_cdap_spec(interaction_coefficients, hhsize, - trace_spec=False, trace_label=None): + trace_spec=False, trace_label=None, cache=True): """ Build a spec file for computing utilities of alternative household member interaction patterns for households of specified size. @@ -290,6 +333,9 @@ def build_cdap_spec(interaction_coefficients, hhsize, spec: pandas.DataFrame """ + + t0 = tracing.print_elapsed_time() + # if DUMP: # # dump the interaction_coefficients table because it has been preprocessed # tracing.trace_df(interaction_coefficients, @@ -299,6 +345,11 @@ def build_cdap_spec(interaction_coefficients, hhsize, # cdap spec is same for all households of MAX_HHSIZE and greater hhsize = min(hhsize, MAX_HHSIZE) + if cache: + spec = get_cached_spec(hhsize) + if spec is not None: + return spec + expression_name = "Expression" # generate a list of activity pattern alternatives for this hhsize @@ -410,6 +461,11 @@ def build_cdap_spec(interaction_coefficients, hhsize, tracing.trace_df(spec, '%s.hhsize%d_spec_patched' % (trace_label, hhsize), transpose=False, slicer='NONE') + if cache: + cache_spec(hhsize, spec) + + t0 = tracing.print_elapsed_time("build_cdap_spec hh_size %s" % hhsize, t0) + return spec diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 5dc0995ef..8b84606bf 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -129,7 +129,7 @@ def test_build_cdap_spec_hhsize2(people, cdap_indiv_and_hhsize1, cdap_interactio choosers = cdap.hh_choosers(indiv_utils, hhsize=hhsize) - spec = cdap.build_cdap_spec(cdap_interaction_coefficients, hhsize=hhsize) + spec = cdap.build_cdap_spec(cdap_interaction_coefficients, hhsize=hhsize, cache=False) vars = cdap.eval_variables(spec.index, choosers) diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index 700979fad..b8d13f287 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -23,7 +23,7 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): df_full = read_input_table("households") # only using households listed in override_hh_ids - if override_hh_ids: + if override_hh_ids is not None: # trace_hh_id will not used if it is not in list of override_hh_ids logger.info("override household list containing %s households" % len(override_hh_ids)) @@ -43,8 +43,6 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): # df contains only trace_hh (or empty if not in full store) df = tracing.slice_ids(df_full, trace_hh_id) - df = df[df.index.isin(ids)] - # if we need a subset of full store elif households_sample_size > 0 and df_full.shape[0] > households_sample_size: diff --git a/activitysim/abm/test/configs/override_hh_ids.csv b/activitysim/abm/test/configs/override_hh_ids.csv new file mode 100644 index 000000000..c25b7720a --- /dev/null +++ b/activitysim/abm/test/configs/override_hh_ids.csv @@ -0,0 +1,11 @@ +household_id +961042 +608031 +93713 +93769 +2525286 +945618 +945700 +1321232 +237967 +1320576 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index d02643a13..4cf2b9254 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -20,6 +20,7 @@ from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import inject +from activitysim.core import config # set the max households for all tests (this is to limit memory use on travis) HOUSEHOLDS_SAMPLE_SIZE = 100 @@ -34,6 +35,21 @@ SKIP_FULL_RUN = False +def setup_dirs(configs_dir): + + inject.add_injectable("configs_dir", configs_dir) + + output_dir = os.path.join(os.path.dirname(__file__), 'output') + inject.add_injectable("output_dir", output_dir) + + data_dir = os.path.join(os.path.dirname(__file__), 'data') + inject.add_injectable("data_dir", data_dir) + + inject.clear_cache() + + tracing.config_logger() + + def teardown_function(func): inject.clear_cache() inject.reinject_decorated_tables() @@ -49,20 +65,13 @@ def close_handlers(): logger.setLevel(logging.NOTSET) -def inject_settings(configs_dir, households_sample_size, chunk_size=None, - trace_hh_id=None, trace_od=None, check_for_variability=None): +def inject_settings(configs_dir, **kwargs): with open(os.path.join(configs_dir, 'settings.yaml')) as f: settings = yaml.load(f) - settings['households_sample_size'] = households_sample_size - if chunk_size is not None: - settings['chunk_size'] = chunk_size - if trace_hh_id is not None: - settings['trace_hh_id'] = trace_hh_id - if trace_od is not None: - settings['trace_od'] = trace_od - if check_for_variability is not None: - settings['check_for_variability'] = check_for_variability + + for k in kwargs: + settings[k] = kwargs[k] inject.add_injectable("settings", settings) @@ -72,17 +81,8 @@ def inject_settings(configs_dir, households_sample_size, chunk_size=None, def test_rng_access(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - inject.add_injectable("configs_dir", configs_dir) - output_dir = os.path.join(os.path.dirname(__file__), 'output') - inject.add_injectable("output_dir", output_dir) - - data_dir = os.path.join(os.path.dirname(__file__), 'data') - inject.add_injectable("data_dir", data_dir) - - inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - - inject.clear_cache() + setup_dirs(configs_dir) inject.add_injectable('rng_base_seed', 0) @@ -144,20 +144,11 @@ def regress_mini_mtf(): def test_mini_pipeline_run(): configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - inject.add_injectable("configs_dir", configs_dir) - output_dir = os.path.join(os.path.dirname(__file__), 'output') - inject.add_injectable("output_dir", output_dir) - - data_dir = os.path.join(os.path.dirname(__file__), 'data') - inject.add_injectable("data_dir", data_dir) + setup_dirs(configs_dir) inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - inject.clear_cache() - - tracing.config_logger() - _MODELS = [ 'initialize_landuse', 'compute_accessibility', @@ -192,7 +183,6 @@ def test_mini_pipeline_run(): pipeline.close_pipeline() inject.clear_cache() - close_handlers() @@ -203,18 +193,11 @@ def test_mini_pipeline_run2(): # when we restart pipeline configs_dir = os.path.join(os.path.dirname(__file__), 'configs') - inject.add_injectable("configs_dir", configs_dir) - output_dir = os.path.join(os.path.dirname(__file__), 'output') - inject.add_injectable("output_dir", output_dir) - - data_dir = os.path.join(os.path.dirname(__file__), 'data') - inject.add_injectable("data_dir", data_dir) + setup_dirs(configs_dir) inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) - inject.clear_cache() - # should be able to get this BEFORE pipeline is opened checkpoints_df = pipeline.get_checkpoints() prev_checkpoint_count = len(checkpoints_df.index) @@ -241,8 +224,38 @@ def test_mini_pipeline_run2(): checkpoints_df = pipeline.get_checkpoints() assert len(checkpoints_df.index) == prev_checkpoint_count + # - write list of override_hh_ids to override_hh_ids.csv in configs for use in next test + num_hh_ids = 10 + hh_ids = pipeline.get_table("households").head(num_hh_ids).index.values + hh_ids = pd.DataFrame({'household_id': hh_ids}) + hh_ids.to_csv(os.path.join(configs_dir, 'override_hh_ids.csv'), index=False, header=True) + pipeline.close_pipeline() inject.clear_cache() + close_handlers() + + +def test_mini_pipeline_run3(): + + # test that hh_ids setting overrides household sampling + + configs_dir = os.path.join(os.path.dirname(__file__), 'configs') + setup_dirs(configs_dir) + inject_settings(configs_dir, hh_ids='override_hh_ids.csv') + + households = inject.get_table('households').to_frame() + + override_hh_ids = pd.read_csv(config.config_file_path('override_hh_ids.csv')) + + print("\noverride_hh_ids\n", override_hh_ids) + + print("\nhouseholds\n", households.index) + + assert households.shape[0] == override_hh_ids.shape[0] + assert households.index.isin(override_hh_ids.household_id).all() + + inject.clear_cache() + close_handlers() def full_run(resume_after=None, chunk_size=0, @@ -250,13 +263,8 @@ def full_run(resume_after=None, chunk_size=0, trace_hh_id=None, trace_od=None, check_for_variability=None): configs_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'example', 'configs') - inject.add_injectable("configs_dir", configs_dir) - - data_dir = os.path.join(os.path.dirname(__file__), 'data') - inject.add_injectable("data_dir", data_dir) - output_dir = os.path.join(os.path.dirname(__file__), 'output') - inject.add_injectable("output_dir", output_dir) + setup_dirs(configs_dir) settings = inject_settings( configs_dir, @@ -266,10 +274,6 @@ def full_run(resume_after=None, chunk_size=0, trace_od=trace_od, check_for_variability=check_for_variability) - inject.clear_cache() - - tracing.config_logger() - MODELS = settings['models'] pipeline.run(models=MODELS, resume_after=resume_after) @@ -441,6 +445,21 @@ def test_full_run_stability(): pipeline.close_pipeline() +def test_full_run_singleton(): + + # should wrk with only one hh + + if SKIP_FULL_RUN: + return + + tour_count = full_run(trace_hh_id=HH_ID, + households_sample_size=1) + + regress() + + pipeline.close_pipeline() + + if __name__ == "__main__": print("running test_full_run1") diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index fb8536932..b08c4ae8f 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -241,7 +241,7 @@ def to_series(x): (target, expression, type(target)) if target in local_keys: - logger.warning("assign_variables target obscures local_d name '%s'" % str(target)) + logger.warning("assign_variables target obscures local_d name '%s'", str(target)) if is_temp_scalar(target) or is_throwaway(target): x = eval(expression, globals(), _locals_dict) @@ -267,12 +267,8 @@ def to_series(x): np.seterrcall(saved_handler) except Exception as err: - logger.error("assign_variables error: %s: %s" % (type(err).__name__, str(err))) - - logger.error("assign_variables expression: %s = %s" - % (str(target), str(expression))) - - # expr_values = to_series(None, target=target) + logger.error("assign_variables error: %s: %s", type(err).__name__, str(err)) + logger.error("assign_variables expression: %s = %s", str(target), str(expression)) raise err if not is_temp(target): diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 0fe6e8b4c..df8578b03 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -226,6 +226,12 @@ def build_output_file_path(file_name, use_prefix=None): return file_path +def data_file_path(file_name): + + data_dir = inject.get_injectable('data_dir') + return os.path.join(data_dir, file_name) + + def output_file_path(file_name): prefix = inject.get_injectable('output_file_prefix', None) @@ -235,7 +241,12 @@ def output_file_path(file_name): def trace_file_path(file_name): output_dir = inject.get_injectable('output_dir') - file_name = "trace.%s" % (file_name, ) + + if os.path.exists(os.path.join(output_dir, 'trace')): + output_dir = os.path.join(output_dir, 'trace') + else: + file_name = "trace.%s" % (file_name,) + file_path = os.path.join(output_dir, file_name) return file_path diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 957ffe840..d38fe55dc 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -18,6 +18,8 @@ import time import logging import multiprocessing +import cProfile + from collections import OrderedDict import yaml @@ -289,8 +291,7 @@ def load_skim_data(skim_buffer): logger.info("load_skim_data") - data_dir = inject.get_injectable('data_dir') - omx_file_path = os.path.join(data_dir, setting('skims_file')) + omx_file_path = config.data_file_path(setting('skims_file')) tags_to_load = setting('skim_time_periods')['labels'] skim_info = get_skim_info(omx_file_path, tags_to_load) @@ -298,10 +299,10 @@ def load_skim_data(skim_buffer): def allocate_shared_skim_buffer(): + logger.info("allocate_shared_skim_buffer") - data_dir = inject.get_injectable('data_dir') - omx_file_path = os.path.join(data_dir, setting('skims_file')) + omx_file_path = config.data_file_path(setting('skims_file')) tags_to_load = setting('skim_time_periods')['labels'] # select the skims to load @@ -323,7 +324,7 @@ def setup_injectables_and_logging(injectables): tracing.config_logger() -def run_simulation(queue, injectables, step_info, resume_after, skim_buffer): +def run_simulation(queue, step_info, resume_after, skim_buffer): step_label = step_info['name'] models = step_info['models'] @@ -366,6 +367,11 @@ def run_simulation(queue, injectables, step_info, resume_after, skim_buffer): pipeline.close_pipeline() +def profile_path(): + path = config.output_file_path('%s.prof' % multiprocessing.current_process().name) + return path + + """ multiprocessing entry points """ @@ -377,29 +383,47 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): handle_standard_args() setup_injectables_and_logging(injectables) - process_name = multiprocessing.current_process().name if step_info['num_processes'] > 1: - pipeline_prefix = process_name + pipeline_prefix = multiprocessing.current_process().name logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) - run_simulation(queue, injectables, step_info, resume_after, skim_buffer) + if setting('profile', False): + cProfile.runctx('run_simulation(queue, step_info, resume_after, skim_buffer)', + globals(), locals(), filename=profile_path()) + else: + run_simulation(queue, step_info, resume_after, skim_buffer) def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): setup_injectables_and_logging(injectables) - apportion_pipeline(sub_job_proc_names, slice_info) + + if setting('profile', False): + cProfile.runctx('apportion_pipeline(sub_job_proc_names, slice_info)', + globals(), locals(), filename=profile_path()) + else: + apportion_pipeline(sub_job_proc_names, slice_info) def mp_setup_skims(injectables, **kwargs): skim_buffer = kwargs setup_injectables_and_logging(injectables) - load_skim_data(skim_buffer) + + if setting('profile', False): + cProfile.runctx('load_skim_data(skim_buffer)', + globals(), locals(), filename=profile_path()) + else: + load_skim_data(skim_buffer) def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): setup_injectables_and_logging(injectables) - coalesce_pipelines(sub_job_proc_names, slice_info) + + if setting('profile', False): + cProfile.runctx('coalesce_pipelines(sub_job_proc_names, slice_info)', + globals(), locals(), filename=profile_path()) + else: + coalesce_pipelines(sub_job_proc_names, slice_info) """ @@ -500,15 +524,16 @@ def drop_breadcrumb(phase): new_breadcrumbs.setdefault(step_name, {'name': step_name})[phase] = True write_breadcrumbs(new_breadcrumbs) - logger.info('setup shared skim data') - shared_skim_buffer = allocate_shared_skim_buffer() + with tracing.timing('allocate shared skim buffer', logger): + shared_skim_buffer = allocate_shared_skim_buffer() # - mp_setup_skims - run_sub_task( - multiprocessing.Process( - target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), - kwargs=shared_skim_buffer) - ) + with tracing.timing('setup skims', logger): + run_sub_task( + multiprocessing.Process( + target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), + kwargs=shared_skim_buffer) + ) for step_info in run_list['multiprocess_steps']: @@ -522,12 +547,9 @@ def drop_breadcrumb(phase): else: sub_proc_names = ["%s_%s" % (step_name, i) for i in range(num_processes)] - logger.info('running step %s with %s processes', step_name, num_processes,) - # - mp_apportion_pipeline - if not skip_phase('apportion'): - if num_processes > 1: - logger.info('apportioning households to sub_processes') + if not skip_phase('apportion') and num_processes > 1: + with tracing.timing('apportion %s pipelines' % step_name, logger): run_sub_task( multiprocessing.Process( target=mp_apportion_pipeline, name='%s_apportion' % step_name, @@ -537,17 +559,17 @@ def drop_breadcrumb(phase): # - run_sub_simulations if not skip_phase('simulate'): - error_count = \ - run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, - resume_after=step_info.get('resume_after', None)) + with tracing.timing('run step %s' % step_name, logger): + error_count = \ + run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, + resume_after=step_info.get('resume_after', None)) if error_count: raise RuntimeError("%s processes failed in step %s" % (error_count, step_name)) drop_breadcrumb('simulate') # - mp_coalesce_pipelines - if not skip_phase('coalesce'): - if num_processes > 1: - logger.info('coalescing sub_process pipelines') + if not skip_phase('coalesce') and num_processes > 1: + with tracing.timing('coalesce %s pipelines' % step_name, logger): run_sub_task( multiprocessing.Process( target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 954267eb0..731b41251 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -488,6 +488,8 @@ def open_pipeline(resume_after=None): if _PIPELINE.is_open: raise RuntimeError("Pipeline is already open!") + + _PIPELINE.init_state() _PIPELINE.is_open = True get_rn_generator().set_base_seed(inject.get_injectable('rng_base_seed', 0)) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index c3f0d96ac..2ac163967 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -15,6 +15,7 @@ import logging.config import sys import time +import contextlib from collections import OrderedDict import yaml @@ -63,7 +64,29 @@ def print_elapsed_time(msg=None, t0=None, debug=False): return t1 -def delete_output_files(file_type, ignore=None): +@contextlib.contextmanager +def timing(msg, callers_logger, level=logging.DEBUG): + """ + A context manager to log time to execute a block + + Parameters + ---------- + msg : str + Will be prefixed with "start: " and "finish: ". + callers_logger : logging.Logger + logger passed from caller's context + level : int, optional + Level at which to log, passed to ``logger.log``. + + """ + callers_logger.log(level, msg) + t = time.time() + yield + t = time.time() - t + callers_logger.log(level, "Time to execute %s : %s" % (msg, format_elapsed_time(t))) + + +def delete_output_files(file_type, ignore=None, subdir=None): """ Delete files in output directory of specified type @@ -79,9 +102,14 @@ def delete_output_files(file_type, ignore=None): output_dir = inject.get_injectable('output_dir') + if subdir: + output_dir = os.path.join(output_dir, subdir) + if not os.path.exists(output_dir): + logger.warn("delete_output_files: No subdirectory %s" % (file_type, output_dir)) + return + if ignore: ignore = [os.path.realpath(p) for p in ignore] - print("delete_output_files ignoring", ignore) logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) diff --git a/docs/abmexample.rst b/docs/abmexample.rst index 36300b421..1e77a65f7 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -479,7 +479,7 @@ Logging Included in the ``configs`` folder is the ``logging.yaml``, which configures Python logging library and defines two key log files: -* ``asim.log`` - overall system log file +* ``activitysim.log`` - overall system log file * ``hhtrace.log`` - household trace log file if tracing is on Refer to the :ref:`tracing` section for more detail on tracing. @@ -642,9 +642,9 @@ The example ``simulation.py`` run model script also writes the final tables to C for illustrative purposes by using the :func:`activitysim.core.pipeline.get_table` method. This method returns a pandas DataFrame, which can then be written to a CSV with the ``to_csv(file_path)`` method. -ActivitySim also writes log and trace files to the ``outputs`` folder. The asim.log file, which -is the overall log file is always produced. If tracing is specified, then trace files are output -as well. +ActivitySim also writes log and trace files to the ``outputs`` folder. The activitysim.log file, +which is the overall log file is always produced. If tracing is specified, then trace files are +output as well. .. _tracing : diff --git a/example/configs/logging.yaml b/example/configs/logging.yaml index 7ab585e84..a30a25c82 100644 --- a/example/configs/logging.yaml +++ b/example/configs/logging.yaml @@ -28,7 +28,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] mode: w formatter: fileFormatter level: NOTSET diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index d69998f6e..9f8ba299c 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -33,7 +33,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] mode: w formatter: fileFormatter level: NOTSET diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 7243a07e3..e169a73d2 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,10 +1,12 @@ inherit_settings: True -households_sample_size: 100 -multiprocess: False +households_sample_size: 300 chunk_size: 3000000000 +multiprocess: False +profile: True + check_for_variability: False #trace household id; comment out for no trace @@ -54,7 +56,7 @@ models: - write_data_dictionary - write_tables -#resume_after: school_location_simulate +#resume_after: auto_ownership_simulate # #resume: _workplace_location_sample #restore_checkpoint: school_location_simulate diff --git a/example_mp/output/.gitignore b/example_mp/output/.gitignore index b987779f4..1bd8ff2e6 100644 --- a/example_mp/output/.gitignore +++ b/example_mp/output/.gitignore @@ -4,3 +4,4 @@ *.h5 *.txt *.yaml +trace/ diff --git a/example_mp/simulation.py b/example_mp/simulation.py index e8506e3c4..ea3bae380 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -17,6 +17,7 @@ from activitysim.core import config from activitysim.core import pipeline from activitysim.core import mp_tasks + # from activitysim import abm @@ -30,11 +31,22 @@ def cleanup_output_files(): tracing.delete_output_files('log', ignore=active_log_files) tracing.delete_output_files('h5') - tracing.delete_output_files('csv') + tracing.delete_output_files('csv', subdir='trace') tracing.delete_output_files('txt') tracing.delete_output_files('yaml') +def run(run_list, injectables=None): + + if run_list['multiprocess']: + logger.info("run multiprocess simulation") + mp_tasks.run_multiprocess(run_list, injectables) + else: + logger.info("run single process simulation") + pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) + pipeline.close_pipeline() + + if __name__ == '__main__': # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') @@ -51,19 +63,19 @@ def cleanup_output_files(): cleanup_output_files() run_list = mp_tasks.get_run_list() - # mp_tasks.print_run_list(run_list) if run_list['multiprocess']: - logger.info("run multiprocess simulation") - # do this after config.handle_standard_args, as command line args may override injectables injectables = ['data_dir', 'configs_dir', 'output_dir'] injectables = {k: inject.get_injectable(k) for k in injectables} + else: + injectables = None - mp_tasks.run_multiprocess(run_list, injectables) + if config.setting('profile', False): + import cProfile + cProfile.runctx('run(run_list, injectables)', + globals(), locals(), filename=config.output_file_path('simulation.prof')) else: - logger.info("run single process simulation") - pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) - pipeline.close_pipeline() + run(run_list, injectables) t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_multi/configs/logging.yaml b/example_multi/configs/logging.yaml index 71b34b60e..f2a0e6cb7 100644 --- a/example_multi/configs/logging.yaml +++ b/example_multi/configs/logging.yaml @@ -28,7 +28,7 @@ logging: logfile: class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.config.log_file_path ['asim.log'] + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] mode: w formatter: fileFormatter level: !!python/name:logging.NOTSET diff --git a/example_multi/extensions/models.py b/example_multi/extensions/models.py index 2446e52c4..10b91a2ea 100644 --- a/example_multi/extensions/models.py +++ b/example_multi/extensions/models.py @@ -1,14 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -import os import logging import numpy as np import pandas as pd -from activitysim.core import simulate as asim - from activitysim.core import assign from activitysim.core import inject from activitysim.core import tracing @@ -34,7 +31,7 @@ def best_transit_path(set_random_seed, model_settings = config.read_model_settings('best_transit_path.yaml') - logger.info("best_transit_path VECTOR_TEST_SIZE %s" % VECTOR_TEST_SIZE) + logger.info("best_transit_path VECTOR_TEST_SIZE %s", VECTOR_TEST_SIZE) omaz = network_los.maz_df.sample(VECTOR_TEST_SIZE, replace=True).index dmaz = network_los.maz_df.sample(VECTOR_TEST_SIZE, replace=True).index @@ -60,9 +57,9 @@ def best_transit_path(set_random_seed, how='left' ) - logger.info("len od_df %s" % len(od_df.index)) - logger.info("len atap_btap_df %s" % len(atap_btap_df.index)) - logger.info("avg explosion %s" % (len(atap_btap_df.index) / (1.0 * len(od_df.index)))) + logger.info("len od_df %s", len(od_df.index)) + logger.info("len atap_btap_df %s", len(atap_btap_df.index)) + logger.info("avg explosion %s", (len(atap_btap_df.index) / (1.0 * len(od_df.index)))) if trace_od: trace_orig, trace_dest = trace_od @@ -90,7 +87,7 @@ def best_transit_path(set_random_seed, n = len(atap_btap_df.index) atap_btap_df = atap_btap_df.dropna(subset=['utility']) - logger.info("Dropped %s of %s rows with null utility" % (n - len(atap_btap_df.index), n)) + logger.info("Dropped %s of %s rows with null utility", n - len(atap_btap_df.index), n) # choose max utility atap_btap_df = atap_btap_df.sort_values(by='utility').groupby('idx').tail(1) @@ -98,7 +95,7 @@ def best_transit_path(set_random_seed, if trace_od: if not trace_oabd_rows.any(): - logger.warning("trace_od not found origin = %s, dest = %s" % (trace_orig, trace_dest)) + logger.warning("trace_od not found origin = %s, dest = %s", trace_orig, trace_dest) else: tracing.trace_df(atap_btap_df, From 06ef4f227cb3696069d6debc551211d7f2857a5d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 19 Oct 2018 13:13:43 -0400 Subject: [PATCH 028/122] smrter mp resume with completed list --- activitysim/abm/models/__init__.py | 3 - activitysim/abm/models/annotate_table.py | 43 ------ activitysim/abm/models/util/test/test_cdap.py | 12 -- activitysim/abm/tables/input_store.py | 2 +- activitysim/abm/tables/skims.py | 11 +- activitysim/core/config.py | 4 +- activitysim/core/inject.py | 12 +- activitysim/core/mp_tasks.py | 128 ++++++++++++------ activitysim/core/{orca => }/orca.py | 66 +++++++-- activitysim/core/orca/__init__.py | 7 - activitysim/core/orca/tests/__init__.py | 3 - activitysim/core/orca/utils/__init__.py | 5 - activitysim/core/orca/utils/logutil.py | 127 ----------------- activitysim/core/orca/utils/tests/__init__.py | 3 - .../core/orca/utils/tests/test_testing.py | 87 ------------ .../core/orca/utils/tests/test_utils.py | 9 -- activitysim/core/orca/utils/utils.py | 27 ---- activitysim/core/pipeline.py | 22 +-- .../{orca/tests => test}/test_mergetables.py | 2 +- .../core/{orca/tests => test}/test_orca.py | 7 +- activitysim/core/test/test_pipeline.py | 8 +- activitysim/core/test/test_simulate.py | 10 +- .../testing.py => test/utils_testing.py} | 0 conftest.py | 13 -- example_mp/configs/logging.yaml | 2 +- example_mp/configs/settings.yaml | 2 +- example_mp/output/.gitignore | 2 +- example_mp/output/trace/.gitignore | 1 + 28 files changed, 191 insertions(+), 427 deletions(-) delete mode 100644 activitysim/abm/models/annotate_table.py rename activitysim/core/{orca => }/orca.py (97%) delete mode 100644 activitysim/core/orca/__init__.py delete mode 100644 activitysim/core/orca/tests/__init__.py delete mode 100644 activitysim/core/orca/utils/__init__.py delete mode 100644 activitysim/core/orca/utils/logutil.py delete mode 100644 activitysim/core/orca/utils/tests/__init__.py delete mode 100644 activitysim/core/orca/utils/tests/test_testing.py delete mode 100644 activitysim/core/orca/utils/tests/test_utils.py delete mode 100644 activitysim/core/orca/utils/utils.py rename activitysim/core/{orca/tests => test}/test_mergetables.py (99%) rename activitysim/core/{orca/tests => test}/test_orca.py (99%) rename activitysim/core/{orca/utils/testing.py => test/utils_testing.py} (100%) delete mode 100644 conftest.py create mode 100644 example_mp/output/trace/.gitignore diff --git a/activitysim/abm/models/__init__.py b/activitysim/abm/models/__init__.py index 080fee5e9..e9ebf8dff 100644 --- a/activitysim/abm/models/__init__.py +++ b/activitysim/abm/models/__init__.py @@ -32,6 +32,3 @@ from . import trip_purpose_and_destination from . import trip_scheduling from . import trip_mode_choice - -# parameterized models -from . import annotate_table diff --git a/activitysim/abm/models/annotate_table.py b/activitysim/abm/models/annotate_table.py deleted file mode 100644 index 4077dd25b..000000000 --- a/activitysim/abm/models/annotate_table.py +++ /dev/null @@ -1,43 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -import os -import logging - -import pandas as pd -import numpy as np - -from activitysim.core import tracing -from activitysim.core import inject -from activitysim.core import pipeline -# from activitysim.core import timetable as tt -# from activitysim.core import assign -from activitysim.core import config - -from .util import expressions -from activitysim.core.util import assign_in_place - -logger = logging.getLogger(__name__) - - -@inject.step() -def annotate_table(): - - # model_settings name should have been provided as a step argument - model_name = inject.get_step_arg('model_name') - - trace_label = 'annotate_table.%s' % model_name - - model_settings = config.read_model_settings('%s.yaml' % model_name) - - df_name = model_settings['DF'] - df = inject.get_table(df_name).to_frame() - - results = expressions.compute_columns( - df, - model_settings=model_settings, - trace_label=trace_label) - - assign_in_place(df, results) - - pipeline.replace_table(df_name, df) diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 8b84606bf..c889d9ddc 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -51,18 +51,6 @@ def individual_utils( return cdap.individual_utilities(people, cdap_indiv_and_hhsize1, locals_d=None) -# @pytest.fixture -# def hh_utils(individual_utils, people, hh_id_col): -# hh_utils = cdap.initial_household_utilities( -# individual_utils, people, hh_id_col) -# return hh_utils -# -# -# @pytest.fixture -# def hh_choices(random_seed, hh_utils): -# return cdap.make_household_choices(hh_utils) - - def test_bad_coefficients(configs_dir): f = os.path.join(configs_dir, 'cdap_interaction_coefficients.csv') diff --git a/activitysim/abm/tables/input_store.py b/activitysim/abm/tables/input_store.py index 4a59aa125..8c7399026 100644 --- a/activitysim/abm/tables/input_store.py +++ b/activitysim/abm/tables/input_store.py @@ -11,7 +11,7 @@ from activitysim.core.config import setting # FIXME -warnings.filterwarnings('ignore', category=pd.io.pytables.PerformanceWarning) +# warnings.filterwarnings('ignore', category=pd.io.pytables.PerformanceWarning) pd.options.mode.chained_assignment = None logger = logging.getLogger(__name__) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index d51ca8f3b..dea416be2 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -36,8 +36,8 @@ def get_skim_info(omx_file_path, tags_to_load=None): # this is sys.maxint for p2.7 but no limit for p3 - # MAX_BLOCK_BYTES = 28880000 - MAX_BLOCK_BYTES = sys.maxsize + # windows sys.maxint = 2147483647 + MAX_BLOCK_BYTES = sys.maxint - 1 if sys.version_info < (3,) else sys.maxsize - 1 # Note: we load all skims except those with key2 not in tags_to_load # Note: we require all skims to be of same dtype so they can share buffer - is that ok? @@ -46,7 +46,12 @@ def get_skim_info(omx_file_path, tags_to_load=None): omx_name = os.path.splitext(os.path.basename(omx_file_path))[0] with omx.open_file(omx_file_path) as omx_file: - omx_shape = tuple(map(int, omx_file.shape())) # sometimes omx shape are floats! + # omx_shape = tuple(map(int, tuple(omx_file.shape()))) # sometimes omx shape are floats! + + # fixme call to omx_file.shape() failing in windows p3.5 + omx_shape = omx_file.shape() + omx_shape = (int(omx_shape[0]), int(omx_shape[1])) # sometimes omx shape are floats! + omx_skim_names = omx_file.listMatrices() # - omx_keys dict maps skim key to omx_key diff --git a/activitysim/core/config.py b/activitysim/core/config.py index df8578b03..744dda40e 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -54,9 +54,7 @@ def settings(): @inject.injectable(cache=True) def pipeline_file_name(settings): - """ - Orca injectable to return the path to the pipeline hdf5 file based on output_dir and settings - """ + pipeline_file_name = settings.get('pipeline', 'pipeline.h5') return pipeline_file_name diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index b406e91b4..263d485c3 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -123,7 +123,7 @@ def get_injectable(name, default=_NO_DEFAULT): def remove_injectable(name): - orca.orca._INJECTABLES.pop(name, None) + orca._INJECTABLES.pop(name, None) def reinject_decorated_tables(): @@ -134,10 +134,10 @@ def reinject_decorated_tables(): logger.info("reinject_decorated_tables") # need to clear any non-decorated tables that were added during the previous run - orca.orca._TABLES.clear() - orca.orca._COLUMNS.clear() - orca.orca._TABLE_CACHE.clear() - orca.orca._COLUMN_CACHE.clear() + orca._TABLES.clear() + orca._COLUMNS.clear() + orca._TABLE_CACHE.clear() + orca._COLUMN_CACHE.clear() for name, func in iteritems(_DECORATED_TABLES): logger.debug("reinject decorated table %s" % name) @@ -177,4 +177,4 @@ def get_step_arg(arg_name, default=_NO_DEFAULT): def dump_state(): print("_DECORATED_STEPS", list(_DECORATED_STEPS.keys())) - print("orca._STEPS", list(orca.orca._STEPS.keys())) + print("orca._STEPS", list(orca._STEPS.keys())) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index d38fe55dc..8675b86d2 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -431,12 +431,52 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): """ -def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_names, resume_after): +def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_names, + resume_after, previously_completed): + + def log_queued_messages(): + for i, process, queue in zip(list(range(num_simulations)), procs, queues): + while not queue.empty(): + msg = queue.get(block=False) + logger.info("%s %s : %s", process.name, msg['model'], + tracing.format_elapsed_time(msg['time'])) + + def check_proc_status(): + # we want to drop 'completed' breadcrumb when it happens, lest we terminate + for p in procs: + if p.exitcode is None: + pass # still running + elif p.exitcode == 0: + # completed successfully + if p.name not in completed: + logger.info("process %s completed", p.name) + completed.add(p.name) + drob_breadcrumb(step_name, 'completed', list(completed)) + else: + # process failed + if p.name not in failed: + logger.info("process %s failed with exitcode %s", p.name, p.exitcode) + failed.add(p.name) + + def idle(seconds): + log_queued_messages() + check_proc_status() + for _ in range(seconds): + time.sleep(1) + log_queued_messages() + check_proc_status() step_name = step_info['name'] logger.info('run_sub_simulations step %s models resume_after %s', step_name, resume_after) + if previously_completed: + assert resume_after == '_' + assert set(previously_completed).issubset(set(process_names)) + process_names = [name for name in process_names if name not in previously_completed] + logger.info('run_sub_simulations step %s: skipping %s previously completed subprocedures', + step_name, len(previously_completed)) + # if not the first step, resume_after the last checkpoint from the previous step if resume_after is None and step_info['step_num'] > 0: resume_after = '_' @@ -444,6 +484,11 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name num_simulations = len(process_names) procs = [] queues = [] + stagger_starts = step_info['stagger'] + + completed = set(previously_completed) + failed = set([]) # so we can log process failure when it happens + drob_breadcrumb(step_name, 'completed', list(completed)) for process_name in process_names: q = multiprocessing.Queue() @@ -453,45 +498,33 @@ def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_name procs.append(p) queues.append(q) - def log_queued_messages(): - for i, process, queue in zip(list(range(num_simulations)), procs, queues): - while not queue.empty(): - msg = queue.get(block=False) - logger.info("%s %s : %s", - process.name, - msg['model'], - tracing.format_elapsed_time(msg['time'])) - - def idle(seconds): - log_queued_messages() - for _ in range(seconds): - time.sleep(1) - log_queued_messages() - - stagger = 0 - for p in procs: - if stagger > 0: - logger.info("stagger process %s by %s seconds", p.name, stagger) - idle(stagger) - stagger = step_info['stagger'] + # - start processes + for i, p in zip(list(range(num_simulations)), procs): + if stagger_starts > 0 and i > 0: + logger.info("stagger process %s by %s seconds", p.name, stagger_starts) + idle(seconds=stagger_starts) logger.info("start process %s", p.name) p.start() + # - idle logging queued messages and proc completion while multiprocessing.active_children(): - idle(1) - log_queued_messages() + idle(seconds=1) + idle(seconds=0) - for p in procs: - p.join() + # no need to join explicitly since multiprocessing.active_children joins completed procs + # for p in procs: + # p.join() - # log exitcode of sub_simulations that failed - error_count = 0 for p in procs: + assert p.exitcode is not None if p.exitcode: - logger.error("Process %s returned exitcode %s", p.name, p.exitcode) - error_count += 1 + logger.error("Process %s failed with exitcode %s", p.name, p.exitcode) + assert p.name in failed + else: + logger.error("Process %s completed with exitcode %s", p.name, p.exitcode) + assert p.name in completed - return error_count + return list(completed) def run_sub_task(p): @@ -505,6 +538,13 @@ def run_sub_task(p): raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) +def drob_breadcrumb(step_name, crumb, value=True): + breadcrumbs = inject.get_injectable('breadcrumbs', OrderedDict()) + breadcrumbs.setdefault(step_name, {'name': step_name})[crumb] = value + inject.add_injectable('breadcrumbs', breadcrumbs) + write_breadcrumbs(breadcrumbs) + + def run_multiprocess(run_list, injectables): if not run_list['multiprocess']: @@ -512,7 +552,6 @@ def run_multiprocess(run_list, injectables): run_list['multiprocess']) old_breadcrumbs = run_list.get('breadcrumbs', {}) - new_breadcrumbs = OrderedDict() def skip_phase(phase): skip = old_breadcrumbs and old_breadcrumbs.get(step_name, {}).get(phase, False) @@ -520,9 +559,8 @@ def skip_phase(phase): logger.info("Skipping %s %s", step_name, phase) return skip - def drop_breadcrumb(phase): - new_breadcrumbs.setdefault(step_name, {'name': step_name})[phase] = True - write_breadcrumbs(new_breadcrumbs) + def find_breadcrumb(crumb, default=None): + return old_breadcrumbs.get(step_name, {}).get(crumb, default) with tracing.timing('allocate shared skim buffer', logger): shared_skim_buffer = allocate_shared_skim_buffer() @@ -555,17 +593,23 @@ def drop_breadcrumb(phase): target=mp_apportion_pipeline, name='%s_apportion' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - drop_breadcrumb('apportion') + drob_breadcrumb(step_name, 'apportion') # - run_sub_simulations if not skip_phase('simulate'): + resume_after = step_info.get('resume_after', None) + + completed = find_breadcrumb('completed', default=[]) if resume_after == '_' else [] + with tracing.timing('run step %s' % step_name, logger): - error_count = \ + completed = \ run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, - resume_after=step_info.get('resume_after', None)) - if error_count: - raise RuntimeError("%s processes failed in step %s" % (error_count, step_name)) - drop_breadcrumb('simulate') + resume_after, completed) + + if len(completed) != num_processes: + raise RuntimeError("%s processes failed in step %s" % + (num_processes - len(completed), step_name)) + drob_breadcrumb(step_name, 'simulate') # - mp_coalesce_pipelines if not skip_phase('coalesce') and num_processes > 1: @@ -575,7 +619,7 @@ def drop_breadcrumb(phase): target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - drop_breadcrumb('coalesce') + drob_breadcrumb(step_name, 'coalesce') def get_breadcrumbs(run_list): diff --git a/activitysim/core/orca/orca.py b/activitysim/core/orca.py similarity index 97% rename from activitysim/core/orca/orca.py rename to activitysim/core/orca.py index b8dab93e4..12efe40bd 100644 --- a/activitysim/core/orca/orca.py +++ b/activitysim/core/orca.py @@ -14,17 +14,17 @@ from collections import Callable, namedtuple from contextlib import contextmanager from functools import wraps +import inspect import pandas as pd import tables import tlz as tz -from . import utils -from .utils.logutil import log_start_finish from collections import namedtuple -warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) -logger = logging.getLogger(__name__) +# warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) +# logger = logging.getLogger(__name__) +logger = logging.getLogger('orca') _TABLES = {} _COLUMNS = {} @@ -45,6 +45,52 @@ CacheItem = namedtuple('CacheItem', ['name', 'value', 'scope']) +@contextmanager +def log_start_finish(msg, logger, level=logging.DEBUG): + """ + A context manager to log messages with "start: " and "finish: " + prefixes before and after a block. + + Parameters + ---------- + msg : str + Will be prefixed with "start: " and "finish: ". + logger : logging.Logger + level : int, optional + Level at which to log, passed to ``logger.log``. + + """ + # logger.log(level, 'start: ' + msg) + yield + # logger.log(level, 'finish: ' + msg) + + +def _func_source_data(func): + """ + Return data about a function source, including file name, + line number, and source code. + + Parameters + ---------- + func : object + May be anything support by the inspect module, such as a function, + method, or class. + + Returns + ------- + filename : str + lineno : int + The line number on which the function starts. + source : str + + """ + filename = inspect.getsourcefile(func) + lineno = inspect.getsourcelines(func)[1] + source = inspect.getsource(func) + + return filename, lineno, source + + def clear_all(): """ Clear any and all stored state from Orca. @@ -574,7 +620,7 @@ def func_source_data(self): source : str """ - return utils.func_source_data(self._func) + return _func_source_data(self._func) class _ColumnFuncWrapper(object): @@ -668,7 +714,7 @@ def func_source_data(self): source : str """ - return utils.func_source_data(self._func) + return _func_source_data(self._func) class _SeriesWrapper(object): @@ -832,7 +878,7 @@ def func_source_data(self): source : str """ - return utils.func_source_data(self._func) + return _func_source_data(self._func) def is_table(name): @@ -1472,11 +1518,11 @@ def get_injectable_func_source_data(name): inj = get_raw_injectable(name) if isinstance(inj, _InjectableFuncWrapper): - return utils.func_source_data(inj._func) + return _func_source_data(inj._func) elif hasattr(inj, '__wrapped__'): - return utils.func_source_data(inj.__wrapped__) + return _func_source_data(inj.__wrapped__) else: - return utils.func_source_data(inj) + return _func_source_data(inj) def add_step(step_name, func): diff --git a/activitysim/core/orca/__init__.py b/activitysim/core/orca/__init__.py deleted file mode 100644 index a4e55efe5..000000000 --- a/activitysim/core/orca/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. - -from .orca import * - -version = __version__ = '1.5.1' diff --git a/activitysim/core/orca/tests/__init__.py b/activitysim/core/orca/tests/__init__.py deleted file mode 100644 index 0d4e355c7..000000000 --- a/activitysim/core/orca/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. diff --git a/activitysim/core/orca/utils/__init__.py b/activitysim/core/orca/utils/__init__.py deleted file mode 100644 index 947c0411b..000000000 --- a/activitysim/core/orca/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. - -from .utils import * diff --git a/activitysim/core/orca/utils/logutil.py b/activitysim/core/orca/utils/logutil.py deleted file mode 100644 index 258a24f0a..000000000 --- a/activitysim/core/orca/utils/logutil.py +++ /dev/null @@ -1,127 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. - -import contextlib -import logging - -US_LOG_FMT = ('%(asctime)s|%(levelname)s|%(name)s|' - '%(funcName)s|%(filename)s|%(lineno)s|%(message)s') -US_LOG_DATE_FMT = '%Y-%m-%d %H:%M:%S' -US_FMT = logging.Formatter(fmt=US_LOG_FMT, datefmt=US_LOG_DATE_FMT) - - -@contextlib.contextmanager -def log_start_finish(msg, logger, level=logging.DEBUG): - """ - A context manager to log messages with "start: " and "finish: " - prefixes before and after a block. - - Parameters - ---------- - msg : str - Will be prefixed with "start: " and "finish: ". - logger : logging.Logger - level : int, optional - Level at which to log, passed to ``logger.log``. - - """ - # logger.log(level, 'start: ' + msg) - yield - # logger.log(level, 'finish: ' + msg) - - -def set_log_level(level): - """ - Set the logging level for Orca. - - Parameters - ---------- - level : int - A supporting logging level. Use logging constants like logging.DEBUG. - - """ - logging.getLogger('orca').setLevel(level) - - -def _add_log_handler( - handler, level=None, fmt=None, datefmt=None, propagate=None): - """ - Add a logging handler to Orca. - - Parameters - ---------- - handler : logging.Handler subclass - level : int, optional - An optional logging level that will apply only to this stream - handler. - fmt : str, optional - An optional format string that will be used for the log - messages. - datefmt : str, optional - An optional format string for formatting dates in the log - messages. - propagate : bool, optional - Whether the Orca logger should propagate. If None the - propagation will not be modified, otherwise it will be set - to this value. - - """ - if not fmt: - fmt = US_LOG_FMT - if not datefmt: - datefmt = US_LOG_DATE_FMT - - handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt)) - - if level is not None: - handler.setLevel(level) - - logger = logging.getLogger('orca') - logger.addHandler(handler) - - if propagate is not None: - logger.propagate = propagate - - -def log_to_stream(level=None, fmt=None, datefmt=None): - """ - Send log messages to the console. - - Parameters - ---------- - level : int, optional - An optional logging level that will apply only to this stream - handler. - fmt : str, optional - An optional format string that will be used for the log - messages. - datefmt : str, optional - An optional format string for formatting dates in the log - messages. - - """ - _add_log_handler( - logging.StreamHandler(), fmt=fmt, datefmt=datefmt, propagate=False) - - -def log_to_file(filename, level=None, fmt=None, datefmt=None): - """ - Send log output to the given file. - - Parameters - ---------- - filename : str - level : int, optional - An optional logging level that will apply only to this stream - handler. - fmt : str, optional - An optional format string that will be used for the log - messages. - datefmt : str, optional - An optional format string for formatting dates in the log - messages. - - """ - _add_log_handler( - logging.FileHandler(filename), fmt=fmt, datefmt=datefmt) diff --git a/activitysim/core/orca/utils/tests/__init__.py b/activitysim/core/orca/utils/tests/__init__.py deleted file mode 100644 index 0d4e355c7..000000000 --- a/activitysim/core/orca/utils/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. diff --git a/activitysim/core/orca/utils/tests/test_testing.py b/activitysim/core/orca/utils/tests/test_testing.py deleted file mode 100644 index bd382f822..000000000 --- a/activitysim/core/orca/utils/tests/test_testing.py +++ /dev/null @@ -1,87 +0,0 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. - -import pandas as pd -import pytest - -from .. import testing - - -def test_frames_equal_not_frames(): - frame = pd.DataFrame({'a': [1]}) - with pytest.raises(AssertionError) as info: - testing.assert_frames_equal(frame, 1) - - assert 'Inputs must both be pandas DataFrames.' in str(info.value) - - -def test_frames_equal_mismatched_columns(): - expected = pd.DataFrame({'a': [1]}) - actual = pd.DataFrame({'b': [2]}) - - with pytest.raises(AssertionError) as info: - testing.assert_frames_equal(actual, expected) - - assert "Expected column 'a' not found." in str(info.value) - - -def test_frames_equal_mismatched_rows(): - expected = pd.DataFrame({'a': [1]}, index=[0]) - actual = pd.DataFrame({'a': [1]}, index=[1]) - - with pytest.raises(AssertionError) as info: - testing.assert_frames_equal(actual, expected) - - assert "Expected row 0 not found." in str(info.value) - - -def test_frames_equal_mismatched_items(): - expected = pd.DataFrame({'a': [1]}) - actual = pd.DataFrame({'a': [2]}) - - with pytest.raises(AssertionError) as info: - testing.assert_frames_equal(actual, expected) - - assert (""" -Items are not equal: - ACTUAL: 2 - DESIRED: 1 - -Column: 'a' -Row: 0""" in str(info.value)) - - -def test_frames_equal(): - frame = pd.DataFrame({'a': [1]}) - testing.assert_frames_equal(frame, frame) - - -def test_frames_equal_close(): - frame1 = pd.DataFrame({'a': [1]}) - frame2 = pd.DataFrame({'a': [1.00000000000002]}) - - with pytest.raises(AssertionError): - testing.assert_frames_equal(frame1, frame2) - - testing.assert_frames_equal(frame1, frame2, use_close=True) - - -def test_index_equal_order_agnostic(): - left = pd.Index([1, 2, 3]) - right = pd.Index([3, 2, 1]) - testing.assert_index_equal(left, right) - - -def test_index_equal_order_agnostic_raises_left(): - left = pd.Index([1, 2, 3, 4]) - right = pd.Index([3, 2, 1]) - with pytest.raises(AssertionError): - testing.assert_index_equal(left, right) - - -def test_index_equal_order_agnostic_raises_right(): - left = pd.Index([1, 2, 3]) - right = pd.Index([3, 2, 1, 4]) - with pytest.raises(AssertionError): - testing.assert_index_equal(left, right) diff --git a/activitysim/core/orca/utils/tests/test_utils.py b/activitysim/core/orca/utils/tests/test_utils.py deleted file mode 100644 index 03f5260f7..000000000 --- a/activitysim/core/orca/utils/tests/test_utils.py +++ /dev/null @@ -1,9 +0,0 @@ -from .. import utils - - -def test_func_source_data(): - filename, line, source = utils.func_source_data(test_func_source_data) - - assert filename.endswith('test_utils.py') - assert isinstance(line, int) - assert 'assert isinstance(line, int)' in source diff --git a/activitysim/core/orca/utils/utils.py b/activitysim/core/orca/utils/utils.py deleted file mode 100644 index e00994c5a..000000000 --- a/activitysim/core/orca/utils/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -import inspect - - -def func_source_data(func): - """ - Return data about a function source, including file name, - line number, and source code. - - Parameters - ---------- - func : object - May be anything support by the inspect module, such as a function, - method, or class. - - Returns - ------- - filename : str - lineno : int - The line number on which the function starts. - source : str - - """ - filename = inspect.getsourcefile(func) - lineno = inspect.getsourcelines(func)[1] - source = inspect.getsource(func) - - return filename, lineno, source diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 731b41251..e373b2e9b 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -14,22 +14,24 @@ from builtins import next from builtins import map from builtins import object + import os +import logging import datetime as dt import pandas as pd -from . import orca -import logging +from . import orca from . import inject from . import config -from .util import memory_info -from .util import df_size - from . import random from . import tracing + +from .util import memory_info +from .util import df_size from .tracing import print_elapsed_time + logger = logging.getLogger(__name__) # name of the checkpoint dict keys @@ -108,7 +110,7 @@ def open_pipeline_store(overwrite=False): if _PIPELINE.pipeline_store is not None: raise RuntimeError("Pipeline store is already open!") - pipeline_file_path = config.pipeline_file_path(orca.get_injectable('pipeline_file_name')) + pipeline_file_path = config.pipeline_file_path(inject.get_injectable('pipeline_file_name')) if overwrite: try: @@ -241,10 +243,10 @@ def rewrap(table_name, df=None): for column_name in orca.list_columns_for_table(table_name): # logger.debug("pop %s.%s: %s" % (table_name, column_name, t.column_type(column_name))) # fixme - orca.orca._COLUMNS.pop((table_name, column_name), None) + orca._COLUMNS.pop((table_name, column_name), None) # remove from orca's table list - orca.orca._TABLES.pop(table_name, None) + orca._TABLES.pop(table_name, None) assert df is not None @@ -757,10 +759,10 @@ def drop_table(table_name): for column_name in orca.list_columns_for_table(table_name): # logger.debug("pop %s.%s: %s" % (table_name, column_name, t.column_type(column_name))) - orca.orca._COLUMNS.pop((table_name, column_name), None) + orca._COLUMNS.pop((table_name, column_name), None) # remove from orca's table list - orca.orca._TABLES.pop(table_name, None) + orca._TABLES.pop(table_name, None) if table_name in _PIPELINE.replaced_tables: diff --git a/activitysim/core/orca/tests/test_mergetables.py b/activitysim/core/test/test_mergetables.py similarity index 99% rename from activitysim/core/orca/tests/test_mergetables.py rename to activitysim/core/test/test_mergetables.py index 8c924bc6e..0cad197c4 100644 --- a/activitysim/core/orca/tests/test_mergetables.py +++ b/activitysim/core/test/test_mergetables.py @@ -6,7 +6,7 @@ import pytest from .. import orca -from ..utils.testing import assert_frames_equal +from .utils_testing import assert_frames_equal def setup_function(func): diff --git a/activitysim/core/orca/tests/test_orca.py b/activitysim/core/test/test_orca.py similarity index 99% rename from activitysim/core/orca/tests/test_orca.py rename to activitysim/core/test/test_orca.py index 7a3f07ab2..bd25caf51 100644 --- a/activitysim/core/orca/tests/test_orca.py +++ b/activitysim/core/test/test_orca.py @@ -9,8 +9,9 @@ import pytest from pandas.util import testing as pdt -from .. import orca -from ..utils.testing import assert_frames_equal +from activitysim.core import orca +from activitysim.core import inject +from .utils_testing import assert_frames_equal def setup_function(func): @@ -21,6 +22,8 @@ def setup_function(func): def teardown_function(func): orca.clear_all() orca.enable_cache() + # be nice to the others tests that expect decorated injectables to be there + inject.reinject_decorated_tables() @pytest.fixture diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index d5a79966e..37b25b6d9 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -23,7 +23,9 @@ HH_ID = 961042 -def setup(): +def setup_function(): + + inject.reinject_decorated_tables() inject.remove_injectable('skim_dict') inject.remove_injectable('skim_stack') @@ -59,8 +61,6 @@ def close_handlers(): def test_pipeline_run(): - setup() - inject.add_step('step1', steps.step1) inject.add_step('step2', steps.step2) inject.add_step('step3', steps.step3) @@ -106,8 +106,6 @@ def test_pipeline_run(): def test_pipeline_checkpoint_drop(): - setup() - inject.add_step('step1', steps.step1) inject.add_step('step2', steps.step2) inject.add_step('step3', steps.step3) diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 4f9141d73..a51a9d23f 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -1,5 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import print_function import os.path @@ -63,8 +64,13 @@ def test_eval_variables(spec, data): [0, 1, 5, 1]], index=data.index, columns=spec.index) - for i in [0, 1]: - expected[expected.columns[i]] = expected[expected.columns[i]].astype(np.int8) + expected[expected.columns[0]] = expected[expected.columns[0]].astype(np.int8) + expected[expected.columns[1]] = expected[expected.columns[1]].astype(np.int8) + expected[expected.columns[2]] = expected[expected.columns[2]].astype(np.int64) + expected[expected.columns[3]] = expected[expected.columns[3]].astype(int) + + print("\nexpected\n", expected.dtypes) + print("\nresult\n", result.dtypes) pdt.assert_frame_equal(result, expected, check_names=False) diff --git a/activitysim/core/orca/utils/testing.py b/activitysim/core/test/utils_testing.py similarity index 100% rename from activitysim/core/orca/utils/testing.py rename to activitysim/core/test/utils_testing.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index a17f029f3..000000000 --- a/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -import numpy as np -import pytest - - -@pytest.fixture -def random_seed(request): - current = np.random.get_state() - - def fin(): - np.random.set_state(current) - request.addfinalizer(fin) - - np.random.seed(0) diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index 9f8ba299c..f326f3095 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -43,7 +43,7 @@ logging: stream: ext://sys.stdout formatter: simpleFormatter #level: WARNING - level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [DEBUG, NOTSET] + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] mp_console: class: logging.StreamHandler diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index e169a73d2..9944e8844 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -73,7 +73,7 @@ multiprocess_steps: - name: mp_households begin: _school_location_sample num_processes: 3 - stagger: 10 + stagger: 30 #chunk_size: 1000000000 slice: tables: diff --git a/example_mp/output/.gitignore b/example_mp/output/.gitignore index 1bd8ff2e6..c925c5c3d 100644 --- a/example_mp/output/.gitignore +++ b/example_mp/output/.gitignore @@ -4,4 +4,4 @@ *.h5 *.txt *.yaml -trace/ + diff --git a/example_mp/output/trace/.gitignore b/example_mp/output/trace/.gitignore new file mode 100644 index 000000000..afed0735d --- /dev/null +++ b/example_mp/output/trace/.gitignore @@ -0,0 +1 @@ +*.csv From eebd319f9303cae7202518efb7bf9df9bb00583c Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 22 Oct 2018 11:27:28 -0400 Subject: [PATCH 029/122] minor testing tweaks --- activitysim/abm/test/test_pipeline.py | 2 +- activitysim/core/mp_tasks.py | 3 +-- activitysim/core/orca.py | 2 +- activitysim/core/pipeline.py | 2 -- activitysim/core/test/test_orca.py | 4 ++++ activitysim/core/test/test_pipeline.py | 4 +++- example_mp/configs/settings.yaml | 30 +++++++++++--------------- example_mp/simulation.py | 1 + 8 files changed, 23 insertions(+), 25 deletions(-) diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 4cf2b9254..a5d700c05 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -2,9 +2,9 @@ # See full license in LICENSE.txt. from __future__ import (absolute_import, division, print_function, unicode_literals) - from builtins import * + from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 8675b86d2..14ddb01b1 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -70,8 +70,7 @@ def pipeline_table_keys(pipeline_store, checkpoint_name=None): checkpoint_tables = checkpoint_tables[checkpoint_tables != ''] # hdf5 key is / - # FIXME - pathologically knows the format used by pipeline.pipeline_table_key() - checkpoint_tables = {table_name: table_name + '/' + checkpoint_name + checkpoint_tables = {table_name: pipeline.pipeline_table_key(table_name, checkpoint_name) for table_name, checkpoint_name in iteritems(checkpoint_tables)} # checkpoint name and series mapping table name to hdf5 key for tables in that checkpoint diff --git a/activitysim/core/orca.py b/activitysim/core/orca.py index 12efe40bd..19a2a6497 100644 --- a/activitysim/core/orca.py +++ b/activitysim/core/orca.py @@ -22,7 +22,7 @@ from collections import namedtuple -# warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) +warnings.filterwarnings('ignore', category=tables.NaturalNameWarning) # logger = logging.getLogger(__name__) logger = logging.getLogger('orca') diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index e373b2e9b..6ae587f1f 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,9 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. - from __future__ import (absolute_import, division, print_function, unicode_literals) - from builtins import * from future.standard_library import install_aliases diff --git a/activitysim/core/test/test_orca.py b/activitysim/core/test/test_orca.py index bd25caf51..f040a8c28 100644 --- a/activitysim/core/test/test_orca.py +++ b/activitysim/core/test/test_orca.py @@ -5,6 +5,7 @@ import os import tempfile +import tables import pandas as pd import pytest from pandas.util import testing as pdt @@ -909,6 +910,7 @@ def fin(): return fname +@pytest.mark.filterwarnings('ignore::tables.NaturalNameWarning') def test_write_tables(df, store_name): orca.add_table('table', df) @@ -939,6 +941,7 @@ def test_write_all_tables(df, store_name): assert t in store +@pytest.mark.filterwarnings('ignore::tables.NaturalNameWarning') def test_run_and_write_tables(df, store_name): orca.add_table('table', df) @@ -971,6 +974,7 @@ def step(iter_var, table): store['10/table'][year_key(x)], series_year(x)) +@pytest.mark.filterwarnings('ignore::tables.NaturalNameWarning') def test_run_and_write_tables_out_tables_provided(df, store_name): table_names = ['table', 'table2', 'table3'] for t in table_names: diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index 37b25b6d9..354516a07 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -2,7 +2,6 @@ # See full license in LICENSE.txt. from __future__ import (absolute_import, division, print_function, unicode_literals) - from builtins import * from future.standard_library import install_aliases @@ -12,6 +11,8 @@ import logging import pytest +import tables + from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import inject @@ -59,6 +60,7 @@ def close_handlers(): logger.setLevel(logging.NOTSET) +# @pytest.mark.filterwarnings('ignore::tables.NaturalNameWarning') def test_pipeline_run(): inject.add_step('step1', steps.step1) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 9944e8844..0f1e54541 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -2,10 +2,14 @@ inherit_settings: True households_sample_size: 300 + +# 160 GB +#chunk_size: 10000000000 + chunk_size: 3000000000 multiprocess: False -profile: True +profile: False check_for_variability: False @@ -56,16 +60,6 @@ models: - write_data_dictionary - write_tables -#resume_after: auto_ownership_simulate -# -#resume: _workplace_location_sample -#restore_checkpoint: school_location_simulate - - - -# 160 GB -#chunk_size: 10000000000 - multiprocess_steps: - name: mp_initialize @@ -73,7 +67,7 @@ multiprocess_steps: - name: mp_households begin: _school_location_sample num_processes: 3 - stagger: 30 + stagger: 5 #chunk_size: 1000000000 slice: tables: @@ -112,9 +106,9 @@ output_tables: prefix: final_ tables: - checkpoints - - accessibility - - land_use - - households - - persons - - trips - - tours +# - accessibility +# - land_use +# - households +# - persons +# - trips +# - tours diff --git a/example_mp/simulation.py b/example_mp/simulation.py index ea3bae380..b7b68dbbc 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -31,6 +31,7 @@ def cleanup_output_files(): tracing.delete_output_files('log', ignore=active_log_files) tracing.delete_output_files('h5') + tracing.delete_output_files('csv') tracing.delete_output_files('csv', subdir='trace') tracing.delete_output_files('txt') tracing.delete_output_files('yaml') From c70dc914ef1cb8072246f99eb2a86a7cad0d8f5d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 22 Oct 2018 14:03:24 -0400 Subject: [PATCH 030/122] no unicode for python 2.7 --- activitysim/abm/misc.py | 8 +++----- activitysim/abm/models/accessibility.py | 5 +++-- .../abm/models/atwork_subtour_destination.py | 12 ++++-------- activitysim/abm/models/atwork_subtour_frequency.py | 9 ++++----- .../abm/models/atwork_subtour_mode_choice.py | 7 ++++--- .../abm/models/atwork_subtour_scheduling.py | 6 ++++-- activitysim/abm/models/auto_ownership.py | 6 ++++-- activitysim/abm/models/cdap.py | 5 ++++- activitysim/abm/models/initialize.py | 7 ++++--- activitysim/abm/models/joint_tour_composition.py | 7 ++++--- activitysim/abm/models/joint_tour_destination.py | 4 ++++ activitysim/abm/models/joint_tour_frequency.py | 6 ++++-- activitysim/abm/models/joint_tour_participation.py | 4 +++- activitysim/abm/models/joint_tour_scheduling.py | 6 +++--- activitysim/abm/models/mandatory_scheduling.py | 6 +++--- activitysim/abm/models/mandatory_tour_frequency.py | 6 ++++-- .../abm/models/non_mandatory_destination.py | 4 ++++ activitysim/abm/models/non_mandatory_scheduling.py | 4 ++++ .../abm/models/non_mandatory_tour_frequency.py | 5 ++++- activitysim/abm/models/school_location.py | 4 ++++ activitysim/abm/models/stop_frequency.py | 5 ++++- activitysim/abm/models/tour_mode_choice.py | 7 ++++--- activitysim/abm/models/trip_destination.py | 6 ++++-- activitysim/abm/models/trip_mode_choice.py | 7 ++++--- activitysim/abm/models/trip_purpose.py | 7 ++++--- .../abm/models/trip_purpose_and_destination.py | 8 ++++---- activitysim/abm/models/trip_scheduling.py | 6 +++--- activitysim/abm/models/util/overlap.py | 2 -- activitysim/abm/models/util/test/test_cdap.py | 2 -- activitysim/abm/models/util/tour_frequency.py | 1 - activitysim/abm/models/workplace_location.py | 11 ++++------- activitysim/abm/tables/constants.py | 5 +++++ activitysim/abm/tables/households.py | 6 ++++-- activitysim/abm/tables/input_store.py | 5 ++++- activitysim/abm/tables/landuse.py | 4 +++- activitysim/abm/tables/persons.py | 4 +++- activitysim/abm/tables/size_terms.py | 7 ++++--- activitysim/abm/tables/skims.py | 9 +++------ activitysim/abm/tables/table_dict.py | 4 ++++ activitysim/abm/tables/time_windows.py | 6 ++++-- activitysim/abm/tables/tours.py | 8 ++++---- activitysim/abm/tables/trips.py | 6 ++++-- activitysim/abm/test/run_mp.py | 6 +----- activitysim/abm/test/test_misc.py | 10 ++++------ activitysim/abm/test/test_mp_pipeline.py | 5 +---- activitysim/abm/test/test_pipeline.py | 5 +---- activitysim/core/assign.py | 7 ++++--- activitysim/core/chunk.py | 11 ++++------- activitysim/core/config.py | 6 +++--- activitysim/core/inject.py | 4 +++- activitysim/core/interaction_sample.py | 6 ++++-- activitysim/core/interaction_sample_simulate.py | 5 ++++- activitysim/core/interaction_simulate.py | 5 ++++- activitysim/core/logit.py | 7 ++++--- activitysim/core/mp_tasks.py | 8 +------- activitysim/core/orca.py | 10 +++++----- activitysim/core/pipeline.py | 9 +++------ activitysim/core/random.py | 5 +++-- activitysim/core/simulate.py | 8 +++++--- activitysim/core/skim.py | 8 ++++++-- activitysim/core/steps/output.py | 3 +-- activitysim/core/test/extensions/steps.py | 3 +-- activitysim/core/test/test_assign.py | 4 +--- activitysim/core/test/test_inject_defaults.py | 4 +--- activitysim/core/test/test_logit.py | 1 - activitysim/core/test/test_pipeline.py | 3 +-- activitysim/core/test/test_random.py | 1 - activitysim/core/test/test_tracing.py | 7 +------ activitysim/core/test/test_util.py | 1 - activitysim/core/timetable.py | 5 ++++- activitysim/core/tracing.py | 9 +++------ activitysim/core/util.py | 9 +++++++-- example/simulation.py | 7 +++++++ example_mp/simulation.py | 14 ++++++++------ 74 files changed, 243 insertions(+), 200 deletions(-) diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index f96370b83..6b91d1b16 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -1,15 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -import os -import warnings import logging -import numpy as np import pandas as pd -import yaml from activitysim.core import config from activitysim.core import inject diff --git a/activitysim/abm/models/accessibility.py b/activitysim/abm/models/accessibility.py index 1bf42d2da..68c25819d 100644 --- a/activitysim/abm/models/accessibility.py +++ b/activitysim/abm/models/accessibility.py @@ -1,8 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import * +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index fd575cabe..ed00b0b63 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd @@ -14,16 +17,9 @@ from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.interaction_sample import interaction_sample - -from activitysim.core.util import reindex -from activitysim.core.util import left_merge_on_index_and_col - -from .util import expressions from activitysim.core.util import assign_in_place from .util import logsums as logsum - -from .util.expressions import skim_time_period_label from .util.tour_destination import tour_destination_size_terms logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/atwork_subtour_frequency.py b/activitysim/abm/models/atwork_subtour_frequency.py index 40598709e..0987c45c7 100644 --- a/activitysim/abm/models/atwork_subtour_frequency.py +++ b/activitysim/abm/models/atwork_subtour_frequency.py @@ -1,22 +1,21 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd import numpy as np -from activitysim.core.interaction_simulate import interaction_simulate - from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import pipeline from activitysim.core import config from activitysim.core import inject -from activitysim.core.util import reindex - from .util.tour_frequency import process_atwork_subtours logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/atwork_subtour_mode_choice.py b/activitysim/abm/models/atwork_subtour_mode_choice.py index 4fe3eeaf0..df91d2a08 100644 --- a/activitysim/abm/models/atwork_subtour_mode_choice.py +++ b/activitysim/abm/models/atwork_subtour_mode_choice.py @@ -1,12 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd -from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import config from activitysim.core import inject @@ -16,7 +18,6 @@ from activitysim.core.util import assign_in_place from .util.mode import run_tour_mode_choice_simulate -from .util.mode import annotate_preprocessors from .util.mode import tour_mode_choice_spec logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index b2d06e055..7d2c7a6b2 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd @@ -13,7 +16,6 @@ from activitysim.core import inject from activitysim.core import timetable as tt from .util.vectorize_tour_scheduling import vectorize_subtour_scheduling -from .util import expressions from .util.mode import annotate_preprocessors from activitysim.core.util import assign_in_place diff --git a/activitysim/abm/models/auto_ownership.py b/activitysim/abm/models/auto_ownership.py index a4d1f16aa..5d8a76ea6 100644 --- a/activitysim/abm/models/auto_ownership.py +++ b/activitysim/abm/models/auto_ownership.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging from activitysim.core import simulate @@ -9,8 +13,6 @@ from activitysim.core import config from activitysim.core import inject -from .util import expressions - logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/cdap.py b/activitysim/abm/models/cdap.py index 76d06d9f1..978bd2f72 100644 --- a/activitysim/abm/models/cdap.py +++ b/activitysim/abm/models/cdap.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 38eee2601..f04f0bcfc 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -1,13 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging -import os import pandas as pd -import numpy as np -from activitysim.core import assign from activitysim.core import tracing from activitysim.core import config from activitysim.core import inject diff --git a/activitysim/abm/models/joint_tour_composition.py b/activitysim/abm/models/joint_tour_composition.py index 58f73d0da..3edd00cb5 100644 --- a/activitysim/abm/models/joint_tour_composition.py +++ b/activitysim/abm/models/joint_tour_composition.py @@ -1,10 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging -import numpy as np import pandas as pd from activitysim.core import simulate @@ -14,7 +16,6 @@ from activitysim.core import inject from .util import expressions -from activitysim.core.util import assign_in_place from .util.overlap import hh_time_window_overlap diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index b4162c976..36373350a 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + from future.utils import iteritems import logging diff --git a/activitysim/abm/models/joint_tour_frequency.py b/activitysim/abm/models/joint_tour_frequency.py index a7814f882..08a9281ba 100644 --- a/activitysim/abm/models/joint_tour_frequency.py +++ b/activitysim/abm/models/joint_tour_frequency.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np @@ -13,7 +16,6 @@ from activitysim.core import config from activitysim.core import inject -from activitysim.core.util import assign_in_place from .util import expressions from .util.overlap import hh_time_window_overlap from .util.tour_frequency import process_joint_tours diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 45e7036ca..ccc6d7724 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -1,7 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging diff --git a/activitysim/abm/models/joint_tour_scheduling.py b/activitysim/abm/models/joint_tour_scheduling.py index 86436be97..1a4137f50 100644 --- a/activitysim/abm/models/joint_tour_scheduling.py +++ b/activitysim/abm/models/joint_tour_scheduling.py @@ -1,17 +1,17 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging -import pandas as pd - from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline -from activitysim.core import timetable as tt from .util import expressions from .util.vectorize_tour_scheduling import vectorize_joint_tour_scheduling diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index 39e97ce62..0314a2b50 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -1,11 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging -import pandas as pd - from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import config @@ -13,7 +14,6 @@ from activitysim.core import pipeline from activitysim.core import timetable as tt -from .util import expressions from .util.vectorize_tour_scheduling import vectorize_tour_scheduling from activitysim.core.util import assign_in_place diff --git a/activitysim/abm/models/mandatory_tour_frequency.py b/activitysim/abm/models/mandatory_tour_frequency.py index 6f5f3e676..b30e213cf 100644 --- a/activitysim/abm/models/mandatory_tour_frequency.py +++ b/activitysim/abm/models/mandatory_tour_frequency.py @@ -1,11 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd -import numpy as np from activitysim.core import simulate from activitysim.core import tracing diff --git a/activitysim/abm/models/non_mandatory_destination.py b/activitysim/abm/models/non_mandatory_destination.py index ba9146d8f..f8a283831 100644 --- a/activitysim/abm/models/non_mandatory_destination.py +++ b/activitysim/abm/models/non_mandatory_destination.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index 08340c785..4e70dceab 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging from activitysim.core import tracing diff --git a/activitysim/abm/models/non_mandatory_tour_frequency.py b/activitysim/abm/models/non_mandatory_tour_frequency.py index 108777cde..cb938f71c 100644 --- a/activitysim/abm/models/non_mandatory_tour_frequency.py +++ b/activitysim/abm/models/non_mandatory_tour_frequency.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index d3a048ff0..270f9aeb6 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + from future.utils import iteritems import logging diff --git a/activitysim/abm/models/stop_frequency.py b/activitysim/abm/models/stop_frequency.py index cf44eb207..f12458e45 100644 --- a/activitysim/abm/models/stop_frequency.py +++ b/activitysim/abm/models/stop_frequency.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np diff --git a/activitysim/abm/models/tour_mode_choice.py b/activitysim/abm/models/tour_mode_choice.py index d2b46fb1c..9e05fbb0b 100644 --- a/activitysim/abm/models/tour_mode_choice.py +++ b/activitysim/abm/models/tour_mode_choice.py @@ -1,13 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd -import yaml -from activitysim.core import simulate from activitysim.core import tracing from activitysim.core import config from activitysim.core import inject diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 127213922..5c8c84229 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -1,9 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import range + import logging import numpy as np diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 3b33e4919..55176aeb3 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -1,13 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import zip from builtins import range + import logging import pandas as pd -import yaml from activitysim.core import simulate from activitysim.core import tracing @@ -15,7 +17,6 @@ from activitysim.core import inject from activitysim.core import pipeline from activitysim.core.util import force_garbage_collect -from activitysim.core.util import assign_in_place from .util.mode import annotate_preprocessors diff --git a/activitysim/abm/models/trip_purpose.py b/activitysim/abm/models/trip_purpose.py index 3d7fc8d7f..8db2dc848 100644 --- a/activitysim/abm/models/trip_purpose.py +++ b/activitysim/abm/models/trip_purpose.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np @@ -14,9 +17,7 @@ from activitysim.core import chunk from activitysim.core import pipeline -from activitysim.core.util import assign_in_place from .util import expressions -from activitysim.core.util import reindex logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/trip_purpose_and_destination.py b/activitysim/abm/models/trip_purpose_and_destination.py index 328cdaa5d..b53c52760 100644 --- a/activitysim/abm/models/trip_purpose_and_destination.py +++ b/activitysim/abm/models/trip_purpose_and_destination.py @@ -1,19 +1,19 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging -import numpy as np import pandas as pd from activitysim.core import tracing from activitysim.core import config from activitysim.core import pipeline -from activitysim.core import simulate from activitysim.core import inject -from activitysim.core.util import reindex from activitysim.core.util import assign_in_place from activitysim.abm.models.trip_purpose import run_trip_purpose diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 3c26c76e3..f981c3912 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -1,9 +1,9 @@ -from __future__ import division # ActivitySim # See full license in LICENSE.txt. -from __future__ import division - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import range import logging diff --git a/activitysim/abm/models/util/overlap.py b/activitysim/abm/models/util/overlap.py index a363882cf..a292fe523 100644 --- a/activitysim/abm/models/util/overlap.py +++ b/activitysim/abm/models/util/overlap.py @@ -3,8 +3,6 @@ import logging -import copy -import string import pandas as pd import numpy as np diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index c889d9ddc..f323762de 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -1,9 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import str import os.path -from itertools import product import pandas as pd import pandas.util.testing as pdt diff --git a/activitysim/abm/models/util/tour_frequency.py b/activitysim/abm/models/util/tour_frequency.py index fce37b8cc..3e146e0b7 100644 --- a/activitysim/abm/models/util/tour_frequency.py +++ b/activitysim/abm/models/util/tour_frequency.py @@ -1,7 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import str from builtins import range from future.utils import iteritems diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 3b2b7f298..2d6db236e 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -1,7 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd @@ -15,14 +18,8 @@ from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.interaction_sample import interaction_sample -from activitysim.core.util import reindex - from .util import expressions -from .util.logsums import compute_logsums -from .util.expressions import skim_time_period_label - from .util import logsums as logsum - from .util.tour_destination import tour_destination_size_terms diff --git a/activitysim/abm/tables/constants.py b/activitysim/abm/tables/constants.py index a592dea7a..dd6d7694a 100644 --- a/activitysim/abm/tables/constants.py +++ b/activitysim/abm/tables/constants.py @@ -1,4 +1,9 @@ +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 HHT_NONE = 0 HHT_FAMILY_MARRIED = 1 diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index b8d13f287..6840a8664 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -1,9 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import range + import logging import pandas as pd diff --git a/activitysim/abm/tables/input_store.py b/activitysim/abm/tables/input_store.py index 8c7399026..da2b6b46a 100644 --- a/activitysim/abm/tables/input_store.py +++ b/activitysim/abm/tables/input_store.py @@ -1,8 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import os -import warnings import logging import pandas as pd diff --git a/activitysim/abm/tables/landuse.py b/activitysim/abm/tables/landuse.py index 34fabf81d..6f3565c81 100644 --- a/activitysim/abm/tables/landuse.py +++ b/activitysim/abm/tables/landuse.py @@ -1,7 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging diff --git a/activitysim/abm/tables/persons.py b/activitysim/abm/tables/persons.py index c5bb81f3b..330f9d8d9 100644 --- a/activitysim/abm/tables/persons.py +++ b/activitysim/abm/tables/persons.py @@ -1,7 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 import logging diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index 7354843f0..c2ff3b57e 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -1,10 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -import os -import logging +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -import numpy as np +import logging import pandas as pd from activitysim.core import inject diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index dea416be2..9f3e66737 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -1,14 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import map -from builtins import range -from builtins import int - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 +from builtins import range +from builtins import int from future.utils import iteritems diff --git a/activitysim/abm/tables/table_dict.py b/activitysim/abm/tables/table_dict.py index df7b35c43..b2d039998 100644 --- a/activitysim/abm/tables/table_dict.py +++ b/activitysim/abm/tables/table_dict.py @@ -1,6 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging from activitysim.core import inject diff --git a/activitysim/abm/tables/time_windows.py b/activitysim/abm/tables/time_windows.py index 4078f2294..f0be5c08a 100644 --- a/activitysim/abm/tables/time_windows.py +++ b/activitysim/abm/tables/time_windows.py @@ -1,12 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import pandas as pd - from activitysim.core import inject from activitysim.core import config from activitysim.core import timetable as tt diff --git a/activitysim/abm/tables/tours.py b/activitysim/abm/tables/tours.py index 4afe693b5..a7e50106f 100644 --- a/activitysim/abm/tables/tours.py +++ b/activitysim/abm/tables/tours.py @@ -1,12 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. -import logging +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -import numpy as np -import pandas as pd +import logging -from activitysim.core.util import reindex from activitysim.core import inject logger = logging.getLogger(__name__) diff --git a/activitysim/abm/tables/trips.py b/activitysim/abm/tables/trips.py index 4061d4b4a..5a52e47d7 100644 --- a/activitysim/abm/tables/trips.py +++ b/activitysim/abm/tables/trips.py @@ -1,9 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -import logging +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -import pandas as pd +import logging from activitysim.core import inject diff --git a/activitysim/abm/test/run_mp.py b/activitysim/abm/test/run_mp.py index b2ec21716..c09a9e274 100644 --- a/activitysim/abm/test/run_mp.py +++ b/activitysim/abm/test/run_mp.py @@ -1,15 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 import os -import logging import pandas as pd import pandas.util.testing as pdt diff --git a/activitysim/abm/test/test_misc.py b/activitysim/abm/test/test_misc.py index aea46ba49..d4a07c996 100644 --- a/activitysim/abm/test/test_misc.py +++ b/activitysim/abm/test/test_misc.py @@ -1,14 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import str -import os -import tempfile +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 -import numpy as np +import os import pytest -import yaml from activitysim.core import inject diff --git a/activitysim/abm/test/test_mp_pipeline.py b/activitysim/abm/test/test_mp_pipeline.py index 6ad871d19..82b85abfc 100644 --- a/activitysim/abm/test/test_mp_pipeline.py +++ b/activitysim/abm/test/test_mp_pipeline.py @@ -1,10 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index a5d700c05..ef15931d5 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -1,10 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import * - - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index b08c4ae8f..85aff2903 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -1,13 +1,14 @@ # ActivitySim # See full license in LICENSE.txt. -from future.utils import iteritems - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import zip from builtins import object +from future.utils import iteritems import logging -import os from collections import OrderedDict import numpy as np diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 5e40503d6..3a478e98f 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -1,18 +1,15 @@ -from __future__ import division # ActivitySim # See full license in LICENSE.txt. -from math import ceil -import os +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np import pandas as pd -from .skim import SkimDictWrapper, SkimStackWrapper -from . import logit -from . import tracing -from . import pipeline from . import util logger = logging.getLogger(__name__) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 744dda40e..e7077ecc4 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -1,9 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 @@ -267,6 +265,7 @@ def config_file_path(file_name, mandatory=True): if isinstance(configs_dir, str): configs_dir = [configs_dir] + assert isinstance(configs_dir, list) file_path = None @@ -293,6 +292,7 @@ def backfill_settings(settings, backfill): if isinstance(configs_dir, str): configs_dir = [configs_dir] + assert isinstance(configs_dir, list) settings = {} diff --git a/activitysim/core/inject.py b/activitysim/core/inject.py index 263d485c3..9d82efd02 100644 --- a/activitysim/core/inject.py +++ b/activitysim/core/inject.py @@ -1,7 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from future.utils import iteritems diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 9ee7e60cb..13247de8d 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -1,8 +1,10 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import -from __future__ import division +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + from builtins import range diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index bf3d9bd33..5cb2c78b8 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -1,7 +1,10 @@ -from __future__ import division # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + import logging import numpy as np diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 31f25e144..edf6ce210 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -1,8 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import zip -from builtins import str + import logging import numpy as np diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index a10888877..a5098fe41 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -1,10 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import division -from __future__ import absolute_import - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import object + import logging import numpy as np diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 14ddb01b1..0129c87ee 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -1,13 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from builtins import * - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/orca.py b/activitysim/core/orca.py index 19a2a6497..9c35ee300 100644 --- a/activitysim/core/orca.py +++ b/activitysim/core/orca.py @@ -1,15 +1,15 @@ -# Orca -# Copyright (C) 2016 UrbanSim Inc. -# See full license in LICENSE. +# ActivitySim +# See full license in LICENSE.txt. -from __future__ import print_function +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 try: from inspect import getfullargspec as getargspec except ImportError: from inspect import getargspec import logging -import time import warnings from collections import Callable, namedtuple from contextlib import contextmanager diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 6ae587f1f..3c50de03b 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -1,18 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import * - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 - -from future.utils import iteritems - from builtins import next from builtins import map from builtins import object +from future.utils import iteritems + import os import logging import datetime as dt diff --git a/activitysim/core/random.py b/activitysim/core/random.py index f0bfdc835..3219b4071 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -1,8 +1,9 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import absolute_import - +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import range from builtins import object diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index fe80b4b70..89eb5835b 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -1,11 +1,13 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import print_function -from __future__ import division +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 +from builtins import range from future.utils import listvalues -from builtins import range + import sys import os diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 50160dac8..1aab3f879 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -1,11 +1,15 @@ # ActivitySim # See full license in LICENSE.txt. +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 +from builtins import range +from builtins import object + from future.utils import iteritems from future.utils import listvalues -from builtins import range -from builtins import object import logging from collections import OrderedDict diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index e6103223a..6677d459a 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -1,8 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/test/extensions/steps.py b/activitysim/core/test/extensions/steps.py index 73a4f252c..b182726d0 100644 --- a/activitysim/core/test/extensions/steps.py +++ b/activitysim/core/test/extensions/steps.py @@ -1,5 +1,4 @@ -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import * +from __future__ import (absolute_import, division, print_function, ) import pandas as pd from activitysim.core import inject diff --git a/activitysim/core/test/test_assign.py b/activitysim/core/test/test_assign.py index f52e5997c..19051577f 100644 --- a/activitysim/core/test/test_assign.py +++ b/activitysim/core/test/test_assign.py @@ -1,9 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/test/test_inject_defaults.py b/activitysim/core/test/test_inject_defaults.py index c9503c608..5f9b98af9 100644 --- a/activitysim/core/test/test_inject_defaults.py +++ b/activitysim/core/test/test_inject_defaults.py @@ -1,9 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index 991ccea29..c23d24acb 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -1,7 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import str import os.path import numpy as np diff --git a/activitysim/core/test/test_pipeline.py b/activitysim/core/test/test_pipeline.py index 354516a07..94966d64f 100644 --- a/activitysim/core/test/test_pipeline.py +++ b/activitysim/core/test/test_pipeline.py @@ -1,8 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) -from builtins import * +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/test/test_random.py b/activitysim/core/test/test_random.py index 341b57d8b..b142c3983 100644 --- a/activitysim/core/test/test_random.py +++ b/activitysim/core/test/test_random.py @@ -3,7 +3,6 @@ from __future__ import print_function -from builtins import str import numpy as np import pandas as pd import numpy.testing as npt diff --git a/activitysim/core/test/test_tracing.py b/activitysim/core/test/test_tracing.py index d554f8e58..410a026da 100644 --- a/activitysim/core/test/test_tracing.py +++ b/activitysim/core/test/test_tracing.py @@ -1,11 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. - -# ActivitySim -# See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 diff --git a/activitysim/core/test/test_util.py b/activitysim/core/test/test_util.py index 1c38c14f2..188d85e9f 100644 --- a/activitysim/core/test/test_util.py +++ b/activitysim/core/test/test_util.py @@ -1,7 +1,6 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import str import numpy as np import pandas as pd import pandas.util.testing as pdt diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index 4399d3149..f9585ba2c 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -1,9 +1,12 @@ # ActivitySim # See full license in LICENSE.txt. -from builtins import str +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 from builtins import range from builtins import object + import logging import numpy as np diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 2ac163967..624dd0304 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -1,14 +1,11 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import next -from builtins import str -from builtins import range - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 +from builtins import next +from builtins import range import os import logging diff --git a/activitysim/core/util.py b/activitysim/core/util.py index 39ccf98a1..9cd321d04 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -1,7 +1,12 @@ -from __future__ import division +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + from builtins import zip -import os import psutil import gc import logging diff --git a/example/simulation.py b/example/simulation.py index 69602265d..04e69a4d3 100644 --- a/example/simulation.py +++ b/example/simulation.py @@ -3,6 +3,13 @@ from __future__ import print_function +# import sys +# if not sys.warnoptions: # noqa: E402 +# import warnings +# warnings.filterwarnings('error', category=Warning) +# warnings.filterwarnings('ignore', category=PendingDeprecationWarning, module='future') +# warnings.filterwarnings('ignore', category=FutureWarning, module='pandas') + import logging # activitysim.abm imported for its side-effects (dependency injection) diff --git a/example_mp/simulation.py b/example_mp/simulation.py index b7b68dbbc..2030c2fbf 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -1,16 +1,18 @@ # ActivitySim # See full license in LICENSE.txt. -from __future__ import (absolute_import, division, print_function, unicode_literals) - -from builtins import * - +from __future__ import (absolute_import, division, print_function, ) from future.standard_library import install_aliases install_aliases() # noqa: E402 -import logging -from io import open import sys +import logging + +if not sys.warnoptions: # noqa: E402 + import warnings + warnings.filterwarnings('error', category=Warning) + warnings.filterwarnings('ignore', category=PendingDeprecationWarning, module='future') + warnings.filterwarnings('ignore', category=FutureWarning, module='pandas') from activitysim.core import inject from activitysim.core import tracing From 4749f28149b2f53c6b5bd351c3d486acd5d8d43d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 23 Oct 2018 08:40:08 -0400 Subject: [PATCH 031/122] add memory info to log_chunk_size --- activitysim/core/chunk.py | 3 ++- activitysim/core/util.py | 7 +++++-- setup.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 3a478e98f..30a46a510 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -42,7 +42,8 @@ def log_chunk_size(trace_label, cum): bytes = cum[1] logger.debug("%s #chunk CUM %s %s" % (trace_label, elements, util.GB(bytes))) - # logger.debug("%s %s" % (trace_label, util.memory_info())) + logger.debug("%s %s" % (trace_label, util.memory_info())) + logger.debug("%s %s" % (trace_label, util.memory_info(full=True))) def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): diff --git a/activitysim/core/util.py b/activitysim/core/util.py index 9cd321d04..75cb3d972 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -31,10 +31,13 @@ def df_size(df): return "%s %s" % (df.shape, GB(bytes)) -def memory_info(): +def memory_info(full=False): mi = psutil.Process().memory_full_info() - return "memory_info: vms: %s rss: %s uss: %s" % (GB(mi.vms), GB(mi.rss), GB(mi.uss)) + if full: + return "memory_info: full: %s" % str(mi) + else: + return "memory_info: vms: %s rss: %s uss: %s" % (GB(mi.vms), GB(mi.rss), GB(mi.uss)) def force_garbage_collect(): diff --git a/setup.py b/setup.py index f17f3daae..87a8f3f69 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ packages=find_packages(exclude=['*.tests']), install_requires=[ 'numpy >= 1.13.0', - 'openmatrix >= 0.2.4', + 'openmatrix >= 0.3.4.1', 'pandas >= 0.20.3', 'pyyaml >= 3.0', 'tables >= 3.3.0', From e2e9fdd4dfcc09a790f6f5267715ec20723f14c7 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 24 Oct 2018 19:10:14 -0400 Subject: [PATCH 032/122] core.mem and core.chunl.log --- .../abm/models/atwork_subtour_mode_choice.py | 2 +- activitysim/abm/models/tour_mode_choice.py | 2 +- activitysim/abm/models/trip_mode_choice.py | 2 +- activitysim/abm/models/trip_purpose.py | 9 +- .../models/trip_purpose_and_destination.py | 2 +- activitysim/abm/models/trip_scheduling.py | 4 +- activitysim/abm/models/util/cdap.py | 7 +- .../test/test_vectorize_tour_scheduling.py | 4 +- .../models/util/vectorize_tour_scheduling.py | 12 +- activitysim/core/chunk.py | 97 +++++++- activitysim/core/config.py | 4 +- activitysim/core/interaction_sample.py | 24 +- .../core/interaction_sample_simulate.py | 81 ++++--- activitysim/core/interaction_simulate.py | 55 +++-- activitysim/core/logit.py | 3 +- activitysim/core/mem.py | 104 ++++++++ activitysim/core/mp_tasks.py | 15 +- activitysim/core/pipeline.py | 10 +- activitysim/core/simulate.py | 226 +++++++++--------- activitysim/core/tracing.py | 3 - activitysim/core/util.py | 19 +- example_mp/configs/settings.yaml | 5 +- example_mp/simulation.py | 15 +- example_multi/simulation.py | 1 - 24 files changed, 456 insertions(+), 250 deletions(-) create mode 100644 activitysim/core/mem.py diff --git a/activitysim/abm/models/atwork_subtour_mode_choice.py b/activitysim/abm/models/atwork_subtour_mode_choice.py index df91d2a08..0bd850d70 100644 --- a/activitysim/abm/models/atwork_subtour_mode_choice.py +++ b/activitysim/abm/models/atwork_subtour_mode_choice.py @@ -13,7 +13,7 @@ from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from activitysim.core.util import assign_in_place diff --git a/activitysim/abm/models/tour_mode_choice.py b/activitysim/abm/models/tour_mode_choice.py index 9e05fbb0b..6f70bcfa3 100644 --- a/activitysim/abm/models/tour_mode_choice.py +++ b/activitysim/abm/models/tour_mode_choice.py @@ -13,7 +13,7 @@ from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from activitysim.core.util import assign_in_place from .util.mode import tour_mode_choice_spec diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 55176aeb3..35cc76841 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -16,7 +16,7 @@ from activitysim.core import config from activitysim.core import inject from activitysim.core import pipeline -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from .util.mode import annotate_preprocessors diff --git a/activitysim/abm/models/trip_purpose.py b/activitysim/abm/models/trip_purpose.py index 8db2dc848..88ba8065c 100644 --- a/activitysim/abm/models/trip_purpose.py +++ b/activitysim/abm/models/trip_purpose.py @@ -78,6 +78,8 @@ def choose_intermediate_trip_purpose(trips, probs_spec, trace_hh_id, trace_label choosers = pd.merge(trips.reset_index(), probs_spec, on=probs_join_cols, how='left').set_index('trip_id') + chunk.log_df(trace_label, 'choosers', choosers) + # select the matching depart range (this should result on in exactly one chooser row per trip) choosers = choosers[(choosers.start >= choosers['depart_range_start']) & ( choosers.start <= choosers['depart_range_end'])] @@ -90,9 +92,6 @@ def choose_intermediate_trip_purpose(trips, probs_spec, trace_hh_id, trace_label choosers[purpose_cols], trace_label=trace_label, trace_choosers=choosers) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - chunk.log_chunk_size(trace_label, cum_size) - if have_trace_targets: tracing.trace_df(choices, '%s.choices' % trace_label, columns=[None, 'trip_purpose']) tracing.trace_df(rands, '%s.rands' % trace_label, columns=[None, 'rand']) @@ -163,12 +162,16 @@ def run_trip_purpose( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + choices = choose_intermediate_trip_purpose( trips_chunk, probs_spec, trace_hh_id, trace_label=chunk_trace_label) + chunk.log_close(chunk_trace_label) + result_list.append(choices) if len(result_list) > 1: diff --git a/activitysim/abm/models/trip_purpose_and_destination.py b/activitysim/abm/models/trip_purpose_and_destination.py index b53c52760..9830bcf71 100644 --- a/activitysim/abm/models/trip_purpose_and_destination.py +++ b/activitysim/abm/models/trip_purpose_and_destination.py @@ -64,7 +64,7 @@ def trip_purpose_and_destination( model_settings = config.read_model_settings('trip_purpose_and_destination.yaml') MAX_ITERATIONS = model_settings.get('MAX_ITERATIONS', 5) - CLEANUP = model_settings.get('cleanup', True) + CLEANUP = model_settings.get('CLEANUP', True) trips_df = trips.to_frame() tours_merged_df = tours_merged.to_frame() diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index f981c3912..30c7abf82 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -360,7 +360,7 @@ def schedule_trips_in_leg( # calc_rows_per_chunk(chunk_size, persons, by_chunk_id=True) -def trip_scheduling_rpc(chunk_size, choosers, spec, trace_label=None): +def trip_scheduling_rpc(chunk_size, choosers, spec, trace_label): # NOTE we chunk chunk_id num_choosers = choosers['chunk_id'].max() + 1 @@ -556,7 +556,7 @@ def trip_scheduling( (choices.isnull().sum(), trips_df.shape[0], i)) if failfix != FAILFIX_DROP_AND_CLEANUP: - raise RuntimeError("%s setting '%' not enabled in settings" % + raise RuntimeError("%s setting '%s' not enabled in settings" % (FAILFIX, FAILFIX_DROP_AND_CLEANUP)) trips_df['failed'] = choices.isnull() diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 5031fba56..1d4e865dd 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -875,8 +875,7 @@ def _run_cdap( # tracing.trace_df(cdap_results, '%s.DUMP.cdap_results' % trace_label, # transpose=False, slicer='NONE') - cum_size = chunk.log_df_size(trace_label, 'persons', persons, cum_size=None) - chunk.log_chunk_size(trace_label, cum_size) + chunk.log_df(trace_label, 'persons', persons) # return dataframe with two columns return cdap_results @@ -961,6 +960,8 @@ def run_cdap( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) + chunk.log_open(chunk_trace_label, chunk_size) + choices = _run_cdap(persons_chunk, cdap_indiv_spec, cdap_interaction_coefficients, @@ -968,6 +969,8 @@ def run_cdap( locals_d, trace_hh_id, chunk_trace_label) + chunk.log_close(chunk_trace_label) + result_list.append(choices) # FIXME: this will require 2X RAM diff --git a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py index f3405f699..a6eb7b8c3 100644 --- a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py @@ -53,6 +53,7 @@ def test_vts(): persons = pd.DataFrame({ "income": [20, 30, 25] }, index=[1, 2, 3]) + inject.add_table('persons', persons) spec = pd.DataFrame({"Coefficient": [1.2]}, @@ -61,7 +62,8 @@ def test_vts(): inject.add_injectable("check_for_variability", True) - tdd_choices = vectorize_tour_scheduling(tours, persons, alts, spec) + tdd_choices = vectorize_tour_scheduling(tours, persons, alts, spec, + constants={}, chunk_size=0, trace_label='test_vts') # FIXME - dead reckoning regression # there's no real logic here - this is just what came out of the monte carlo diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index b251f59b5..c21a94cd4 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -10,7 +10,7 @@ from activitysim.core import tracing from activitysim.core import inject from activitysim.core import timetable as tt -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from activitysim.core.util import reindex from activitysim.core import chunk @@ -176,11 +176,14 @@ def _schedule_tours( previous_tour_info = get_previous_tour_by_tourid(tours[tour_owner_id_col], previous_tour, alts) tours = tours.join(previous_tour_info) + chunk.log_df(tour_trace_label, "tours", tours) + # build interaction dataset filtered to include only available tdd alts # dataframe columns start, end , duration, person_id, tdd # indexed (not unique) on tour_id choice_column = 'tdd' alt_tdd = tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col) + chunk.log_df(tour_trace_label, "alt_tdd", alt_tdd) locals_d = { 'tt': timetable @@ -202,10 +205,6 @@ def _schedule_tours( timetable.assign(tours[window_id_col], choices) - cum_size = chunk.log_df_size(tour_trace_label, "tours", tours, cum_size=None) - cum_size = chunk.log_df_size(tour_trace_label, "alt_tdd", alt_tdd, cum_size) - chunk.log_chunk_size(tour_trace_label, cum_size) - return choices @@ -280,12 +279,15 @@ def schedule_tours( chunk_trace_label = tracing.extend_trace_label(tour_trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else tour_trace_label + chunk.log_open(chunk_trace_label, chunk_size) choices = _schedule_tours(chooser_chunk, persons_merged, alts, spec, constants, timetable, timetable_window_id_col, previous_tour, tour_owner_id_col, tour_trace_label=chunk_trace_label) + chunk.log_close(chunk_trace_label) + result_list.append(choices) force_garbage_collect() diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 30a46a510..d35cfca28 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -5,17 +5,66 @@ from future.standard_library import install_aliases install_aliases() # noqa: E402 +from builtins import input + import logging +from collections import OrderedDict import numpy as np import pandas as pd from . import util +from . import mem logger = logging.getLogger(__name__) -def log_df_size(trace_label, table_name, df, cum_size): +CHUNK_LOG = OrderedDict() +CHUNK_SIZE = [] + +ELEMENTS_HWM = {} +BYTES_HWM = {} + + +def GB(bytes): + gb = (bytes / (1024 * 1024 * 1024.0)) + return "%s (%s GB)" % (bytes, round(gb, 2), ) + + +def log_open(trace_label, chunk_size): + + # if trace_label is None: + # trace_label = "noname_%s" % len(CHUNK_LOG) + + if len(CHUNK_LOG) > 0: + assert chunk_size == 0 + logger.debug("log_open nested chunker %s" % trace_label) + + CHUNK_LOG[trace_label] = OrderedDict() + CHUNK_SIZE.append(chunk_size) + + +def log_close(trace_label): + + # if trace_label is None: + # trace_label = "noname_%s" % (len(CHUNK_LOG)-1) + + assert CHUNK_LOG and next(reversed(CHUNK_LOG)) == trace_label + + logger.debug("log_close %s" % trace_label) + log_write() + # input("Return to continue: ") + + label, _ = CHUNK_LOG.popitem(last=True) + assert label == trace_label + CHUNK_SIZE.pop() + + +def log_df(trace_label, table_name, df): + + cur_chunker = next(reversed(CHUNK_LOG)) + if table_name in CHUNK_LOG[cur_chunker]: + logger.warning("log_df table %s for chunker %s already logged" % (table_name, cur_chunker)) if isinstance(df, pd.Series): elements = df.shape[0] @@ -23,27 +72,49 @@ def log_df_size(trace_label, table_name, df, cum_size): elif isinstance(df, pd.DataFrame): elements = df.shape[0] * df.shape[1] bytes = df.memory_usage(index=True).sum() + elif isinstance(df, np.ndarray): + elements = np.prod(df.shape) + bytes = df.nbytes else: + logger.error("log_df %s unknown type: %s" % (table_name, type(df))) assert False - # logger.debug("%s #chunk log_df_size %s %s %s %s" % - # (trace_label, table_name, df.shape, elements, util.GB(bytes))) + CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) + mem.log_memory_info("%s.chunk_log_df.%s" % (trace_label, table_name)) + + +def log_write(): + total_elements = 0 + total_bytes = 0 + for label in CHUNK_LOG: + tables = CHUNK_LOG[label] + for table_name in tables: + elements, bytes = tables[table_name] + logger.debug("%s table %s %s %s" % (label, table_name, elements, GB(bytes))) + total_elements += elements + total_bytes += bytes - if cum_size: - elements += cum_size[0] - bytes += cum_size[1] + logger.debug("%s total elements %s %s" % (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) - return elements, bytes + if total_elements > ELEMENTS_HWM.get('mark', 0): + ELEMENTS_HWM['mark'] = total_elements + ELEMENTS_HWM['chunker'] = CHUNK_LOG.keys() + if total_bytes > BYTES_HWM.get('mark', 0): + BYTES_HWM['mark'] = total_bytes + BYTES_HWM['chunker'] = CHUNK_LOG.keys() -def log_chunk_size(trace_label, cum): + if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: + logger.warning("total_elements (%s) > chunk_size (%s)" % (total_elements, CHUNK_SIZE[0])) - elements = cum[0] - bytes = cum[1] - logger.debug("%s #chunk CUM %s %s" % (trace_label, elements, util.GB(bytes))) - logger.debug("%s %s" % (trace_label, util.memory_info())) - logger.debug("%s %s" % (trace_label, util.memory_info(full=True))) +def log_chunk_high_water_mark(): + if 'mark' in ELEMENTS_HWM: + logger.info("chunk high_water_mark total_elements: %s in %s" % + (ELEMENTS_HWM['mark'], ELEMENTS_HWM['chunker']), ) + if 'mark' in BYTES_HWM: + logger.info("chunk high_water_mark total_bytes: %s in %s" % + (GB(BYTES_HWM['mark']), BYTES_HWM['chunker']), ) def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): diff --git a/activitysim/core/config.py b/activitysim/core/config.py index e7077ecc4..ebaa2a309 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -116,9 +116,9 @@ def handle_standard_args(parser=None): raise IOError("Could not find data dir '%s'" % args.data) inject.add_injectable("data_dir", args.data) if args.resume: - inject.add_injectable("resume_after", args.resume) + inject.add_injectable('resume_after', args.resume) if args.multiprocess: - inject.add_injectable("multiprocess", args.multiprocess) + inject.add_injectable('multiprocess', args.multiprocess) return args diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 13247de8d..5e1c3e563 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -14,7 +14,7 @@ import numpy as np import pandas as pd -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from . import logit from . import tracing @@ -217,7 +217,7 @@ def _interaction_sample( interaction_df = \ logit.interaction_dataset(choosers, alternatives, sample_size=alternative_count) - cum_size = chunk.log_df_size(trace_label, 'interaction_df', interaction_df, cum_size=None) + chunk.log_df(trace_label, 'interaction_df', interaction_df) assert alternative_count == len(interaction_df.index) / len(choosers.index) @@ -239,12 +239,10 @@ def _interaction_sample( else: trace_rows = trace_ids = None + # interaction_utilities is a df with one utility column and one row per interaction_df row interaction_utilities, trace_eval_results \ = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) - - # interaction_utilities is a df with one utility column and one row per interaction_df row - - cum_size = chunk.log_df_size(trace_label, 'interaction_utils', interaction_utilities, cum_size) + chunk.log_df(trace_label, 'interaction_utils', interaction_utilities) if have_trace_targets: tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, @@ -261,8 +259,7 @@ def _interaction_sample( utilities = pd.DataFrame( interaction_utilities.values.reshape(len(choosers), alternative_count), index=choosers.index) - - cum_size = chunk.log_df_size(trace_label, 'utilities', utilities, cum_size) + chunk.log_df(trace_label, 'utilities', utilities) if have_trace_targets: tracing.trace_df(utilities, tracing.extend_trace_label(trace_label, 'utilities'), @@ -274,8 +271,7 @@ def _interaction_sample( # probs is same shape as utilities, one row per chooser and one column for alternative probs = logit.utils_to_probs(utilities, allow_zero_probs=allow_zero_probs, trace_label=trace_label, trace_choosers=choosers) - - cum_size = chunk.log_df_size(trace_label, 'probs', probs, cum_size) + chunk.log_df(trace_label, 'probs', probs) if have_trace_targets: tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), @@ -287,6 +283,8 @@ def _interaction_sample( allow_zero_probs=allow_zero_probs, trace_label=trace_label) + chunk.log_df(trace_label, 'choices_df', choices_df) + # make_sample_choices should return choosers index as choices_df column assert choosers.index.name in choices_df.columns @@ -325,8 +323,6 @@ def _interaction_sample( assert (choices_df['pick_count'].max() < 4294967295) or (choices_df.empty) choices_df['pick_count'] = choices_df['pick_count'].astype(np.uint32) - chunk.log_chunk_size(trace_label, cum_size) - return choices_df @@ -440,11 +436,15 @@ def interaction_sample( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + choices = _interaction_sample(chooser_chunk, alternatives, spec, sample_size, alt_col_name, allow_zero_probs, skims, locals_d, chunk_trace_label) + chunk.log_close(chunk_trace_label) + if choices.shape[0] > 0: # might not be any if allow_zero_probs result_list.append(choices) diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index 5cb2c78b8..4eeb6b0bb 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -13,15 +13,15 @@ from . import logit from . import tracing from . import chunk +from . import util +from . import mem from .simulate import set_skim_wrapper_targets -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect from .interaction_simulate import eval_interaction_utilities logger = logging.getLogger(__name__) -DUMP = False - def _interaction_sample_simulate( choosers, alternatives, spec, @@ -109,21 +109,13 @@ def _interaction_sample_simulate( # so we just need to left join alternatives with choosers assert alternatives.index.name == choosers.index.name - interaction_df = pd.merge( - alternatives, choosers, - left_index=True, right_index=True, - suffixes=('', '_r')) - - tracing.dump_df(DUMP, interaction_df, trace_label, 'interaction_df') + with mem.trace('interaction_df', logger): + interaction_df = pd.merge( + alternatives, choosers, + left_index=True, right_index=True, + suffixes=('', '_r')) + chunk.log_df(trace_label, 'interaction_df', interaction_df) - if skims is not None: - set_skim_wrapper_targets(interaction_df, skims) - - # evaluate expressions from the spec multiply by coefficients and sum - # spec is df with one row per spec expression and one col with utility coefficient - # column names of choosers match spec index values - # utilities has utility value for element in the cross product of choosers and alternatives - # interaction_utilities is a df with one utility column and one row per row in alternative if have_trace_targets: trace_rows, trace_ids = tracing.interaction_trace_rows(interaction_df, choosers) @@ -133,10 +125,18 @@ def _interaction_sample_simulate( else: trace_rows = trace_ids = None - interaction_utilities, trace_eval_results \ - = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + if skims is not None: + set_skim_wrapper_targets(interaction_df, skims) - tracing.dump_df(DUMP, interaction_utilities, trace_label, 'interaction_utilities') + # evaluate expressions from the spec multiply by coefficients and sum + # spec is df with one row per spec expression and one col with utility coefficient + # column names of choosers match spec index values + # utilities has utility value for element in the cross product of choosers and alternatives + # interaction_utilities is a df with one utility column and one row per row in alternative + with mem.trace('interaction_utilities', logger): + interaction_utilities, trace_eval_results \ + = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) if have_trace_targets: tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, @@ -153,6 +153,7 @@ def _interaction_sample_simulate( # number of samples per chooser sample_counts = interaction_utilities.groupby(interaction_utilities.index).size().values + chunk.log_df(trace_label, 'sample_counts', sample_counts) # max number of alternatvies for any chooser max_sample_count = sample_counts.max() @@ -165,19 +166,21 @@ def _interaction_sample_simulate( # (we want to insert dummy utilities at the END of the list of alternative utilities) # inserts is a list of the indices at which we want to do the insertions inserts = np.repeat(last_row_offsets, max_sample_count - sample_counts) + chunk.log_df(trace_label, 'inserts', inserts) # insert the zero-prob utilities to pad each alternative set to same size padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) # reshape to array with one row per chooser, one column per alternative padded_utilities = padded_utilities.reshape(-1, max_sample_count) + chunk.log_df(trace_label, 'padded_utilities', padded_utilities) # convert to a dataframe with one row per chooser and one column per alternative - utilities_df = pd.DataFrame( - padded_utilities, - index=choosers.index) - - tracing.dump_df(DUMP, utilities_df, trace_label, 'utilities_df') + with mem.trace('utilities_df', logger): + utilities_df = pd.DataFrame( + padded_utilities, + index=choosers.index) + chunk.log_df(trace_label, 'utilities_df', utilities_df) if have_trace_targets: tracing.trace_df(utilities_df, tracing.extend_trace_label(trace_label, 'utilities'), @@ -185,15 +188,15 @@ def _interaction_sample_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - probs = logit.utils_to_probs(utilities_df, allow_zero_probs=allow_zero_probs, - trace_label=trace_label, trace_choosers=choosers) + with mem.trace('utils_to_probs', logger): + probs = logit.utils_to_probs(utilities_df, allow_zero_probs=allow_zero_probs, + trace_label=trace_label, trace_choosers=choosers) + chunk.log_df(trace_label, 'probs', probs) if have_trace_targets: tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), column_labels=['alternative', 'probability']) - tracing.dump_df(DUMP, probs, trace_label, 'probs') - if allow_zero_probs: zero_probs = (probs.sum(axis=1) == 0) if zero_probs.any(): @@ -203,7 +206,12 @@ def _interaction_sample_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - positions, rands = logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + with mem.trace('logit.make_choices', logger): + positions, rands = \ + logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + + chunk.log_df(trace_label, 'positions', positions) + chunk.log_df(trace_label, 'rands', rands) # shouldn't have chosen any of the dummy pad utilities assert positions.max() < max_sample_count @@ -218,23 +226,18 @@ def _interaction_sample_simulate( # create a series with index from choosers and the index of the chosen alternative choices = pd.Series(choices, index=choosers.index) + chunk.log_df(trace_label, 'choices', choices) + if allow_zero_probs and zero_probs.any(): # FIXME this is kind of gnarly, patch choice for zero_probs choices.loc[zero_probs] = zero_prob_choice_val - tracing.dump_df(DUMP, choices, trace_label, 'choices') - if have_trace_targets: tracing.trace_df(choices, tracing.extend_trace_label(trace_label, 'choices'), columns=[None, trace_choice_name]) tracing.trace_df(rands, tracing.extend_trace_label(trace_label, 'rands'), columns=[None, 'rand']) - cum_size = chunk.log_df_size(trace_label, 'interaction_df', interaction_df, cum_size=None) - cum_size = chunk.log_df_size(trace_label, 'interaction_utils', interaction_utilities, cum_size) - - chunk.log_chunk_size(trace_label, cum_size) - return choices @@ -336,12 +339,16 @@ def interaction_sample_simulate( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + choices = _interaction_sample_simulate( chooser_chunk, alternative_chunk, spec, choice_column, allow_zero_probs, zero_prob_choice_val, skims, locals_d, chunk_trace_label, trace_choice_name) + chunk.log_close(chunk_trace_label) + result_list.append(choices) force_garbage_collect() diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index edf6ce210..6781554be 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -17,10 +17,11 @@ from . import config from .simulate import set_skim_wrapper_targets from . import chunk +from . import mem from . import assign -from activitysim.core.util import force_garbage_collect +from activitysim.core.mem import force_garbage_collect logger = logging.getLogger(__name__) @@ -227,13 +228,13 @@ def _interaction_simulate( # cross join choosers and alternatives (cartesian product) # for every chooser, there will be a row for each alternative # index values (non-unique) are from alternatives df - interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) + with mem.trace('interaction_df', logger): + interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) + chunk.log_df(trace_label, 'interaction_df', interaction_df) if skims is not None: set_skim_wrapper_targets(interaction_df, skims) - cum_size = chunk.log_df_size(trace_label, 'interaction_df', interaction_df, cum_size=None) - # evaluate expressions from the spec multiply by coefficients and sum # spec is df with one row per spec expression and one col with utility coefficient # column names of model_design match spec index values @@ -249,8 +250,10 @@ def _interaction_simulate( else: trace_rows = trace_ids = None - interaction_utilities, trace_eval_results \ - = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + with mem.trace('interaction_utilities', logger): + interaction_utilities, trace_eval_results \ + = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) if have_trace_targets: tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, @@ -262,9 +265,11 @@ def _interaction_simulate( # reshape utilities (one utility column and one row per row in model_design) # to a dataframe with one row per chooser and one column per alternative - utilities = pd.DataFrame( - interaction_utilities.values.reshape(len(choosers), sample_size), - index=choosers.index) + with mem.trace('utilities', logger): + utilities = pd.DataFrame( + interaction_utilities.values.reshape(len(choosers), sample_size), + index=choosers.index) + chunk.log_df(trace_label, 'utilities', utilities) if have_trace_targets: tracing.trace_df(utilities, tracing.extend_trace_label(trace_label, 'utilities'), @@ -274,7 +279,9 @@ def _interaction_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + with mem.trace('probs', logger): + probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + chunk.log_df(trace_label, 'probs', probs) if have_trace_targets: tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), @@ -283,19 +290,23 @@ def _interaction_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - positions, rands = logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + with mem.trace('logit.make_choices', logger): + positions, rands = \ + logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + chunk.log_df(trace_label, 'positions', positions) + chunk.log_df(trace_label, 'rands', rands) # need to get from an integer offset into the alternative sample to the alternative index # that is, we want the index value of the row that is offset by rows into the # tranche of this choosers alternatives created by cross join of alternatives and choosers - - # offsets is the offset into model_design df of first row of chooser alternatives - offsets = np.arange(len(positions)) * sample_size - # resulting pandas Int64Index has one element per chooser row and is in same order as choosers - choices = interaction_utilities.index.take(positions + offsets) - - # create a series with index from choosers and the index of the chosen alternative - choices = pd.Series(choices, index=choosers.index) + with mem.trace('choices', logger): + # offsets is the offset into model_design df of first row of chooser alternatives + offsets = np.arange(len(positions)) * sample_size + # resulting Int64Index has one element per chooser row and is in same order as choosers + choices = interaction_utilities.index.take(positions + offsets) + # create a series with index from choosers and the index of the chosen alternative + choices = pd.Series(choices, index=choosers.index) + chunk.log_df(trace_label, 'choices', choices) if have_trace_targets: tracing.trace_df(choices, tracing.extend_trace_label(trace_label, 'choices'), @@ -303,8 +314,6 @@ def _interaction_simulate( tracing.trace_df(rands, tracing.extend_trace_label(trace_label, 'rands'), columns=[None, 'rand']) - chunk.log_chunk_size(trace_label, cum_size) - return choices @@ -410,11 +419,15 @@ def interaction_simulate( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + choices = _interaction_simulate(chooser_chunk, alternatives, spec, skims, locals_d, sample_size, chunk_trace_label, trace_choice_name) + chunk.log_close(chunk_trace_label) + result_list.append(choices) force_garbage_collect() diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index a5098fe41..d26fcaf6e 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -132,7 +132,8 @@ def utils_to_probs(utils, trace_label=None, exponentiated=False, allow_zero_prob trace_choosers=trace_choosers) # if allow_zero_probs, this may cause a RuntimeWarning: invalid value encountered in divide - with np.errstate(invalid='ignore' if allow_zero_probs else 'warn'): + with np.errstate(invalid='ignore' if allow_zero_probs else 'warn', + divide='ignore' if allow_zero_probs else 'warn'): np.divide(utils_arr, arr_sum.reshape(len(utils_arr), 1), out=utils_arr) PROB_MIN = 0.0 diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py new file mode 100644 index 000000000..36a2a052e --- /dev/null +++ b/activitysim/core/mem.py @@ -0,0 +1,104 @@ + +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +from builtins import input + +import psutil +import logging +import contextlib +import time +import inspect +import gc + +logger = logging.getLogger(__name__) + +MEM = {} + + +def force_garbage_collect(): + gc.collect() + + +def GB(bytes): + gb = (bytes / (1024 * 1024 * 1024.0)) + return "%s GB" % (round(gb, 2), ) + + +def format_elapsed_time(t): + return "%s seconds (%s minutes)" % (round(t, 3), round(t / 60.0, 1)) + + +def set_pause_threshold(gb): + bytes = gb * 1024 * 1024 * 1024 + MEM['pause'] = bytes + + +def _track_memory_info(trace_label): + + gc.collect() + mi = psutil.Process().memory_info() + logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % + (GB(mi.rss), GB(mi.vms), trace_label)) + + cur_mem = mi.vms + + if cur_mem > MEM.get('high_water_mark', 0): + MEM['high_water_mark'] = cur_mem + MEM['high_water_mark_trace_label'] = trace_label + logger.debug( + "memory_info new high_water_mark: %s trace_label: %s" % (GB(cur_mem), trace_label,)) + + if 'pause' in MEM and cur_mem > MEM['pause']: + MEM['pause'] = cur_mem + input("Return to continue: ") + + return cur_mem + + +def log_memory_info(trace_label): + + _track_memory_info(trace_label) + + mi = psutil.Process().memory_info() + logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % + (GB(mi.rss), GB(mi.vms), trace_label)) + + +def log_mem_high_water_mark(): + if 'high_water_mark' in MEM: + logger.info("mem high_water_mark %s in %s" % + (GB(MEM['high_water_mark']), MEM['high_water_mark_trace_label']), ) + + +@contextlib.contextmanager +def trace(msg, callers_logger, level=logging.DEBUG): + """ + A context manager to log delta time and memory to execute a block + + Parameters + ---------- + msg : str + callers_logger : logging.Logger + logger passed from caller's context + level : int, optional + Level at which to log, passed to ``logger.log``. + + """ + callerframerecord = inspect.stack()[2] + caller_name = inspect.getframeinfo(callerframerecord[0]).function + + prev_mem = _track_memory_info("%s.before" % msg) + t = time.time() + yield + t = time.time() - t + post_mem = _track_memory_info("%s.after" % msg) + + delta_mem = post_mem - prev_mem + + callers_logger.log(level, "Time to perform %s.%s : %s memory: %s (%s)" % + (caller_name, msg, format_elapsed_time(t), GB(post_mem), GB(delta_mem))) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 0129c87ee..704cf56e4 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -44,6 +44,16 @@ """ +def filter_warnings(): + + if setting('strict', False): # noqa: E402 + import warnings + warnings.filterwarnings('error', category=Warning) + warnings.filterwarnings('default', category=PendingDeprecationWarning, module='future') + warnings.filterwarnings('default', category=FutureWarning, module='pandas') + warnings.filterwarnings('default', category=RuntimeWarning, module='numpy') + + def pipeline_table_keys(pipeline_store, checkpoint_name=None): checkpoints = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] @@ -312,6 +322,8 @@ def setup_injectables_and_logging(injectables): inject.add_injectable("is_sub_task", True) + filter_warnings() + process_name = multiprocessing.current_process().name inject.add_injectable("log_file_prefix", process_name) tracing.config_logger() @@ -373,7 +385,8 @@ def profile_path(): def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): skim_buffer = kwargs - handle_standard_args() + # handle_standard_args() + setup_injectables_and_logging(injectables) if step_info['num_processes'] > 1: diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 3c50de03b..d268b261b 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -21,9 +21,9 @@ from . import config from . import random from . import tracing +from . import mem -from .util import memory_info -from .util import df_size +from . import util from .tracing import print_elapsed_time @@ -279,7 +279,7 @@ def add_checkpoint(checkpoint_name): continue logger.debug("add_checkpoint '%s' table '%s' %s" % - (checkpoint_name, table_name, df_size(df))) + (checkpoint_name, table_name, util.df_size(df))) write_df(df, table_name, checkpoint_name) # remember which checkpoint it was last written @@ -568,7 +568,7 @@ def run(models, resume_after=None): resume_after = _PIPELINE.last_checkpoint[CHECKPOINT_NAME] if resume_after: - logger.info('resume_after %s' % resume_after) + logger.info("resume_after %s" % resume_after) if resume_after in models: models = models[models.index(resume_after) + 1:] @@ -584,8 +584,6 @@ def run(models, resume_after=None): run_model(model) t1 = print_elapsed_time("run_model %s" % model, t1) - logger.debug('#mem after %s, %s' % (model, memory_info())) - t0 = print_elapsed_time("run (%s models)" % len(models), t0) # don't close the pipeline, as the user may want to read intermediate results from the store diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 89eb5835b..60a83fc83 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -23,10 +23,9 @@ from . import pipeline from . import config from . import util - from . import assign - from . import chunk +from . import mem logger = logging.getLogger(__name__) @@ -203,9 +202,6 @@ def to_array(x): values = OrderedDict() for expr in exprs: - sys.stdout.flush() - # logger.debug("eval_variables: %s" % expr) - # logger.debug("eval_variables %s" % util.memory_info()) try: if expr.startswith('@'): expr_values = to_array(eval(expr[1:], globals_dict, locals_dict)) @@ -215,7 +211,6 @@ def to_array(x): assert expr not in values values[expr] = expr_values except Exception as err: - print() # print ... logger.exception("Variable evaluation failed for: %s" % str(expr)) raise err @@ -481,10 +476,19 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, of `spec`. """ - trace_label = tracing.extend_trace_label(trace_label, 'mnl') + trace_label = tracing.extend_trace_label(trace_label, 'eval_mnl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - expression_values = eval_variables(spec.index, choosers, locals_d) + if have_trace_targets: + tracing.trace_df(choosers, '%s.choosers' % trace_label) + + with mem.trace('expression_values', logger): + expression_values = eval_variables(spec.index, choosers, locals_d) + chunk.log_df(trace_label, 'expression_values', expression_values) + + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) @@ -494,17 +498,21 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, # resulting in a dataframe with one row per chooser and one column per alternative # pandas.dot depends on column names of expression_values matching spec index values - utilities = compute_utilities(expression_values, spec) + with mem.trace('utilities', logger): + utilities = compute_utilities(expression_values, spec) + chunk.log_df(trace_label, "utilities", utilities) + + del expression_values if have_trace_targets: - # report these now in case utils_to_probs throws error on zero-probs - tracing.trace_df(choosers, '%s.choosers' % trace_label) tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + with mem.trace('probs', logger): + probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + chunk.log_df(trace_label, "probs", probs) + + del utilities if have_trace_targets: # report these now in case make_choices throws error on bad_choices @@ -517,12 +525,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, else: choices, rands = logit.make_choices(probs, trace_label=trace_label) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - - cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) - cum_size = chunk.log_df_size(trace_label, "utilities", utilities, cum_size) - cum_size = chunk.log_df_size(trace_label, "probs", probs, cum_size) - chunk.log_chunk_size(trace_label, cum_size) + del probs if have_trace_targets: tracing.trace_df(choices, '%s.choices' % trace_label, @@ -568,41 +571,68 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, of `spec`. """ - trace_label = tracing.extend_trace_label(trace_label, 'nl') + trace_label = tracing.extend_trace_label(trace_label, 'eval_nl') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) + if have_trace_targets: + tracing.trace_df(choosers, '%s.choosers' % trace_label) + # column names of expression_values match spec index values - expression_values = eval_variables(spec.index, choosers, locals_d) + with mem.trace('expression_values', logger): + expression_values = eval_variables(spec.index, choosers, locals_d) + chunk.log_df(trace_label, "expression_values", expression_values) + + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) # raw utilities of all the leaves - raw_utilities = compute_utilities(expression_values, spec) + with mem.trace('raw_utilities', logger): + raw_utilities = compute_utilities(expression_values, spec) + chunk.log_df(trace_label, "raw_utilities", raw_utilities) - # exponentiated utilities of leaves and nests - nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + if have_trace_targets: + tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, + column_labels=['alternative', 'utility']) - # probabilities of alternatives relative to siblings sharing the same nest - nested_probabilities = compute_nested_probabilities(nested_exp_utilities, nest_spec, - trace_label=trace_label) + del expression_values - # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) - base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) + # exponentiated utilities of leaves and nests + with mem.trace('nested_exp_utilities', logger): + nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + chunk.log_df(trace_label, "nested_exp_utils", nested_exp_utilities) + + del raw_utilities if have_trace_targets: - # report these now in case of no_choices - tracing.trace_df(choosers, '%s.choosers' % trace_label) - tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, - column_labels=['alternative', 'utility']) tracing.trace_df(nested_exp_utilities, '%s.nested_exp_utilities' % trace_label, column_labels=['alternative', 'utility']) + + # probabilities of alternatives relative to siblings sharing the same nest + with mem.trace('nested_probabilities', logger): + nested_probabilities = \ + compute_nested_probabilities(nested_exp_utilities, nest_spec, trace_label=trace_label) + chunk.log_df(trace_label, "nested_probs", nested_probabilities) + + del nested_exp_utilities + + if have_trace_targets: tracing.trace_df(nested_probabilities, '%s.nested_probabilities' % trace_label, column_labels=['alternative', 'probability']) + + # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) + with mem.trace('base_probabilities', logger): + base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) + chunk.log_df(trace_label, "base_probs", base_probabilities) + + del nested_probabilities + + if have_trace_targets: tracing.trace_df(base_probabilities, '%s.base_probabilities' % trace_label, column_labels=['alternative', 'probability']) - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) # note base_probabilities could all be zero since we allowed all probs for nests to be zero # check here to print a clear message but make_choices will raise error if probs don't sum to 1 @@ -625,13 +655,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, else: choices, rands = logit.make_choices(base_probabilities, trace_label=trace_label) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - cum_size = chunk.log_df_size(trace_label, "expression_values", expression_values, cum_size) - cum_size = chunk.log_df_size(trace_label, "raw_utilities", raw_utilities, cum_size) - cum_size = chunk.log_df_size(trace_label, "nested_exp_utils", nested_exp_utilities, cum_size) - cum_size = chunk.log_df_size(trace_label, "nested_probs", nested_probabilities, cum_size) - cum_size = chunk.log_df_size(trace_label, "base_probs", base_probabilities, cum_size) - chunk.log_chunk_size(trace_label, cum_size) + del base_probabilities if have_trace_targets: tracing.trace_df(choices, '%s.choices' % trace_label, @@ -763,6 +787,8 @@ def simple_simulate(choosers, spec, nest_spec, skims=None, locals_d=None, chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + choices = _simple_simulate( chooser_chunk, spec, nest_spec, skims, locals_d, @@ -770,6 +796,8 @@ def simple_simulate(choosers, spec, nest_spec, skims=None, locals_d=None, chunk_trace_label, trace_choice_name) + chunk.log_close(chunk_trace_label) + result_list.append(choices) if len(result_list) > 1: @@ -792,39 +820,38 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): # FIXME - untested and not currently used by any models... - trace_label = tracing.extend_trace_label(trace_label, 'mnl') + trace_label = tracing.extend_trace_label(trace_label, 'eval_mnl_logsums') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) logger.debug("running eval_mnl_logsums") - t00 = t0 = print_elapsed_time() # trace choosers if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) - expression_values = eval_variables(spec.index, choosers, locals_d) - t0 = print_elapsed_time("eval_variables", t0, debug=True) + with mem.trace('expression_values', logger): + expression_values = eval_variables(spec.index, choosers, locals_d) + chunk.log_df(trace_label, 'expression_values', expression_values) + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) # utility values - utilities = compute_utilities(expression_values, spec) - t0 = print_elapsed_time("compute_utilities", t0, debug=True) + with mem.trace('utilities', logger): + utilities = compute_utilities(expression_values, spec) + chunk.log_df(trace_label, "utilities", utilities,) - # trace expression_values - if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) del expression_values # done with expression_values # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row - logsums = np.log(np.exp(utilities.values).sum(axis=1)) - logsums = pd.Series(logsums, index=choosers.index) - t0 = print_elapsed_time("logsums", t0, debug=True) + with mem.trace('logsums', logger): + logsums = np.log(np.exp(utilities.values).sum(axis=1)) + logsums = pd.Series(logsums, index=choosers.index) + chunk.log_df(trace_label, "logsums", logsums) # trace utilities if have_trace_targets: @@ -832,29 +859,12 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): utilities['logsum'] = logsums tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) - cum_size = chunk.log_df_size(trace_label, "utilities", utilities, cum_size) - - # trace logsums - if have_trace_targets: tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - cum_size = chunk.log_df_size(trace_label, "logsums", logsums, cum_size) - chunk.log_chunk_size(trace_label, cum_size) - - t0 = print_elapsed_time("end eval_mnl_logsums", t00, debug=True) return logsums -def print_elapsed_time(msg=None, t0=None, debug=False): - - # msg = "%s %s" % (msg, util.memory_info()) - # print(msg) - # sys.stdout.write('\a') - # sys.stdout.flush() - return tracing.print_elapsed_time(msg, t0, debug=debug) - - def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): """ like eval_nl except return logsums instead of making choices @@ -865,70 +875,62 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): Index will be that of `choosers`, values will be nest logsum based on spec column values """ - trace_label = tracing.extend_trace_label(trace_label, 'nl') + trace_label = tracing.extend_trace_label(trace_label, 'eval_nl_logsums') have_trace_targets = trace_label and tracing.has_trace_targets(choosers) - t00 = t0 = print_elapsed_time("begin eval_nl_logsums") - # trace choosers if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - cum_size = chunk.log_df_size(trace_label, 'choosers', choosers, cum_size=None) # - eval spec expressions # column names of expression_values match spec index values - expression_values = eval_variables(spec.index, choosers, locals_d) - t0 = print_elapsed_time("eval_variables", t0, debug=True) + with mem.trace('expression_values', logger): + expression_values = eval_variables(spec.index, choosers, locals_d) + chunk.log_df(trace_label, 'expression_values', expression_values) + + if have_trace_targets: + tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + column_labels=['expression', None]) if config.setting('check_for_variability'): _check_for_variability(expression_values, trace_label) - t0 = print_elapsed_time("_check_for_variability", t0, debug=True) # - raw utilities of all the leaves - raw_utilities = compute_utilities(expression_values, spec) - t0 = print_elapsed_time("compute_utilities", t0, debug=True) + with mem.trace('raw_utilities', logger): + raw_utilities = compute_utilities(expression_values, spec) + chunk.log_df(trace_label, "raw_utilities", raw_utilities) - # trace expression_values if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - cum_size = chunk.log_df_size(trace_label, 'expression_values', expression_values, cum_size) - del expression_values # done with expression_values + tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, + column_labels=['alternative', 'utility']) + + with mem.trace('del expression_values', logger): + del expression_values # done with expression_values # - exponentiated utilities of leaves and nests - nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) - t0 = print_elapsed_time("compute_nested_exp_utilities", t0, debug=True) + with mem.trace('nested_exp_utilities', logger): + nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) - # trace raw_utilities - if have_trace_targets: - tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, - column_labels=['alternative', 'utility']) - cum_size = chunk.log_df_size(trace_label, "raw_utilities", raw_utilities, cum_size) - del raw_utilities # done with raw_utilities + with mem.trace('del raw_utilities', logger): + del raw_utilities # done with raw_utilities # - logsums - logsums = np.log(nested_exp_utilities.root) - logsums = pd.Series(logsums, index=choosers.index) - t0 = print_elapsed_time("logsums", t0, debug=True) + with mem.trace('logsums', logger): + logsums = np.log(nested_exp_utilities.root) + logsums = pd.Series(logsums, index=choosers.index) + chunk.log_df(trace_label, "logsums", logsums) - # trace nested_exp_utilities if have_trace_targets: # add logsum to nested_exp_utilities for tracing nested_exp_utilities['logsum'] = logsums tracing.trace_df(nested_exp_utilities, '%s.nested_exp_utilities' % trace_label, column_labels=['alternative', 'utility']) - cum_size = \ - chunk.log_df_size(trace_label, "nested_exp_utilities", nested_exp_utilities, cum_size) - del nested_exp_utilities # done with nested_exp_utilities - - # trace logsums - if have_trace_targets: tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - cum_size = chunk.log_df_size(trace_label, "logsums", logsums, cum_size) - chunk.log_chunk_size(trace_label, cum_size) - t0 = print_elapsed_time("end eval_nl_logsums", t00, debug=True) + with mem.trace('del nested_exp_utilities', logger): + del nested_exp_utilities # done with nested_exp_utilities return logsums @@ -1017,11 +1019,15 @@ def simple_simulate_logsums(choosers, spec, nest_spec, chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label + chunk.log_open(chunk_trace_label, chunk_size) + logsums = _simple_simulate_logsums( chooser_chunk, spec, nest_spec, skims, locals_d, chunk_trace_label) + chunk.log_close(chunk_trace_label) + result_list.append(logsums) if len(result_list) > 1: diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 624dd0304..962372070 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -69,7 +69,6 @@ def timing(msg, callers_logger, level=logging.DEBUG): Parameters ---------- msg : str - Will be prefixed with "start: " and "finish: ". callers_logger : logging.Logger logger passed from caller's context level : int, optional @@ -472,8 +471,6 @@ def get_trace_target(df, slicer): target_ids = inject.get_injectable('trace_%s' % table_name, []) elif slicer == 'TAZ': target_ids = inject.get_injectable('trace_od', []) - else: - raise RuntimeError("slice_canonically: bad slicer '%s'" % (slicer, )) return target_ids, column diff --git a/activitysim/core/util.py b/activitysim/core/util.py index 75cb3d972..c8f85e804 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -7,8 +7,6 @@ from builtins import zip -import psutil -import gc import logging from operator import itemgetter @@ -18,6 +16,8 @@ from zbox import toolz as tz +from . import mem + logger = logging.getLogger(__name__) @@ -31,21 +31,6 @@ def df_size(df): return "%s %s" % (df.shape, GB(bytes)) -def memory_info(full=False): - - mi = psutil.Process().memory_full_info() - if full: - return "memory_info: full: %s" % str(mi) - else: - return "memory_info: vms: %s rss: %s uss: %s" % (GB(mi.vms), GB(mi.rss), GB(mi.uss)) - - -def force_garbage_collect(): - - gc.collect() - logger.debug("force_garbage_collect %s" % memory_info()) - - def left_merge_on_index_and_col(left_df, right_df, join_col, target_col): """ like pandas left merge, but join on both index and a specified join_col diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 0f1e54541..99c7dc853 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,15 +1,16 @@ inherit_settings: True -households_sample_size: 300 +households_sample_size: 100 +#households_sample_size: 123281 # 160 GB #chunk_size: 10000000000 - chunk_size: 3000000000 multiprocess: False profile: False +strict: False check_for_variability: False diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 2030c2fbf..272e98e2b 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -8,17 +8,13 @@ import sys import logging -if not sys.warnoptions: # noqa: E402 - import warnings - warnings.filterwarnings('error', category=Warning) - warnings.filterwarnings('ignore', category=PendingDeprecationWarning, module='future') - warnings.filterwarnings('ignore', category=FutureWarning, module='pandas') - +from activitysim.core import mem from activitysim.core import inject from activitysim.core import tracing from activitysim.core import config from activitysim.core import pipeline from activitysim.core import mp_tasks +from activitysim.core import chunk # from activitysim import abm @@ -57,6 +53,8 @@ def run(run_list, injectables=None): inject.add_injectable('configs_dir', ['configs', '../example/configs']) config.handle_standard_args() + + mp_tasks.filter_warnings() tracing.config_logger() t0 = tracing.print_elapsed_time() @@ -69,7 +67,7 @@ def run(run_list, injectables=None): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables - injectables = ['data_dir', 'configs_dir', 'output_dir'] + injectables = ['data_dir', 'configs_dir', 'output_dir', 'strict'] injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None @@ -82,3 +80,6 @@ def run(run_list, injectables=None): run(run_list, injectables) t0 = tracing.print_elapsed_time("everything", t0) + + chunk.log_chunk_high_water_mark() + mem.log_mem_high_water_mark() diff --git a/example_multi/simulation.py b/example_multi/simulation.py index 2e5f6a578..c8cd4d8e5 100644 --- a/example_multi/simulation.py +++ b/example_multi/simulation.py @@ -7,7 +7,6 @@ from activitysim.core import pipeline from activitysim.core import simulate as asim -from activitysim.core.util import memory_info import pandas as pd import numpy as np From 26db144881fe4707c89c0eee6eff6fb3e664cb07 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 24 Oct 2018 22:25:26 -0400 Subject: [PATCH 033/122] log_df also taketh away for del --- activitysim/core/chunk.py | 64 +++++++++++++------ .../core/interaction_sample_simulate.py | 23 ++++++- activitysim/core/mem.py | 6 +- activitysim/core/simulate.py | 23 +++++-- example_mp/simulation.py | 2 +- 5 files changed, 87 insertions(+), 31 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index d35cfca28..f6221f6a2 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -63,39 +63,47 @@ def log_close(trace_label): def log_df(trace_label, table_name, df): cur_chunker = next(reversed(CHUNK_LOG)) - if table_name in CHUNK_LOG[cur_chunker]: - logger.warning("log_df table %s for chunker %s already logged" % (table_name, cur_chunker)) - - if isinstance(df, pd.Series): - elements = df.shape[0] - bytes = df.memory_usage(index=True) - elif isinstance(df, pd.DataFrame): - elements = df.shape[0] * df.shape[1] - bytes = df.memory_usage(index=True).sum() - elif isinstance(df, np.ndarray): - elements = np.prod(df.shape) - bytes = df.nbytes + + if df is None: + elements, bytes = list(-n for n in CHUNK_LOG.get(cur_chunker).pop(table_name)) + df_trace_label = "%s.del_df.%s" % (trace_label, table_name) else: - logger.error("log_df %s unknown type: %s" % (table_name, type(df))) - assert False - CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) - mem.log_memory_info("%s.chunk_log_df.%s" % (trace_label, table_name)) + if isinstance(df, pd.Series): + elements = df.shape[0] + bytes = df.memory_usage(index=True) + elif isinstance(df, pd.DataFrame): + elements = df.shape[0] * df.shape[1] + bytes = df.memory_usage(index=True).sum() + elif isinstance(df, np.ndarray): + elements = np.prod(df.shape) + bytes = df.nbytes + else: + logger.error("log_df %s unknown type: %s" % (table_name, type(df))) + assert False + CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) -def log_write(): + df_trace_label = "%s.add_df.%s" % (trace_label, table_name) + + logger.debug("df elements %s %s : %s " % (elements, GB(bytes), df_trace_label)) + + total_elements, total_bytes = _log_totals() + logger.debug("%s total elements %s %s" % (total_elements, GB(total_bytes), df_trace_label)) + + mem.log_memory_info(df_trace_label) + + +def _log_totals(): total_elements = 0 total_bytes = 0 for label in CHUNK_LOG: tables = CHUNK_LOG[label] for table_name in tables: elements, bytes = tables[table_name] - logger.debug("%s table %s %s %s" % (label, table_name, elements, GB(bytes))) total_elements += elements total_bytes += bytes - logger.debug("%s total elements %s %s" % (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) - if total_elements > ELEMENTS_HWM.get('mark', 0): ELEMENTS_HWM['mark'] = total_elements ELEMENTS_HWM['chunker'] = CHUNK_LOG.keys() @@ -107,6 +115,22 @@ def log_write(): if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: logger.warning("total_elements (%s) > chunk_size (%s)" % (total_elements, CHUNK_SIZE[0])) + return total_elements, total_bytes + + +def log_write(): + total_elements = 0 + total_bytes = 0 + for label in CHUNK_LOG: + tables = CHUNK_LOG[label] + for table_name in tables: + elements, bytes = tables[table_name] + logger.debug("%s table %s %s %s" % (label, table_name, elements, GB(bytes))) + total_elements += elements + total_bytes += bytes + + logger.debug("%s total elements %s %s" % (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) + def log_chunk_high_water_mark(): if 'mark' in ELEMENTS_HWM: diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index 4eeb6b0bb..bea989ce9 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -138,6 +138,9 @@ def _interaction_sample_simulate( = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) + del interaction_df + chunk.log_df(trace_label, 'interaction_df', None) + if have_trace_targets: tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, tracing.extend_trace_label(trace_label, 'eval')) @@ -168,9 +171,18 @@ def _interaction_sample_simulate( inserts = np.repeat(last_row_offsets, max_sample_count - sample_counts) chunk.log_df(trace_label, 'inserts', inserts) + del sample_counts + chunk.log_df(trace_label, 'sample_counts', None) + # insert the zero-prob utilities to pad each alternative set to same size padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) + del inserts + chunk.log_df(trace_label, 'inserts', None) + + del interaction_utilities + chunk.log_df(trace_label, 'interaction_utilities', None) + # reshape to array with one row per chooser, one column per alternative padded_utilities = padded_utilities.reshape(-1, max_sample_count) chunk.log_df(trace_label, 'padded_utilities', padded_utilities) @@ -182,6 +194,9 @@ def _interaction_sample_simulate( index=choosers.index) chunk.log_df(trace_label, 'utilities_df', utilities_df) + del padded_utilities + chunk.log_df(trace_label, 'padded_utilities', None) + if have_trace_targets: tracing.trace_df(utilities_df, tracing.extend_trace_label(trace_label, 'utilities'), column_labels=['alternative', 'utility']) @@ -193,6 +208,9 @@ def _interaction_sample_simulate( trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) + del utilities_df + chunk.log_df(trace_label, 'utilities_df', None) + if have_trace_targets: tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), column_labels=['alternative', 'probability']) @@ -213,6 +231,9 @@ def _interaction_sample_simulate( chunk.log_df(trace_label, 'positions', positions) chunk.log_df(trace_label, 'rands', rands) + del probs + chunk.log_df(trace_label, 'probs', None) + # shouldn't have chosen any of the dummy pad utilities assert positions.max() < max_sample_count @@ -221,7 +242,7 @@ def _interaction_sample_simulate( # tranche of this choosers alternatives created by cross join of alternatives and choosers # resulting pandas Int64Index has one element per chooser row and is in same order as choosers - choices = interaction_df[choice_column].take(positions + first_row_offsets) + choices = alternatives[choice_column].take(positions + first_row_offsets) # create a series with index from choosers and the index of the chosen alternative choices = pd.Series(choices, index=choosers.index) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 36a2a052e..6697795ee 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -92,6 +92,8 @@ def trace(msg, callers_logger, level=logging.DEBUG): callerframerecord = inspect.stack()[2] caller_name = inspect.getframeinfo(callerframerecord[0]).function + msg = "%s.%s" % (caller_name, msg) + prev_mem = _track_memory_info("%s.before" % msg) t = time.time() yield @@ -100,5 +102,5 @@ def trace(msg, callers_logger, level=logging.DEBUG): delta_mem = post_mem - prev_mem - callers_logger.log(level, "Time to perform %s.%s : %s memory: %s (%s)" % - (caller_name, msg, format_elapsed_time(t), GB(post_mem), GB(delta_mem))) + callers_logger.log(level, "Time to perform %s : %s memory: %s (%s)" % + (msg, format_elapsed_time(t), GB(post_mem), GB(delta_mem))) diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 60a83fc83..a0c4d33e9 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -503,6 +503,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, chunk.log_df(trace_label, "utilities", utilities) del expression_values + chunk.log_df(trace_label, 'expression_values', None) if have_trace_targets: tracing.trace_df(utilities, '%s.utilities' % trace_label, @@ -513,6 +514,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, chunk.log_df(trace_label, "probs", probs) del utilities + chunk.log_df(trace_label, 'utilities', None) if have_trace_targets: # report these now in case make_choices throws error on bad_choices @@ -526,6 +528,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, choices, rands = logit.make_choices(probs, trace_label=trace_label) del probs + chunk.log_df(trace_label, 'probs', None) if have_trace_targets: tracing.trace_df(choices, '%s.choices' % trace_label, @@ -599,13 +602,15 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, column_labels=['alternative', 'utility']) del expression_values + chunk.log_df(trace_label, 'expression_values', None) # exponentiated utilities of leaves and nests with mem.trace('nested_exp_utilities', logger): nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) - chunk.log_df(trace_label, "nested_exp_utils", nested_exp_utilities) + chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) del raw_utilities + chunk.log_df(trace_label, 'raw_utilities', None) if have_trace_targets: tracing.trace_df(nested_exp_utilities, '%s.nested_exp_utilities' % trace_label, @@ -615,9 +620,10 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, with mem.trace('nested_probabilities', logger): nested_probabilities = \ compute_nested_probabilities(nested_exp_utilities, nest_spec, trace_label=trace_label) - chunk.log_df(trace_label, "nested_probs", nested_probabilities) + chunk.log_df(trace_label, "nested_probabilities", nested_probabilities) del nested_exp_utilities + chunk.log_df(trace_label, 'nested_exp_utilities', None) if have_trace_targets: tracing.trace_df(nested_probabilities, '%s.nested_probabilities' % trace_label, @@ -626,9 +632,10 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) with mem.trace('base_probabilities', logger): base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) - chunk.log_df(trace_label, "base_probs", base_probabilities) + chunk.log_df(trace_label, "base_probabilities", base_probabilities) del nested_probabilities + chunk.log_df(trace_label, 'nested_probabilities', None) if have_trace_targets: tracing.trace_df(base_probabilities, '%s.base_probabilities' % trace_label, @@ -656,6 +663,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, choices, rands = logit.make_choices(base_probabilities, trace_label=trace_label) del base_probabilities + chunk.log_df(trace_label, 'base_probabilities', None) if have_trace_targets: tracing.trace_df(choices, '%s.choices' % trace_label, @@ -845,6 +853,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): chunk.log_df(trace_label, "utilities", utilities,) del expression_values # done with expression_values + chunk.log_df(trace_label, 'expression_values', None) # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row @@ -904,16 +913,16 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, column_labels=['alternative', 'utility']) - with mem.trace('del expression_values', logger): - del expression_values # done with expression_values + del expression_values # done with expression_values + chunk.log_df(trace_label, 'expression_values', None) # - exponentiated utilities of leaves and nests with mem.trace('nested_exp_utilities', logger): nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) - with mem.trace('del raw_utilities', logger): - del raw_utilities # done with raw_utilities + del raw_utilities # done with raw_utilities + chunk.log_df(trace_label, 'raw_utilities', None) # - logsums with mem.trace('logsums', logger): diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 272e98e2b..01f26c1c1 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -67,7 +67,7 @@ def run(run_list, injectables=None): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables - injectables = ['data_dir', 'configs_dir', 'output_dir', 'strict'] + injectables = ['data_dir', 'configs_dir', 'output_dir'] injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None From f8eb4198bfc558647086f5add5beb5a12e5a5ee8 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 25 Oct 2018 18:54:20 -0400 Subject: [PATCH 034/122] filter mandatory_tour_scheduling choosers, narrow tdd_alts --- .../abm/models/joint_tour_destination.py | 2 +- .../abm/models/mandatory_scheduling.py | 18 ++++++++ activitysim/abm/models/school_location.py | 2 +- .../models/trip_purpose_and_destination.py | 37 ++++++++------- .../abm/models/util/tour_destination.py | 2 +- activitysim/abm/models/util/trip.py | 2 +- .../models/util/vectorize_tour_scheduling.py | 45 ++++++++++--------- activitysim/abm/tables/time_windows.py | 4 ++ activitysim/core/chunk.py | 30 ++++++++----- activitysim/core/interaction_sample.py | 2 +- .../core/interaction_sample_simulate.py | 10 ++--- activitysim/core/interaction_simulate.py | 12 ++--- activitysim/core/logit.py | 3 +- activitysim/core/mem.py | 13 +++--- activitysim/core/simulate.py | 32 ++++++------- .../configs/mandatory_tour_scheduling.yaml | 14 ++++++ example_mp/configs/settings.yaml | 12 ++--- example_mp/simulation.py | 1 + 18 files changed, 146 insertions(+), 95 deletions(-) create mode 100644 example/configs/mandatory_tour_scheduling.yaml diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index 36373350a..c72b0c9d6 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -183,7 +183,7 @@ def joint_tour_destination_sample( choices = pd.concat(choices_list) - # - # NARROW + # - NARROW choices['tour_type_id'] = choices['tour_type_id'].astype(np.uint8) inject.add_table('joint_tour_destination_sample', choices) diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index 0314a2b50..c1923ccf9 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -23,6 +23,22 @@ DUMP = False +def filter_chooser_columns(choosers, model_settings): + + chooser_columns = model_settings.get('CHOOSER_COLUMNS', []) + + missing_columns = [c for c in chooser_columns if c not in choosers] + if missing_columns: + logger.warning("filter_chooser_columns missing_columns %s" % missing_columns) + bug + + # ignore any columns not appearing in choosers df + chooser_columns = [c for c in chooser_columns if c in choosers] + + choosers = choosers[chooser_columns] + return choosers + + @inject.step() def mandatory_tour_scheduling(tours, persons_merged, @@ -46,6 +62,8 @@ def mandatory_tour_scheduling(tours, tracing.no_results(trace_label) return + persons_merged = filter_chooser_columns(persons_merged, model_settings) + model_constants = config.get_model_constants(model_settings) logger.info("Running mandatory_tour_scheduling with %d tours", len(tours)) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 270f9aeb6..829448451 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -130,7 +130,7 @@ def school_location_sample( if len(choices_list) > 0: choices = pd.concat(choices_list) - # - # NARROW + # - NARROW choices['school_type'] = choices['school_type'].astype(np.uint8) else: logger.info("Skipping %s: add_null_results" % model_name) diff --git a/activitysim/abm/models/trip_purpose_and_destination.py b/activitysim/abm/models/trip_purpose_and_destination.py index 9830bcf71..21cd58435 100644 --- a/activitysim/abm/models/trip_purpose_and_destination.py +++ b/activitysim/abm/models/trip_purpose_and_destination.py @@ -64,26 +64,32 @@ def trip_purpose_and_destination( model_settings = config.read_model_settings('trip_purpose_and_destination.yaml') MAX_ITERATIONS = model_settings.get('MAX_ITERATIONS', 5) - CLEANUP = model_settings.get('CLEANUP', True) trips_df = trips.to_frame() tours_merged_df = tours_merged.to_frame() + if trips_df.empty: + logger.info("%s - no trips. Nothing to do." % trace_label) + return + # FIXME could allow MAX_ITERATIONS=0 to allow for cleanup-only run # in which case, we would need to drop bad trips, WITHOUT failing bad_trip leg_mates assert (MAX_ITERATIONS > 0) # if trip_destination has been run before, keep only failed trips (and leg_mates) to retry - if 'failed' in trips_df: - logger.info('trip_destination has already been run. Rerunning failed trips') - flag_failed_trip_leg_mates(trips_df, 'failed') - trips_df = trips_df[trips_df.failed] - tours_merged_df = tours_merged_df[tours_merged_df.index.isin(trips_df.tour_id)] - logger.info('Rerunning %s failed trips and leg-mates' % trips_df.shape[0]) - - if trips_df.empty: - logger.info("%s - no trips. Nothing to do." % trace_label) - return + if 'destination' in trips_df: + if trips_df.failed.any(): + logger.info('trip_destination has already been run. Rerunning failed trips') + flag_failed_trip_leg_mates(trips_df, 'failed') + trips_df = trips_df[trips_df.failed] + tours_merged_df = tours_merged_df[tours_merged_df.index.isin(trips_df.tour_id)] + logger.info('Rerunning %s failed trips and leg-mates' % trips_df.shape[0]) + else: + # no failed trips from prior run of trip_destination + logger.info("%s - no failed trips from prior model run." % trace_label) + del trips_df['failed'] + pipeline.replace_table("trips", trips_df) + return results = [] i = 0 @@ -93,7 +99,8 @@ def trip_purpose_and_destination( i += 1 for c in RESULT_COLUMNS: - del trips_df[c] + if c in trips_df: + del trips_df[c] trips_df = run_trip_purpose_and_destination( trips_df, @@ -139,11 +146,7 @@ def trip_purpose_and_destination( trips_df = trips.to_frame() assign_in_place(trips_df, results) - if CLEANUP: - trips_df = cleanup_failed_trips(trips_df) - elif trips_df.failed.any(): - logger.warning("%s keeping %s sidelined failed trips" % - (trace_label, trips_df.failed.sum())) + trips_df = cleanup_failed_trips(trips_df) pipeline.replace_table("trips", trips_df) diff --git a/activitysim/abm/models/util/tour_destination.py b/activitysim/abm/models/util/tour_destination.py index c81c2b429..2c64ed62e 100644 --- a/activitysim/abm/models/util/tour_destination.py +++ b/activitysim/abm/models/util/tour_destination.py @@ -86,7 +86,7 @@ def tour_destination_size_terms(land_use, size_terms, selector): if not (df.dtypes == 'float64').all(): logger.warning('Surprised to find that not all size_terms were float64!') - # - #NARROW + # - NARROW # float16 has 3.3 decimal digits of precision, float32 has 7.2 df = df.astype(np.float16, errors='raise') diff --git a/activitysim/abm/models/util/trip.py b/activitysim/abm/models/util/trip.py index 18f25ff85..e1b9a9e36 100644 --- a/activitysim/abm/models/util/trip.py +++ b/activitysim/abm/models/util/trip.py @@ -54,7 +54,7 @@ def cleanup_failed_trips(trips): """ if trips.failed.any(): - logger.info("cleanup_failed_trips dropping %s sidelined failed trips" % trips.failed.sum()) + logger.warning("cleanup_failed_trips dropping %s failed trips" % trips.failed.sum()) trips['patch'] = False flag_failed_trip_leg_mates(trips, 'patch') diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index c21a94cd4..3d0e4bb85 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -10,7 +10,7 @@ from activitysim.core import tracing from activitysim.core import inject from activitysim.core import timetable as tt -from activitysim.core.mem import force_garbage_collect +from activitysim.core import mem from activitysim.core.util import reindex from activitysim.core import chunk @@ -60,7 +60,7 @@ def get_previous_tour_by_tourid(current_tour_window_ids, return previous_tour_by_tourid -def tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col): +def tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col, trace_label): """ interaction_sample_simulate expects alts index same as choosers (e.g. tour_id) @@ -90,17 +90,17 @@ def tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col tour_ids = np.repeat(tours.index, len(alts.index)) window_row_ids = np.repeat(tours[window_id_col], len(alts.index)) - alt_tdd = alts.take(alts_ids).copy() + alt_tdd = alts.take(alts_ids) + alt_tdd.index = tour_ids alt_tdd[window_id_col] = window_row_ids alt_tdd[choice_column] = alts_ids - # slice out all non-available tours - available = timetable.tour_available(alt_tdd[window_id_col], alt_tdd[choice_column]) - - assert available.any() - - alt_tdd = alt_tdd[available] + with mem.trace(trace_label, 'tour_available', logger): + # slice out all non-available tours + available = timetable.tour_available(alt_tdd[window_id_col], alt_tdd[choice_column]) + assert available.any() + alt_tdd = alt_tdd[available] # FIXME - don't need this any more after slicing del alt_tdd[window_id_col] @@ -160,9 +160,11 @@ def _schedule_tours( logger.info("%s schedule_tours running %d tour choices" % (tour_trace_label, len(tours))) # merge persons into tours - # avoid dual suffix for redundant columns names (e.g. household_id) that appear in both - tours = pd.merge(tours, persons_merged, left_on='person_id', right_index=True, - suffixes=('', '_y')) + with mem.trace(tour_trace_label, 'tours.merge', logger): + + # avoid dual suffix for redundant columns names (e.g. household_id) that appear in both + tours = pd.merge(tours, persons_merged, left_on='person_id', right_index=True, + suffixes=('', '_y')) # if no timetable window_id_col specified, then add index as an explicit column if window_id_col is None: @@ -181,8 +183,10 @@ def _schedule_tours( # build interaction dataset filtered to include only available tdd alts # dataframe columns start, end , duration, person_id, tdd # indexed (not unique) on tour_id - choice_column = 'tdd' - alt_tdd = tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col) + with mem.trace(tour_trace_label, 'alt_tdd', logger): + choice_column = 'tdd' + alt_tdd = tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col, + tour_trace_label) chunk.log_df(tour_trace_label, "alt_tdd", alt_tdd) locals_d = { @@ -227,10 +231,10 @@ def calc_rows_per_chunk(chunk_size, tours, persons_merged, alternatives, trace_ row_size = (chooser_row_size + extra_chooser_columns + alt_row_size) * sample_size - # logger.debug("%s #chunk_calc choosers %s" % (trace_label, tours.shape)) - # logger.debug("%s #chunk_calc extra_chooser_columns %s" % (trace_label, extra_chooser_columns)) - # logger.debug("%s #chunk_calc alternatives %s" % (trace_label, alternatives.shape)) - # logger.debug("%s #chunk_calc alt_row_size %s" % (trace_label, alt_row_size)) + logger.debug("%s #chunk_calc choosers %s" % (trace_label, tours.shape)) + logger.debug("%s #chunk_calc extra_chooser_columns %s" % (trace_label, extra_chooser_columns)) + logger.debug("%s #chunk_calc alternatives %s" % (trace_label, alternatives.shape)) + logger.debug("%s #chunk_calc alt_row_size %s" % (trace_label, alt_row_size)) return chunk.rows_per_chunk(chunk_size, row_size, num_choosers, trace_label) @@ -262,9 +266,6 @@ def schedule_tours( else: assert not tours[timetable_window_id_col].duplicated().any() - # persons_merged columns plus 2 previous tour columns - extra_chooser_columns = persons_merged.shape[1] + 2 - rows_per_chunk = \ calc_rows_per_chunk(chunk_size, tours, persons_merged, alts, trace_label=tour_trace_label) @@ -290,7 +291,7 @@ def schedule_tours( result_list.append(choices) - force_garbage_collect() + mem.force_garbage_collect() # FIXME: this will require 2X RAM # if necessary, could append to hdf5 store on disk: diff --git a/activitysim/abm/tables/time_windows.py b/activitysim/abm/tables/time_windows.py index f0be5c08a..2d1a03e97 100644 --- a/activitysim/abm/tables/time_windows.py +++ b/activitysim/abm/tables/time_windows.py @@ -7,6 +7,7 @@ import logging +import numpy as np import pandas as pd from activitysim.core import inject @@ -24,6 +25,9 @@ def tdd_alts(): df['duration'] = df.end - df.start + # - NARROW + df = df.astype(np.int8) + return df diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index f6221f6a2..d05722da2 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -27,8 +27,12 @@ def GB(bytes): - gb = (bytes / (1024 * 1024 * 1024.0)) - return "%s (%s GB)" % (bytes, round(gb, 2), ) + if bytes < (1024 * 1024 * 1024.0): + mb = (bytes / (1024 * 1024.0)) + return "(%s MB)" % round(mb, 2) + else: + gb = (bytes / (1024 * 1024 * 1024.0)) + return "(%s GB)" % round(gb, 2) def log_open(trace_label, chunk_size): @@ -65,31 +69,33 @@ def log_df(trace_label, table_name, df): cur_chunker = next(reversed(CHUNK_LOG)) if df is None: - elements, bytes = list(-n for n in CHUNK_LOG.get(cur_chunker).pop(table_name)) + elements, bytes, shape = list(CHUNK_LOG.get(cur_chunker).pop(table_name)) + elements *= -1 + bytes *= -1 df_trace_label = "%s.del_df.%s" % (trace_label, table_name) else: + shape = df.shape + elements = np.prod(shape) + if isinstance(df, pd.Series): - elements = df.shape[0] bytes = df.memory_usage(index=True) elif isinstance(df, pd.DataFrame): - elements = df.shape[0] * df.shape[1] bytes = df.memory_usage(index=True).sum() elif isinstance(df, np.ndarray): - elements = np.prod(df.shape) bytes = df.nbytes else: logger.error("log_df %s unknown type: %s" % (table_name, type(df))) assert False - CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) + CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes, shape) df_trace_label = "%s.add_df.%s" % (trace_label, table_name) - logger.debug("df elements %s %s : %s " % (elements, GB(bytes), df_trace_label)) + logger.debug("%s df %s %s %s : %s " % (table_name, elements, shape, GB(bytes), df_trace_label)) total_elements, total_bytes = _log_totals() - logger.debug("%s total elements %s %s" % (total_elements, GB(total_bytes), df_trace_label)) + logger.debug("total elements %s %s : %s" % (total_elements, GB(total_bytes), df_trace_label)) mem.log_memory_info(df_trace_label) @@ -100,7 +106,7 @@ def _log_totals(): for label in CHUNK_LOG: tables = CHUNK_LOG[label] for table_name in tables: - elements, bytes = tables[table_name] + elements, bytes, shape = tables[table_name] total_elements += elements total_bytes += bytes @@ -124,8 +130,8 @@ def log_write(): for label in CHUNK_LOG: tables = CHUNK_LOG[label] for table_name in tables: - elements, bytes = tables[table_name] - logger.debug("%s table %s %s %s" % (label, table_name, elements, GB(bytes))) + elements, bytes, shape = tables[table_name] + logger.debug("%s table %s %s %s %s" % (label, table_name, shape, elements, GB(bytes))) total_elements += elements total_bytes += bytes diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 5e1c3e563..a43d9622a 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -318,7 +318,7 @@ def _interaction_sample( # don't need this after tracing del choices_df['rand'] - # - #NARROW + # - NARROW choices_df['prob'] = choices_df['prob'].astype(np.float32) assert (choices_df['pick_count'].max() < 4294967295) or (choices_df.empty) choices_df['pick_count'] = choices_df['pick_count'].astype(np.uint32) diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index bea989ce9..e2a0d422e 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -109,7 +109,7 @@ def _interaction_sample_simulate( # so we just need to left join alternatives with choosers assert alternatives.index.name == choosers.index.name - with mem.trace('interaction_df', logger): + with mem.trace(trace_label, 'interaction_df', logger): interaction_df = pd.merge( alternatives, choosers, left_index=True, right_index=True, @@ -133,7 +133,7 @@ def _interaction_sample_simulate( # column names of choosers match spec index values # utilities has utility value for element in the cross product of choosers and alternatives # interaction_utilities is a df with one utility column and one row per row in alternative - with mem.trace('interaction_utilities', logger): + with mem.trace(trace_label, 'interaction_utilities', logger): interaction_utilities, trace_eval_results \ = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) @@ -188,7 +188,7 @@ def _interaction_sample_simulate( chunk.log_df(trace_label, 'padded_utilities', padded_utilities) # convert to a dataframe with one row per chooser and one column per alternative - with mem.trace('utilities_df', logger): + with mem.trace(trace_label, 'utilities_df', logger): utilities_df = pd.DataFrame( padded_utilities, index=choosers.index) @@ -203,7 +203,7 @@ def _interaction_sample_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - with mem.trace('utils_to_probs', logger): + with mem.trace(trace_label, 'utils_to_probs', logger): probs = logit.utils_to_probs(utilities_df, allow_zero_probs=allow_zero_probs, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) @@ -224,7 +224,7 @@ def _interaction_sample_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - with mem.trace('logit.make_choices', logger): + with mem.trace(trace_label, 'logit.make_choices', logger): positions, rands = \ logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 6781554be..3b4132afd 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -228,7 +228,7 @@ def _interaction_simulate( # cross join choosers and alternatives (cartesian product) # for every chooser, there will be a row for each alternative # index values (non-unique) are from alternatives df - with mem.trace('interaction_df', logger): + with mem.trace(trace_label, 'interaction_df', logger): interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) chunk.log_df(trace_label, 'interaction_df', interaction_df) @@ -250,7 +250,7 @@ def _interaction_simulate( else: trace_rows = trace_ids = None - with mem.trace('interaction_utilities', logger): + with mem.trace(trace_label, 'interaction_utilities', logger): interaction_utilities, trace_eval_results \ = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) @@ -265,7 +265,7 @@ def _interaction_simulate( # reshape utilities (one utility column and one row per row in model_design) # to a dataframe with one row per chooser and one column per alternative - with mem.trace('utilities', logger): + with mem.trace(trace_label, 'utilities', logger): utilities = pd.DataFrame( interaction_utilities.values.reshape(len(choosers), sample_size), index=choosers.index) @@ -279,7 +279,7 @@ def _interaction_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - with mem.trace('probs', logger): + with mem.trace(trace_label, 'probs', logger): probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) @@ -290,7 +290,7 @@ def _interaction_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - with mem.trace('logit.make_choices', logger): + with mem.trace(trace_label, 'logit.make_choices', logger): positions, rands = \ logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'positions', positions) @@ -299,7 +299,7 @@ def _interaction_simulate( # need to get from an integer offset into the alternative sample to the alternative index # that is, we want the index value of the row that is offset by rows into the # tranche of this choosers alternatives created by cross join of alternatives and choosers - with mem.trace('choices', logger): + with mem.trace(trace_label, 'choices', logger): # offsets is the offset into model_design df of first row of chooser alternatives offsets = np.arange(len(positions)) * sample_size # resulting Int64Index has one element per chooser row and is in same order as choosers diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index d26fcaf6e..776a4936b 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -251,7 +251,8 @@ def interaction_dataset(choosers, alternatives, sample_size=None): else: sample = np.tile(alts_idx, numchoosers) - alts_sample = alternatives.take(sample).copy() + alts_sample = alternatives.take(sample) + alts_sample['chooser_idx'] = np.repeat(choosers.index.values, sample_size) logger.debug("interaction_dataset pre-merge choosers %s alternatives %s alts_sample %s" % diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 6697795ee..5fd068022 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -42,8 +42,8 @@ def _track_memory_info(trace_label): gc.collect() mi = psutil.Process().memory_info() - logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % - (GB(mi.rss), GB(mi.vms), trace_label)) + # logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % + # (GB(mi.rss), GB(mi.vms), trace_label)) cur_mem = mi.vms @@ -76,7 +76,7 @@ def log_mem_high_water_mark(): @contextlib.contextmanager -def trace(msg, callers_logger, level=logging.DEBUG): +def trace(trace_label, tag, callers_logger, level=logging.DEBUG): """ A context manager to log delta time and memory to execute a block @@ -92,13 +92,14 @@ def trace(msg, callers_logger, level=logging.DEBUG): callerframerecord = inspect.stack()[2] caller_name = inspect.getframeinfo(callerframerecord[0]).function - msg = "%s.%s" % (caller_name, msg) + trace_label = "%s.%s" % (trace_label, tag) + msg = "%s.%s" % (caller_name, tag) - prev_mem = _track_memory_info("%s.before" % msg) + prev_mem = _track_memory_info("%s.before" % trace_label) t = time.time() yield t = time.time() - t - post_mem = _track_memory_info("%s.after" % msg) + post_mem = _track_memory_info("%s.after" % trace_label) delta_mem = post_mem - prev_mem diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index a0c4d33e9..75a4bec6f 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -482,7 +482,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - with mem.trace('expression_values', logger): + with mem.trace(trace_label, 'expression_values', logger): expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) @@ -498,7 +498,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, # resulting in a dataframe with one row per chooser and one column per alternative # pandas.dot depends on column names of expression_values matching spec index values - with mem.trace('utilities', logger): + with mem.trace(trace_label, 'utilities', logger): utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "utilities", utilities) @@ -509,7 +509,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) - with mem.trace('probs', logger): + with mem.trace(trace_label, 'probs', logger): probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, "probs", probs) @@ -581,7 +581,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, tracing.trace_df(choosers, '%s.choosers' % trace_label) # column names of expression_values match spec index values - with mem.trace('expression_values', logger): + with mem.trace(trace_label, 'expression_values', logger): expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, "expression_values", expression_values) @@ -593,7 +593,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, _check_for_variability(expression_values, trace_label) # raw utilities of all the leaves - with mem.trace('raw_utilities', logger): + with mem.trace(trace_label, 'raw_utilities', logger): raw_utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "raw_utilities", raw_utilities) @@ -605,7 +605,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, chunk.log_df(trace_label, 'expression_values', None) # exponentiated utilities of leaves and nests - with mem.trace('nested_exp_utilities', logger): + with mem.trace(trace_label, 'nested_exp_utilities', logger): nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) @@ -617,7 +617,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, column_labels=['alternative', 'utility']) # probabilities of alternatives relative to siblings sharing the same nest - with mem.trace('nested_probabilities', logger): + with mem.trace(trace_label, 'nested_probabilities', logger): nested_probabilities = \ compute_nested_probabilities(nested_exp_utilities, nest_spec, trace_label=trace_label) chunk.log_df(trace_label, "nested_probabilities", nested_probabilities) @@ -630,7 +630,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, column_labels=['alternative', 'probability']) # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) - with mem.trace('base_probabilities', logger): + with mem.trace(trace_label, 'base_probabilities', logger): base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) chunk.log_df(trace_label, "base_probabilities", base_probabilities) @@ -837,7 +837,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - with mem.trace('expression_values', logger): + with mem.trace(trace_label, 'expression_values', logger): expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) @@ -848,7 +848,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): _check_for_variability(expression_values, trace_label) # utility values - with mem.trace('utilities', logger): + with mem.trace(trace_label, 'utilities', logger): utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "utilities", utilities,) @@ -857,7 +857,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row - with mem.trace('logsums', logger): + with mem.trace(trace_label, 'logsums', logger): logsums = np.log(np.exp(utilities.values).sum(axis=1)) logsums = pd.Series(logsums, index=choosers.index) chunk.log_df(trace_label, "logsums", logsums) @@ -893,7 +893,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): # - eval spec expressions # column names of expression_values match spec index values - with mem.trace('expression_values', logger): + with mem.trace(trace_label, 'expression_values', logger): expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) @@ -905,7 +905,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): _check_for_variability(expression_values, trace_label) # - raw utilities of all the leaves - with mem.trace('raw_utilities', logger): + with mem.trace(trace_label, 'raw_utilities', logger): raw_utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "raw_utilities", raw_utilities) @@ -917,7 +917,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): chunk.log_df(trace_label, 'expression_values', None) # - exponentiated utilities of leaves and nests - with mem.trace('nested_exp_utilities', logger): + with mem.trace(trace_label, 'nested_exp_utilities', logger): nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) @@ -925,7 +925,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): chunk.log_df(trace_label, 'raw_utilities', None) # - logsums - with mem.trace('logsums', logger): + with mem.trace(trace_label, 'logsums', logger): logsums = np.log(nested_exp_utilities.root) logsums = pd.Series(logsums, index=choosers.index) chunk.log_df(trace_label, "logsums", logsums) @@ -938,7 +938,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - with mem.trace('del nested_exp_utilities', logger): + with mem.trace(trace_label, 'del nested_exp_utilities', logger): del nested_exp_utilities # done with nested_exp_utilities return logsums diff --git a/example/configs/mandatory_tour_scheduling.yaml b/example/configs/mandatory_tour_scheduling.yaml new file mode 100644 index 000000000..0052c0a23 --- /dev/null +++ b/example/configs/mandatory_tour_scheduling.yaml @@ -0,0 +1,14 @@ + +CHOOSER_COLUMNS: + - ptype + - hhsize + - roundtrip_auto_time_to_work + - num_workers + - income_in_thousands + - work_and_school_and_worker + - work_and_school_and_student + - workplace_in_cbd + - home_is_rural + - mandatory_tour_frequency + - is_worker + - is_student diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 99c7dc853..18ff9f8cb 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,16 +1,18 @@ inherit_settings: True -households_sample_size: 100 -#households_sample_size: 123281 +#households_sample_size: 100 +households_sample_size: 273272 + # 160 GB #chunk_size: 10000000000 -chunk_size: 3000000000 +#chunk_size: 3000000000 +chunk_size: 1500000000 multiprocess: False -profile: False -strict: False +profile: True +strict: True check_for_variability: False diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 01f26c1c1..d32c4e0f2 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -33,6 +33,7 @@ def cleanup_output_files(): tracing.delete_output_files('csv', subdir='trace') tracing.delete_output_files('txt') tracing.delete_output_files('yaml') + tracing.delete_output_files('prof') def run(run_list, injectables=None): From 3cabce1d7f854f53ef4fe5d73dd654d25e626607 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 26 Oct 2018 16:15:19 -0400 Subject: [PATCH 035/122] timetable tdd_footprints as np array using smart indexing isntead of loc --- .../abm/models/atwork_subtour_scheduling.py | 2 +- activitysim/abm/models/initialize.py | 2 + .../abm/models/mandatory_scheduling.py | 21 +------ .../abm/models/non_mandatory_scheduling.py | 11 ++-- activitysim/abm/models/stop_frequency.py | 1 + activitysim/abm/models/tour_mode_choice.py | 2 - activitysim/abm/models/trip_destination.py | 4 +- activitysim/abm/models/trip_mode_choice.py | 2 +- activitysim/abm/models/util/expressions.py | 47 +++++++++++++++ activitysim/abm/models/util/mode.py | 30 +--------- .../models/util/vectorize_tour_scheduling.py | 54 +++++++++++------ activitysim/core/chunk.py | 38 +++++++----- .../core/interaction_sample_simulate.py | 31 ++++------ activitysim/core/interaction_simulate.py | 36 +++++------ activitysim/core/mem.py | 47 +-------------- activitysim/core/mp_tasks.py | 59 +++++++++++-------- activitysim/core/pipeline.py | 9 ++- activitysim/core/simulate.py | 55 +++++++---------- activitysim/core/timetable.py | 30 +++------- activitysim/core/tracing.py | 21 ------- example/configs/annotate_households.csv | 2 +- example/configs/annotate_households_cdap.csv | 6 +- example/configs/annotate_persons_jtp.csv | 2 +- .../non_mandatory_tour_scheduling.yaml | 9 +++ example/configs/trip_mode_choice.yaml | 6 -- example_mp/configs/settings.yaml | 4 +- example_mp/simulation.py | 1 - 27 files changed, 231 insertions(+), 301 deletions(-) diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index 7d2c7a6b2..48bd5a536 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -16,7 +16,7 @@ from activitysim.core import inject from activitysim.core import timetable as tt from .util.vectorize_tour_scheduling import vectorize_subtour_scheduling -from .util.mode import annotate_preprocessors +from .util.expressions import annotate_preprocessors from activitysim.core.util import assign_in_place diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index f04f0bcfc..9ee70b4e0 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -53,6 +53,8 @@ def annotate_tables(model_settings, trace_label): model_settings=annotate, trace_label=tracing.extend_trace_label(trace_label, 'annotate_%s' % tablename)) + # fixme - narrow? + # - write table to pipeline pipeline.replace_table(tablename, df) diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index c1923ccf9..1a514085b 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -15,6 +15,7 @@ from activitysim.core import timetable as tt from .util.vectorize_tour_scheduling import vectorize_tour_scheduling +from .util import expressions from activitysim.core.util import assign_in_place @@ -23,22 +24,6 @@ DUMP = False -def filter_chooser_columns(choosers, model_settings): - - chooser_columns = model_settings.get('CHOOSER_COLUMNS', []) - - missing_columns = [c for c in chooser_columns if c not in choosers] - if missing_columns: - logger.warning("filter_chooser_columns missing_columns %s" % missing_columns) - bug - - # ignore any columns not appearing in choosers df - chooser_columns = [c for c in chooser_columns if c in choosers] - - choosers = choosers[chooser_columns] - return choosers - - @inject.step() def mandatory_tour_scheduling(tours, persons_merged, @@ -54,7 +39,6 @@ def mandatory_tour_scheduling(tours, school_spec = simulate.read_model_spec(file_name='tour_scheduling_school.csv') tours = tours.to_frame() - persons_merged = persons_merged.to_frame() mandatory_tours = tours[tours.tour_category == 'mandatory'] # - if no mandatory_tours @@ -62,7 +46,8 @@ def mandatory_tour_scheduling(tours, tracing.no_results(trace_label) return - persons_merged = filter_chooser_columns(persons_merged, model_settings) + persons_merged = persons_merged.to_frame() + persons_merged = expressions.filter_chooser_columns(persons_merged, model_settings) model_constants = config.get_model_constants(model_settings) diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index 4e70dceab..d2655cecf 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -34,20 +34,21 @@ def non_mandatory_tour_scheduling(tours, """ trace_label = 'non_mandatory_tour_scheduling' - model_settinsg = config.read_model_settings('non_mandatory_tour_scheduling.yaml') + model_settings = config.read_model_settings('non_mandatory_tour_scheduling.yaml') model_spec = simulate.read_model_spec(file_name='tour_scheduling_nonmandatory.csv') tours = tours.to_frame() - persons_merged = persons_merged.to_frame() - non_mandatory_tours = tours[tours.tour_category == 'non_mandatory'] logger.info("Running non_mandatory_tour_scheduling with %d tours", len(tours)) - constants = config.get_model_constants(model_settinsg) + persons_merged = persons_merged.to_frame() + persons_merged = expressions.filter_chooser_columns(persons_merged, model_settings) + + constants = config.get_model_constants(model_settings) # - run preprocessor to annotate choosers - preprocessor_settings = model_settinsg.get('preprocessor', None) + preprocessor_settings = model_settings.get('preprocessor', None) if preprocessor_settings: locals_d = {} diff --git a/activitysim/abm/models/stop_frequency.py b/activitysim/abm/models/stop_frequency.py index f12458e45..f2be9834c 100644 --- a/activitysim/abm/models/stop_frequency.py +++ b/activitysim/abm/models/stop_frequency.py @@ -41,6 +41,7 @@ def process_trips(tours, stop_frequency_alts): # get the actual alternatives for each person - have to go back to the # stop_frequency_alts dataframe to get this - the stop_frequency choice # column has the index values for the chosen alternative + trips = stop_frequency_alts.loc[tours.stop_frequency] # assign tour ids to the index diff --git a/activitysim/abm/models/tour_mode_choice.py b/activitysim/abm/models/tour_mode_choice.py index 6f70bcfa3..f85d99e0b 100644 --- a/activitysim/abm/models/tour_mode_choice.py +++ b/activitysim/abm/models/tour_mode_choice.py @@ -17,9 +17,7 @@ from activitysim.core.util import assign_in_place from .util.mode import tour_mode_choice_spec - from .util.mode import run_tour_mode_choice_simulate -from .util.mode import annotate_preprocessors logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 5c8c84229..b3478e797 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -24,7 +24,6 @@ from .util import expressions -from .util.mode import annotate_preprocessors from activitysim.core import assign from .util.tour_destination import tour_destination_size_terms @@ -123,7 +122,7 @@ def compute_ood_logsums( locals_dict.update(od_skims) - annotate_preprocessors( + expressions.annotate_preprocessors( choosers, locals_dict, od_skims, logsum_settings, trace_label) @@ -293,7 +292,6 @@ def choose_trip_destination( logger.info("choose_trip_destination %s with %d trips", trace_label, trips.shape[0]) - # FIXME want timing? t0 = print_elapsed_time() # - trip_destination_sample diff --git a/activitysim/abm/models/trip_mode_choice.py b/activitysim/abm/models/trip_mode_choice.py index 35cc76841..2ec5528cf 100644 --- a/activitysim/abm/models/trip_mode_choice.py +++ b/activitysim/abm/models/trip_mode_choice.py @@ -18,7 +18,7 @@ from activitysim.core import pipeline from activitysim.core.mem import force_garbage_collect -from .util.mode import annotate_preprocessors +from .util.expressions import annotate_preprocessors from activitysim.core import assign diff --git a/activitysim/abm/models/util/expressions.py b/activitysim/abm/models/util/expressions.py index eaf76b2f3..8cfdad930 100644 --- a/activitysim/abm/models/util/expressions.py +++ b/activitysim/abm/models/util/expressions.py @@ -13,12 +13,16 @@ from activitysim.core import config from activitysim.core import assign from activitysim.core import inject +from activitysim.core import simulate from activitysim.core.util import other_than from activitysim.core.util import assign_in_place from activitysim.core import util +logger = logging.getLogger(__name__) + + def reindex_i(series1, series2, dtype=np.int8): """ version of reindex that replaces missing na values and converts to int @@ -179,3 +183,46 @@ def skim_time_period_label(time): return skim_time_periods['labels'][bin] return pd.cut(time, skim_time_periods['hours'], labels=skim_time_periods['labels']).astype(str) + + +def annotate_preprocessors( + tours_df, locals_dict, skims, + model_settings, trace_label): + + locals_d = {} + locals_d.update(locals_dict) + locals_d.update(skims) + + preprocessor_settings = model_settings.get('preprocessor', []) + if not isinstance(preprocessor_settings, list): + assert isinstance(preprocessor_settings, dict) + preprocessor_settings = [preprocessor_settings] + + simulate.set_skim_wrapper_targets(tours_df, skims) + + annotations = None + for model_settings in preprocessor_settings: + + results = compute_columns( + df=tours_df, + model_settings=model_settings, + locals_dict=locals_d, + trace_label=trace_label) + + assign_in_place(tours_df, results) + + +def filter_chooser_columns(choosers, model_settings, column_list='CHOOSER_COLUMNS'): + + chooser_columns = model_settings.get(column_list, []) + + missing_columns = [c for c in chooser_columns if c not in choosers] + if missing_columns: + logger.warning("filter_chooser_columns missing_columns %s" % missing_columns) + assert False + + # ignore any columns not appearing in choosers df + chooser_columns = [c for c in chooser_columns if c in choosers] + + choosers = choosers[chooser_columns] + return choosers diff --git a/activitysim/abm/models/util/mode.py b/activitysim/abm/models/util/mode.py index 236a49ca0..cf52b79f5 100644 --- a/activitysim/abm/models/util/mode.py +++ b/activitysim/abm/models/util/mode.py @@ -11,7 +11,6 @@ from activitysim.core import simulate from activitysim.core import config from activitysim.core.assign import evaluate_constants -from activitysim.core.util import assign_in_place from . import expressions @@ -65,7 +64,7 @@ def run_tour_mode_choice_simulate( choosers['in_period'] = expressions.skim_time_period_label(choosers[in_time]) choosers['out_period'] = expressions.skim_time_period_label(choosers[out_time]) - annotate_preprocessors( + expressions.annotate_preprocessors( choosers, locals_dict, skims, model_settings, trace_label) @@ -83,30 +82,3 @@ def run_tour_mode_choice_simulate( choices = choices.map(dict(list(zip(list(range(len(alts))), alts)))) return choices - - -def annotate_preprocessors( - tours_df, locals_dict, skims, - model_settings, trace_label): - - locals_d = {} - locals_d.update(locals_dict) - locals_d.update(skims) - - preprocessor_settings = model_settings.get('preprocessor', []) - if not isinstance(preprocessor_settings, list): - assert isinstance(preprocessor_settings, dict) - preprocessor_settings = [preprocessor_settings] - - simulate.set_skim_wrapper_targets(tours_df, skims) - - annotations = None - for model_settings in preprocessor_settings: - - results = expressions.compute_columns( - df=tours_df, - model_settings=model_settings, - locals_dict=locals_d, - trace_label=trace_label) - - assign_in_place(tours_df, results) diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index 3d0e4bb85..ffc904392 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -96,11 +96,10 @@ def tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col alt_tdd[window_id_col] = window_row_ids alt_tdd[choice_column] = alts_ids - with mem.trace(trace_label, 'tour_available', logger): - # slice out all non-available tours - available = timetable.tour_available(alt_tdd[window_id_col], alt_tdd[choice_column]) - assert available.any() - alt_tdd = alt_tdd[available] + # slice out all non-available tours + available = timetable.tour_available(alt_tdd[window_id_col], alt_tdd[choice_column]) + assert available.any() + alt_tdd = alt_tdd[available] # FIXME - don't need this any more after slicing del alt_tdd[window_id_col] @@ -160,11 +159,10 @@ def _schedule_tours( logger.info("%s schedule_tours running %d tour choices" % (tour_trace_label, len(tours))) # merge persons into tours - with mem.trace(tour_trace_label, 'tours.merge', logger): - - # avoid dual suffix for redundant columns names (e.g. household_id) that appear in both - tours = pd.merge(tours, persons_merged, left_on='person_id', right_index=True, - suffixes=('', '_y')) + # avoid dual suffix for redundant columns names (e.g. household_id) that appear in both + tours = pd.merge(tours, persons_merged, left_on='person_id', right_index=True, + suffixes=('', '_y')) + chunk.log_df(tour_trace_label, "tours", tours) # if no timetable window_id_col specified, then add index as an explicit column if window_id_col is None: @@ -183,10 +181,9 @@ def _schedule_tours( # build interaction dataset filtered to include only available tdd alts # dataframe columns start, end , duration, person_id, tdd # indexed (not unique) on tour_id - with mem.trace(tour_trace_label, 'alt_tdd', logger): - choice_column = 'tdd' - alt_tdd = tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col, - tour_trace_label) + choice_column = 'tdd' + alt_tdd = tdd_interaction_dataset(tours, alts, timetable, choice_column, window_id_col, + tour_trace_label) chunk.log_df(tour_trace_label, "alt_tdd", alt_tdd) locals_d = { @@ -400,8 +397,14 @@ def vectorize_tour_scheduling(tours, persons_merged, alts, spec, choices = pd.concat(choice_list) # add the start, end, and duration from tdd_alts - tdd = alts.loc[choices] - tdd.index = choices.index + # use np instead of (slower) loc[] since alts has rangeindex + tdd = pd.DataFrame(data=alts.values[choices.values], + columns=alts.columns, + index=choices.index) + + # tdd = alts.loc[choices] + # tdd.index = choices.index + # include the index of the choice in the tdd alts table tdd['tdd'] = choices @@ -500,8 +503,14 @@ def vectorize_subtour_scheduling(parent_tours, subtours, persons_merged, alts, s choices = pd.concat(choice_list) # add the start, end, and duration from tdd_alts - tdd = alts.loc[choices] - tdd.index = choices.index + # assert (alts.index == list(range(alts.shape[0]))).all() + tdd = pd.DataFrame(data=alts.values[choices.values], + columns=alts.columns, + index=choices.index) + + # tdd = alts.loc[choices] + # tdd.index = choices.index + # include the index of the choice in the tdd alts table tdd['tdd'] = choices @@ -623,7 +632,14 @@ def vectorize_joint_tour_scheduling( choices = pd.concat(choice_list) # add the start, end, and duration from tdd_alts - tdd = alts.loc[choices] + # assert (alts.index == list(range(alts.shape[0]))).all() + tdd = pd.DataFrame(data=alts.values[choices.values], + columns=alts.columns, + index=choices.index) + + # tdd = alts.loc[choices] + # tdd.index = choices.index + tdd.index = choices.index # include the index of the choice in the tdd alts table tdd['tdd'] = choices diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index d05722da2..2d8e030ff 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -18,8 +18,11 @@ logger = logging.getLogger(__name__) - +# dict of table_dicts keyed by trace_label +# table_dicts are dicts tuples of (elements, bytes, shape) keyed by table_name CHUNK_LOG = OrderedDict() + +# array of chunk_size active CHUNK_LOG CHUNK_SIZE = [] ELEMENTS_HWM = {} @@ -37,12 +40,10 @@ def GB(bytes): def log_open(trace_label, chunk_size): - # if trace_label is None: - # trace_label = "noname_%s" % len(CHUNK_LOG) - if len(CHUNK_LOG) > 0: assert chunk_size == 0 logger.debug("log_open nested chunker %s" % trace_label) + assert trace_label not in CHUNK_LOG CHUNK_LOG[trace_label] = OrderedDict() CHUNK_SIZE.append(chunk_size) @@ -50,19 +51,23 @@ def log_open(trace_label, chunk_size): def log_close(trace_label): - # if trace_label is None: - # trace_label = "noname_%s" % (len(CHUNK_LOG)-1) - assert CHUNK_LOG and next(reversed(CHUNK_LOG)) == trace_label logger.debug("log_close %s" % trace_label) - log_write() - # input("Return to continue: ") + + # if we are closing the root level + if len(CHUNK_LOG) == 1: + log_write() + # input("Return to continue: ") label, _ = CHUNK_LOG.popitem(last=True) assert label == trace_label CHUNK_SIZE.pop() + if len(CHUNK_LOG) == 0: + ELEMENTS_HWM.clear() + BYTES_HWM.clear() + def log_df(trace_label, table_name, df): @@ -94,13 +99,14 @@ def log_df(trace_label, table_name, df): logger.debug("%s df %s %s %s : %s " % (table_name, elements, shape, GB(bytes), df_trace_label)) - total_elements, total_bytes = _log_totals() + total_elements, total_bytes = _log_totals(table_name) logger.debug("total elements %s %s : %s" % (total_elements, GB(total_bytes), df_trace_label)) - mem.log_memory_info(df_trace_label) + cur_mem = mem.track_memory_info(trace_label) + logger.debug("current memory %s : %s" % (GB(cur_mem), df_trace_label)) -def _log_totals(): +def _log_totals(table_name): total_elements = 0 total_bytes = 0 for label in CHUNK_LOG: @@ -113,13 +119,15 @@ def _log_totals(): if total_elements > ELEMENTS_HWM.get('mark', 0): ELEMENTS_HWM['mark'] = total_elements ELEMENTS_HWM['chunker'] = CHUNK_LOG.keys() + ELEMENTS_HWM['table_name'] = table_name if total_bytes > BYTES_HWM.get('mark', 0): BYTES_HWM['mark'] = total_bytes BYTES_HWM['chunker'] = CHUNK_LOG.keys() + BYTES_HWM['table_name'] = table_name - if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: - logger.warning("total_elements (%s) > chunk_size (%s)" % (total_elements, CHUNK_SIZE[0])) + # if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: + # logger.warning("total_elements (%s) > chunk_size (%s)" % (total_elements, CHUNK_SIZE[0])) return total_elements, total_bytes @@ -137,8 +145,6 @@ def log_write(): logger.debug("%s total elements %s %s" % (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) - -def log_chunk_high_water_mark(): if 'mark' in ELEMENTS_HWM: logger.info("chunk high_water_mark total_elements: %s in %s" % (ELEMENTS_HWM['mark'], ELEMENTS_HWM['chunker']), ) diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index e2a0d422e..1943e4981 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -109,11 +109,10 @@ def _interaction_sample_simulate( # so we just need to left join alternatives with choosers assert alternatives.index.name == choosers.index.name - with mem.trace(trace_label, 'interaction_df', logger): - interaction_df = pd.merge( - alternatives, choosers, - left_index=True, right_index=True, - suffixes=('', '_r')) + interaction_df = pd.merge( + alternatives, choosers, + left_index=True, right_index=True, + suffixes=('', '_r')) chunk.log_df(trace_label, 'interaction_df', interaction_df) if have_trace_targets: @@ -133,9 +132,8 @@ def _interaction_sample_simulate( # column names of choosers match spec index values # utilities has utility value for element in the cross product of choosers and alternatives # interaction_utilities is a df with one utility column and one row per row in alternative - with mem.trace(trace_label, 'interaction_utilities', logger): - interaction_utilities, trace_eval_results \ - = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + interaction_utilities, trace_eval_results \ + = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) del interaction_df @@ -188,10 +186,9 @@ def _interaction_sample_simulate( chunk.log_df(trace_label, 'padded_utilities', padded_utilities) # convert to a dataframe with one row per chooser and one column per alternative - with mem.trace(trace_label, 'utilities_df', logger): - utilities_df = pd.DataFrame( - padded_utilities, - index=choosers.index) + utilities_df = pd.DataFrame( + padded_utilities, + index=choosers.index) chunk.log_df(trace_label, 'utilities_df', utilities_df) del padded_utilities @@ -203,9 +200,8 @@ def _interaction_sample_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - with mem.trace(trace_label, 'utils_to_probs', logger): - probs = logit.utils_to_probs(utilities_df, allow_zero_probs=allow_zero_probs, - trace_label=trace_label, trace_choosers=choosers) + probs = logit.utils_to_probs(utilities_df, allow_zero_probs=allow_zero_probs, + trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) del utilities_df @@ -224,9 +220,8 @@ def _interaction_sample_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - with mem.trace(trace_label, 'logit.make_choices', logger): - positions, rands = \ - logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + positions, rands = \ + logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'positions', positions) chunk.log_df(trace_label, 'rands', rands) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 3b4132afd..ae52deb7c 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -228,8 +228,7 @@ def _interaction_simulate( # cross join choosers and alternatives (cartesian product) # for every chooser, there will be a row for each alternative # index values (non-unique) are from alternatives df - with mem.trace(trace_label, 'interaction_df', logger): - interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) + interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) chunk.log_df(trace_label, 'interaction_df', interaction_df) if skims is not None: @@ -250,9 +249,8 @@ def _interaction_simulate( else: trace_rows = trace_ids = None - with mem.trace(trace_label, 'interaction_utilities', logger): - interaction_utilities, trace_eval_results \ - = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) + interaction_utilities, trace_eval_results \ + = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) if have_trace_targets: @@ -265,10 +263,9 @@ def _interaction_simulate( # reshape utilities (one utility column and one row per row in model_design) # to a dataframe with one row per chooser and one column per alternative - with mem.trace(trace_label, 'utilities', logger): - utilities = pd.DataFrame( - interaction_utilities.values.reshape(len(choosers), sample_size), - index=choosers.index) + utilities = pd.DataFrame( + interaction_utilities.values.reshape(len(choosers), sample_size), + index=choosers.index) chunk.log_df(trace_label, 'utilities', utilities) if have_trace_targets: @@ -279,8 +276,7 @@ def _interaction_simulate( # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative - with mem.trace(trace_label, 'probs', logger): - probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) if have_trace_targets: @@ -290,22 +286,20 @@ def _interaction_simulate( # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample - with mem.trace(trace_label, 'logit.make_choices', logger): - positions, rands = \ - logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) + positions, rands = \ + logit.make_choices(probs, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'positions', positions) chunk.log_df(trace_label, 'rands', rands) # need to get from an integer offset into the alternative sample to the alternative index # that is, we want the index value of the row that is offset by rows into the # tranche of this choosers alternatives created by cross join of alternatives and choosers - with mem.trace(trace_label, 'choices', logger): - # offsets is the offset into model_design df of first row of chooser alternatives - offsets = np.arange(len(positions)) * sample_size - # resulting Int64Index has one element per chooser row and is in same order as choosers - choices = interaction_utilities.index.take(positions + offsets) - # create a series with index from choosers and the index of the chosen alternative - choices = pd.Series(choices, index=choosers.index) + # offsets is the offset into model_design df of first row of chooser alternatives + offsets = np.arange(len(positions)) * sample_size + # resulting Int64Index has one element per chooser row and is in same order as choosers + choices = interaction_utilities.index.take(positions + offsets) + # create a series with index from choosers and the index of the chosen alternative + choices = pd.Series(choices, index=choosers.index) chunk.log_df(trace_label, 'choices', choices) if have_trace_targets: diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 5fd068022..0f8e3a4a5 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -10,9 +10,6 @@ import psutil import logging -import contextlib -import time -import inspect import gc logger = logging.getLogger(__name__) @@ -38,9 +35,8 @@ def set_pause_threshold(gb): MEM['pause'] = bytes -def _track_memory_info(trace_label): +def track_memory_info(trace_label): - gc.collect() mi = psutil.Process().memory_info() # logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % # (GB(mi.rss), GB(mi.vms), trace_label)) @@ -60,48 +56,7 @@ def _track_memory_info(trace_label): return cur_mem -def log_memory_info(trace_label): - - _track_memory_info(trace_label) - - mi = psutil.Process().memory_info() - logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % - (GB(mi.rss), GB(mi.vms), trace_label)) - - def log_mem_high_water_mark(): if 'high_water_mark' in MEM: logger.info("mem high_water_mark %s in %s" % (GB(MEM['high_water_mark']), MEM['high_water_mark_trace_label']), ) - - -@contextlib.contextmanager -def trace(trace_label, tag, callers_logger, level=logging.DEBUG): - """ - A context manager to log delta time and memory to execute a block - - Parameters - ---------- - msg : str - callers_logger : logging.Logger - logger passed from caller's context - level : int, optional - Level at which to log, passed to ``logger.log``. - - """ - callerframerecord = inspect.stack()[2] - caller_name = inspect.getframeinfo(callerframerecord[0]).function - - trace_label = "%s.%s" % (trace_label, tag) - msg = "%s.%s" % (caller_name, tag) - - prev_mem = _track_memory_info("%s.before" % trace_label) - t = time.time() - yield - t = time.time() - t - post_mem = _track_memory_info("%s.after" % trace_label) - - delta_mem = post_mem - prev_mem - - callers_logger.log(level, "Time to perform %s : %s memory: %s (%s)" % - (msg, format_elapsed_time(t), GB(post_mem), GB(delta_mem))) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 704cf56e4..1f5c586d2 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -25,8 +25,10 @@ from activitysim.core import pipeline from activitysim.core import config +from activitysim.core import chunk +from activitysim.core import mem + from activitysim.core.config import setting -from activitysim.core.config import handle_standard_args # activitysim.abm imported for its side-effects (dependency injection) from activitysim import abm @@ -331,9 +333,9 @@ def setup_injectables_and_logging(injectables): def run_simulation(queue, step_info, resume_after, skim_buffer): - step_label = step_info['name'] models = step_info['models'] chunk_size = step_info['chunk_size'] + step_label = step_info['name'] inject.add_injectable('skim_buffer', skim_buffer) inject.add_injectable("chunk_size", chunk_size) @@ -369,6 +371,8 @@ def run_simulation(queue, step_info, resume_after, skim_buffer): t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) + mem.log_mem_high_water_mark() + pipeline.close_pipeline() @@ -474,6 +478,7 @@ def idle(seconds): step_name = step_info['name'] + t0 = tracing.print_elapsed_time() logger.info('run_sub_simulations step %s models resume_after %s', step_name, resume_after) if previously_completed: @@ -530,13 +535,18 @@ def idle(seconds): logger.error("Process %s completed with exitcode %s", p.name, p.exitcode) assert p.name in completed + t0 = tracing.print_elapsed_time('run_sub_simulations step %s' % step_name) + return list(completed) def run_sub_task(p): logger.info("running sub_process %s", p.name) + + t0 = tracing.print_elapsed_time() p.start() p.join() + t0 = tracing.print_elapsed_time('sub_process %s' % p.name) # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) if p.exitcode: @@ -568,16 +578,17 @@ def skip_phase(phase): def find_breadcrumb(crumb, default=None): return old_breadcrumbs.get(step_name, {}).get(crumb, default) - with tracing.timing('allocate shared skim buffer', logger): - shared_skim_buffer = allocate_shared_skim_buffer() + t0 = tracing.print_elapsed_time() + shared_skim_buffer = allocate_shared_skim_buffer() + t0 = tracing.print_elapsed_time('allocate shared skim buffer', t0) # - mp_setup_skims - with tracing.timing('setup skims', logger): - run_sub_task( - multiprocessing.Process( - target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), - kwargs=shared_skim_buffer) - ) + run_sub_task( + multiprocessing.Process( + target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), + kwargs=shared_skim_buffer) + ) + t0 = tracing.print_elapsed_time('setup skims', t0) for step_info in run_list['multiprocess_steps']: @@ -593,12 +604,11 @@ def find_breadcrumb(crumb, default=None): # - mp_apportion_pipeline if not skip_phase('apportion') and num_processes > 1: - with tracing.timing('apportion %s pipelines' % step_name, logger): - run_sub_task( - multiprocessing.Process( - target=mp_apportion_pipeline, name='%s_apportion' % step_name, - args=(injectables, sub_proc_names, slice_info)) - ) + run_sub_task( + multiprocessing.Process( + target=mp_apportion_pipeline, name='%s_apportion' % step_name, + args=(injectables, sub_proc_names, slice_info)) + ) drob_breadcrumb(step_name, 'apportion') # - run_sub_simulations @@ -607,10 +617,8 @@ def find_breadcrumb(crumb, default=None): completed = find_breadcrumb('completed', default=[]) if resume_after == '_' else [] - with tracing.timing('run step %s' % step_name, logger): - completed = \ - run_sub_simulations(injectables, shared_skim_buffer, step_info, sub_proc_names, - resume_after, completed) + completed = run_sub_simulations(injectables, shared_skim_buffer, step_info, + sub_proc_names, resume_after, completed) if len(completed) != num_processes: raise RuntimeError("%s processes failed in step %s" % @@ -619,12 +627,11 @@ def find_breadcrumb(crumb, default=None): # - mp_coalesce_pipelines if not skip_phase('coalesce') and num_processes > 1: - with tracing.timing('coalesce %s pipelines' % step_name, logger): - run_sub_task( - multiprocessing.Process( - target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, - args=(injectables, sub_proc_names, slice_info)) - ) + run_sub_task( + multiprocessing.Process( + target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, + args=(injectables, sub_proc_names, slice_info)) + ) drob_breadcrumb(step_name, 'coalesce') diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index d268b261b..aa6becd04 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -455,15 +455,16 @@ def run_model(model_name): inject.set_step_args(args) + t0 = print_elapsed_time() orca.run([step_name]) + t0 = print_elapsed_time("run_model step '%s'" % model_name, t0, debug=True) inject.set_step_args(None) _PIPELINE.rng().end_step(model_name) if checkpoint: - t0 = print_elapsed_time() add_checkpoint(model_name) - t0 = print_elapsed_time("add_checkpoint '%s'" % model_name, t0, debug=True) + t0 = print_elapsed_time("run_model add_checkpoint '%s'" % model_name, t0, debug=True) else: logger.info("##### skipping %s checkpoint for %s" % (step_name, model_name)) @@ -580,11 +581,9 @@ def run(models, resume_after=None): t0 = print_elapsed_time() for model in models: - t1 = print_elapsed_time() run_model(model) - t1 = print_elapsed_time("run_model %s" % model, t1) - t0 = print_elapsed_time("run (%s models)" % len(models), t0) + t0 = print_elapsed_time("run_model (%s models)" % len(models), t0) # don't close the pipeline, as the user may want to read intermediate results from the store diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 75a4bec6f..dcc1cd016 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -482,8 +482,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - with mem.trace(trace_label, 'expression_values', logger): - expression_values = eval_variables(spec.index, choosers, locals_d) + expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) if have_trace_targets: @@ -498,8 +497,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, # resulting in a dataframe with one row per chooser and one column per alternative # pandas.dot depends on column names of expression_values matching spec index values - with mem.trace(trace_label, 'utilities', logger): - utilities = compute_utilities(expression_values, spec) + utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "utilities", utilities) del expression_values @@ -509,8 +507,7 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) - with mem.trace(trace_label, 'probs', logger): - probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) + probs = logit.utils_to_probs(utilities, trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, "probs", probs) del utilities @@ -581,8 +578,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, tracing.trace_df(choosers, '%s.choosers' % trace_label) # column names of expression_values match spec index values - with mem.trace(trace_label, 'expression_values', logger): - expression_values = eval_variables(spec.index, choosers, locals_d) + expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, "expression_values", expression_values) if have_trace_targets: @@ -593,8 +589,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, _check_for_variability(expression_values, trace_label) # raw utilities of all the leaves - with mem.trace(trace_label, 'raw_utilities', logger): - raw_utilities = compute_utilities(expression_values, spec) + raw_utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "raw_utilities", raw_utilities) if have_trace_targets: @@ -605,8 +600,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, chunk.log_df(trace_label, 'expression_values', None) # exponentiated utilities of leaves and nests - with mem.trace(trace_label, 'nested_exp_utilities', logger): - nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) del raw_utilities @@ -617,9 +611,8 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, column_labels=['alternative', 'utility']) # probabilities of alternatives relative to siblings sharing the same nest - with mem.trace(trace_label, 'nested_probabilities', logger): - nested_probabilities = \ - compute_nested_probabilities(nested_exp_utilities, nest_spec, trace_label=trace_label) + nested_probabilities = \ + compute_nested_probabilities(nested_exp_utilities, nest_spec, trace_label=trace_label) chunk.log_df(trace_label, "nested_probabilities", nested_probabilities) del nested_exp_utilities @@ -630,8 +623,7 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, column_labels=['alternative', 'probability']) # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) - with mem.trace(trace_label, 'base_probabilities', logger): - base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) + base_probabilities = compute_base_probabilities(nested_probabilities, nest_spec, spec) chunk.log_df(trace_label, "base_probabilities", base_probabilities) del nested_probabilities @@ -837,8 +829,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - with mem.trace(trace_label, 'expression_values', logger): - expression_values = eval_variables(spec.index, choosers, locals_d) + expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) if have_trace_targets: @@ -848,8 +839,7 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): _check_for_variability(expression_values, trace_label) # utility values - with mem.trace(trace_label, 'utilities', logger): - utilities = compute_utilities(expression_values, spec) + utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "utilities", utilities,) del expression_values # done with expression_values @@ -857,9 +847,8 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row - with mem.trace(trace_label, 'logsums', logger): - logsums = np.log(np.exp(utilities.values).sum(axis=1)) - logsums = pd.Series(logsums, index=choosers.index) + logsums = np.log(np.exp(utilities.values).sum(axis=1)) + logsums = pd.Series(logsums, index=choosers.index) chunk.log_df(trace_label, "logsums", logsums) # trace utilities @@ -893,8 +882,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): # - eval spec expressions # column names of expression_values match spec index values - with mem.trace(trace_label, 'expression_values', logger): - expression_values = eval_variables(spec.index, choosers, locals_d) + expression_values = eval_variables(spec.index, choosers, locals_d) chunk.log_df(trace_label, 'expression_values', expression_values) if have_trace_targets: @@ -905,8 +893,7 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): _check_for_variability(expression_values, trace_label) # - raw utilities of all the leaves - with mem.trace(trace_label, 'raw_utilities', logger): - raw_utilities = compute_utilities(expression_values, spec) + raw_utilities = compute_utilities(expression_values, spec) chunk.log_df(trace_label, "raw_utilities", raw_utilities) if have_trace_targets: @@ -917,17 +904,15 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): chunk.log_df(trace_label, 'expression_values', None) # - exponentiated utilities of leaves and nests - with mem.trace(trace_label, 'nested_exp_utilities', logger): - nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) del raw_utilities # done with raw_utilities chunk.log_df(trace_label, 'raw_utilities', None) # - logsums - with mem.trace(trace_label, 'logsums', logger): - logsums = np.log(nested_exp_utilities.root) - logsums = pd.Series(logsums, index=choosers.index) + logsums = np.log(nested_exp_utilities.root) + logsums = pd.Series(logsums, index=choosers.index) chunk.log_df(trace_label, "logsums", logsums) if have_trace_targets: @@ -938,8 +923,8 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) - with mem.trace(trace_label, 'del nested_exp_utilities', logger): - del nested_exp_utilities # done with nested_exp_utilities + del nested_exp_utilities # done with nested_exp_utilities + chunk.log_df(trace_label, 'nested_exp_utilities', None) return logsums diff --git a/activitysim/core/timetable.py b/activitysim/core/timetable.py index f9585ba2c..90a004ee8 100644 --- a/activitysim/core/timetable.py +++ b/activitysim/core/timetable.py @@ -213,9 +213,10 @@ def __init__(self, windows_df, tdd_alts_df, table_name=None): (C_END if row.duration > 0 else C_START_END) + (C_EMPTY * (max_period - row.end)) for idx, row in tdd_alts_df.iterrows()] - footprints = np.asanyarray([list(r) for r in w_strings]).astype(int) - self.tdd_footprints_df = pd.DataFrame(data=footprints, index=tdd_alts_df.index) - # print "\tdd_footprints_df\n", self.tdd_footprints_df + + # we want range index so we can use raw numpy + assert (tdd_alts_df.index == list(range(tdd_alts_df.shape[0]))).all() + self.tdd_footprints = np.asanyarray([list(r) for r in w_strings]).astype(int) def slice_windows_by_row_id(self, window_row_ids): """ @@ -283,13 +284,8 @@ def tour_available(self, window_row_ids, tdds): assert len(window_row_ids) == len(tdds) - # t0 = tracing.print_elapsed_time() - # numpy array with one tdd_footprints_df row for tdds - tour_footprints = util.quick_loc_df(tdds, self.tdd_footprints_df).values - - # t0 = tracing.print_elapsed_time("tour_footprints", t0, debug=True) - # assert (tour_footprints == self.tdd_footprints_df.loc[tdds].values).all + tour_footprints = self.tdd_footprints[tdds.values.astype(int)] # numpy array with one windows row for each person windows = self.slice_windows_by_row_id(window_row_ids) @@ -301,8 +297,6 @@ def tour_available(self, window_row_ids, tdds): available = ~np.isin(x, COLLISION_LIST).any(axis=1) available = pd.Series(available, index=window_row_ids.index) - # t0 = tracing.print_elapsed_time("available", t0, debug=True) - return available def assign(self, window_row_ids, tdds): @@ -325,11 +319,8 @@ def assign(self, window_row_ids, tdds): # vectorization doesn't work duplicates assert len(window_row_ids.index) == len(np.unique(window_row_ids.values)) - # df with one tdd_footprint row for each person tdd - tour_footprints = self.tdd_footprints_df.loc[tdds] - - # numpy array with one time window row for each row in df - tour_footprints = tour_footprints.values + # numpy array with one time window row for each person tdd + tour_footprints = self.tdd_footprints[tdds.values.astype(int)] # row idxs of windows to assign to row_ixs = window_row_ids.map(self.window_row_ix).values @@ -365,11 +356,8 @@ def assign_subtour_mask(self, window_row_ids, tdds): self.windows.fill(0) self.assign(window_row_ids, tdds) - # df with one tdd_footprint row for each person tdd - tour_footprints = self.tdd_footprints_df.loc[tdds] - - # numpy array with one time window row for each row in df - tour_footprints = tour_footprints.values + # numpy array with one time window row for each person tdd + tour_footprints = self.tdd_footprints[tdds.values.astype(int)] # row idxs of windows to assign to row_ixs = window_row_ids.map(self.window_row_ix).values diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 962372070..82318238b 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -61,27 +61,6 @@ def print_elapsed_time(msg=None, t0=None, debug=False): return t1 -@contextlib.contextmanager -def timing(msg, callers_logger, level=logging.DEBUG): - """ - A context manager to log time to execute a block - - Parameters - ---------- - msg : str - callers_logger : logging.Logger - logger passed from caller's context - level : int, optional - Level at which to log, passed to ``logger.log``. - - """ - callers_logger.log(level, msg) - t = time.time() - yield - t = time.time() - t - callers_logger.log(level, "Time to execute %s : %s" % (msg, format_elapsed_time(t))) - - def delete_output_files(file_type, ignore=None, subdir=None): """ Delete files in output directory of specified type diff --git a/example/configs/annotate_households.csv b/example/configs/annotate_households.csv index 6d64cb5e8..7b47137c0 100644 --- a/example/configs/annotate_households.csv +++ b/example/configs/annotate_households.csv @@ -1,6 +1,6 @@ Description,Target,Expression #,, annotate households table after import -,_PERSON_COUNT,"lambda query, persons, households: persons.query(query).groupby('household_id').size().reindex(households.index).fillna(0)" +,_PERSON_COUNT,"lambda query, persons, households: persons.query(query).groupby('household_id').size().reindex(households.index).fillna(0).astype(np.int8)" #,,FIXME households.income can be negative, so we clip? income_in_thousands,income_in_thousands,(households.income / 1000).clip(lower=0) income_segment,income_segment,"pd.cut(income_in_thousands, bins=[-np.inf, 30, 60, 100, np.inf], labels=[1, 2, 3, 4]).astype(int)" diff --git a/example/configs/annotate_households_cdap.csv b/example/configs/annotate_households_cdap.csv index 8df638aa0..a025a60e9 100644 --- a/example/configs/annotate_households_cdap.csv +++ b/example/configs/annotate_households_cdap.csv @@ -1,6 +1,6 @@ Description,Target,Expression #,, annotate households table after cdap model has run -num_under16_not_at_school,num_under16_not_at_school,"persons.under16_not_at_school.astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0)" -num_travel_active,num_travel_active,"persons.travel_active.astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0)" -num_travel_active_adults,num_travel_active_adults,"(persons.adult & persons.travel_active).astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0)" +num_under16_not_at_school,num_under16_not_at_school,"persons.under16_not_at_school.astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0).astype(np.int8)" +num_travel_active,num_travel_active,"persons.travel_active.astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0).astype(np.int8)" +num_travel_active_adults,num_travel_active_adults,"(persons.adult & persons.travel_active).astype(int).groupby(persons.household_id).sum().reindex(households.index).fillna(0).astype(np.int8)" num_travel_active_children,num_travel_active_children,"num_travel_active - num_travel_active_adults" diff --git a/example/configs/annotate_persons_jtp.csv b/example/configs/annotate_persons_jtp.csv index 460eba9b5..a72c86605 100644 --- a/example/configs/annotate_persons_jtp.csv +++ b/example/configs/annotate_persons_jtp.csv @@ -1,3 +1,3 @@ Description,Target,Expression #,, annotate persons table after joint_tour_participation model has run -num_joint_tours,num_joint_tours,"joint_tour_participants.groupby('person_id').size().reindex(persons.index).fillna(0)" +num_joint_tours,num_joint_tours,"joint_tour_participants.groupby('person_id').size().reindex(persons.index).fillna(0).astype(np.int8)" diff --git a/example/configs/non_mandatory_tour_scheduling.yaml b/example/configs/non_mandatory_tour_scheduling.yaml index 3aabb44d7..20b04fb0e 100644 --- a/example/configs/non_mandatory_tour_scheduling.yaml +++ b/example/configs/non_mandatory_tour_scheduling.yaml @@ -6,3 +6,12 @@ preprocessor: TABLES: - land_use - joint_tour_participants + +CHOOSER_COLUMNS: + - ptype + - num_children + - roundtrip_auto_time_to_work + - num_mand + - num_escort_tours + - num_non_escort_tours + - adult diff --git a/example/configs/trip_mode_choice.yaml b/example/configs/trip_mode_choice.yaml index 06c8ff10f..f1da48e36 100644 --- a/example/configs/trip_mode_choice.yaml +++ b/example/configs/trip_mode_choice.yaml @@ -112,15 +112,9 @@ preprocessor: # TABLES: # - land_use - - TOURS_MERGED_CHOOSER_COLUMNS: -# - tour_type - hhsize -# - density_index - age -# - age_16_p -# - age_16_to_19 - auto_ownership - number_of_participants - tour_category diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 18ff9f8cb..3f5d1b79a 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,8 +1,8 @@ inherit_settings: True -#households_sample_size: 100 -households_sample_size: 273272 +households_sample_size: 20000 +#households_sample_size: 0 # 160 GB diff --git a/example_mp/simulation.py b/example_mp/simulation.py index d32c4e0f2..261444be6 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -82,5 +82,4 @@ def run(run_list, injectables=None): t0 = tracing.print_elapsed_time("everything", t0) - chunk.log_chunk_high_water_mark() mem.log_mem_high_water_mark() From 32f18150db7f6f3b0a2c5d3715044363980b6818 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 26 Oct 2018 16:42:56 -0400 Subject: [PATCH 036/122] settings cleanup --- example/configs/settings.yaml | 18 ++++++----- example_mp/configs/settings.yaml | 51 ++++++++++++++------------------ 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index c0e53b68e..d7be91c1e 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -3,22 +3,24 @@ input_store: mtc_asim.h5 skims_file: skims.omx #number of households to simulate -households_sample_size: 100 +households_sample_size: 100 +# simulate all households +#households_sample_size: 0 -#trace household id; comment out for no trace -# household with all tour categories -trace_hh_id: 1482966 +#trace household id; comment out or None for no trace +#trace_hh_id: 1482966 +trace_hh_id: None -# trace origin, destination in accessibility calculation +# trace origin, destination in accessibility calculation; comment out or None for no trace #trace_od: [5, 11] +trace_od: None -#internal settings -chunk_size: 400000000 - +chunk_size: 0 # comment out or set false to disable variability check in simple_simulate and interaction_simulate check_for_variability: False + models: - initialize_landuse - compute_accessibility diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 3f5d1b79a..2bdd728d9 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,27 +1,22 @@ inherit_settings: True -households_sample_size: 20000 #households_sample_size: 0 +#multiprocess: False +#profile: False +#strict: False +#chunk_size: 0 +#check_for_variability: False +#trace_hh_id: None +#trace_od: None - -# 160 GB -#chunk_size: 10000000000 -#chunk_size: 3000000000 +households_sample_size: 20000 chunk_size: 1500000000 - multiprocess: False profile: True strict: True -check_for_variability: False - -#trace household id; comment out for no trace -# household with all tour categories -trace_hh_id: 1269102 -# trace origin, destination in accessibility calculation -trace_od: [5, 11] models: - initialize_landuse @@ -47,21 +42,21 @@ models: - non_mandatory_tour_frequency - non_mandatory_tour_destination - non_mandatory_tour_scheduling - - tour_mode_choice_simulate - - atwork_subtour_frequency - - _atwork_subtour_destination_sample - - _atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate - - atwork_subtour_scheduling - - atwork_subtour_mode_choice - - stop_frequency - - trip_purpose - - trip_destination - - trip_purpose_and_destination - - trip_scheduling - - trip_mode_choice - - write_data_dictionary - - write_tables +# - tour_mode_choice_simulate +# - atwork_subtour_frequency +# - _atwork_subtour_destination_sample +# - _atwork_subtour_destination_logsums +# - atwork_subtour_destination_simulate +# - atwork_subtour_scheduling +# - atwork_subtour_mode_choice +# - stop_frequency +# - trip_purpose +# - trip_destination +# - trip_purpose_and_destination +# - trip_scheduling +# - trip_mode_choice +# - write_data_dictionary +# - write_tables multiprocess_steps: From a63fd13c20658d77c9fe80ebee89b6be3190e2b2 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 30 Oct 2018 22:50:38 -0400 Subject: [PATCH 037/122] performance enhancements in simulate.eval_utilities and SkimWrapper.get --- activitysim/abm/models/util/cdap.py | 25 +--- activitysim/abm/models/util/test/test_cdap.py | 9 +- activitysim/abm/tables/skims.py | 5 +- activitysim/core/chunk.py | 124 +++++++++------- activitysim/core/interaction_sample.py | 16 ++- activitysim/core/interaction_simulate.py | 14 ++ activitysim/core/logit.py | 2 +- activitysim/core/mem.py | 38 +---- activitysim/core/mp_tasks.py | 18 ++- activitysim/core/simulate.py | 132 +++++++++--------- activitysim/core/skim.py | 49 ++++--- activitysim/core/test/test_skim.py | 23 +-- example/configs/trip_destination_sample.csv | 32 ++++- example_mp/configs/settings.yaml | 58 +++++--- example_mp/simulation.py | 3 +- 15 files changed, 307 insertions(+), 241 deletions(-) diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 1d4e865dd..14c599e44 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -12,9 +12,7 @@ import pandas as pd from zbox import toolz as tz, gen -from activitysim.core.simulate import eval_variables -from activitysim.core.simulate import compute_utilities -from activitysim.core.simulate import uniquify_spec_index +from activitysim.core import simulate from activitysim.core import chunk from activitysim.core import logit @@ -185,20 +183,13 @@ def individual_utilities( """ # calculate single person utilities - individual_vars = eval_variables(cdap_indiv_spec.index, persons, locals_d) - indiv_utils = compute_utilities(individual_vars, cdap_indiv_spec) + indiv_utils = simulate.eval_utilities(cdap_indiv_spec, persons, locals_d, trace_label) # add columns from persons to facilitate building household interactions useful_columns = [_hh_id_, _ptype_, 'cdap_rank', _hh_size_] indiv_utils[useful_columns] = persons[useful_columns] - # if DUMP: - # tracing.trace_df(indiv_utils, '%s.DUMP.indiv_utils' % trace_label, - # transpose=False, slicer='NONE') - if trace_hh_id: - tracing.trace_df(individual_vars, '%s.individual_vars' % trace_label, - column_labels=['expression', 'person']) tracing.trace_df(indiv_utils, '%s.indiv_utils' % trace_label, column_labels=['activity', 'person']) @@ -445,7 +436,7 @@ def build_cdap_spec(interaction_coefficients, hhsize, # eval expression goes in the index spec.set_index(expression_name, inplace=True) - uniquify_spec_index(spec) + simulate.uniquify_spec_index(spec) if trace_spec: tracing.trace_df(spec, '%s.hhsize%d_spec' % (trace_label, hhsize), @@ -646,9 +637,7 @@ def household_activity_choices(indiv_utils, interaction_coefficients, hhsize, trace_spec=(trace_hh_id in choosers.index), trace_label=trace_label) - vars = eval_variables(spec.index, choosers) - - utils = compute_utilities(vars, spec) + utils = simulate.eval_utilities(spec, choosers, trace_label=trace_label) if len(utils.index) == 0: return pd.Series() @@ -667,8 +656,6 @@ def household_activity_choices(indiv_utils, interaction_coefficients, hhsize, if hhsize > 1: tracing.trace_df(choosers, '%s.hhsize%d_choosers' % (trace_label, hhsize), column_labels=['expression', 'person']) - tracing.trace_df(vars, '%s.hhsize%d_vars' % (trace_label, hhsize), - column_labels=['expression', 'person']) tracing.trace_df(utils, '%s.hhsize%d_utils' % (trace_label, hhsize), column_labels=['expression', 'household']) @@ -768,10 +755,10 @@ def extra_hh_member_choices(persons, cdap_fixed_relative_proportions, locals_d, return pd.Series() # eval the expression file - model_design = eval_variables(cdap_fixed_relative_proportions.index, choosers, locals_d) + values = simulate.eval_variables(cdap_fixed_relative_proportions.index, choosers, locals_d) # cdap_fixed_relative_proportions computes relative proportions by ptype, not utilities - proportions = model_design.dot(cdap_fixed_relative_proportions) + proportions = values.dot(cdap_fixed_relative_proportions) # convert relative proportions to probability probs = proportions.div(proportions.sum(axis=1), axis=0) diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index f323762de..2dbfcea26 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -9,8 +9,7 @@ from .. import cdap -from activitysim.core.simulate import read_model_spec -from activitysim.core.simulate import compute_utilities +from activitysim.core import simulate @pytest.fixture(scope='module') @@ -32,7 +31,7 @@ def people(data_dir): @pytest.fixture(scope='module') def cdap_indiv_and_hhsize1(configs_dir): - return read_model_spec(file_name='cdap_indiv_and_hhsize1.csv', spec_dir=configs_dir) + return simulate.read_model_spec(file_name='cdap_indiv_and_hhsize1.csv', spec_dir=configs_dir) @pytest.fixture(scope='module') @@ -117,9 +116,9 @@ def test_build_cdap_spec_hhsize2(people, cdap_indiv_and_hhsize1, cdap_interactio spec = cdap.build_cdap_spec(cdap_interaction_coefficients, hhsize=hhsize, cache=False) - vars = cdap.eval_variables(spec.index, choosers) + vars = simulate.eval_variables(spec.index, choosers) - utils = compute_utilities(vars, spec) + utils = simulate.compute_utilities(vars, spec) expected = pd.DataFrame([ [0, 3, 0, 3, 7, 3, 0, 3, 0], # household 3 diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 9f3e66737..0a1119fe2 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -157,8 +157,9 @@ def buffer_for_skims(skim_info, shared=False): # buffer_size must be int (or p2.7 long), not np.int64 buffer_size = int(np.prod(omx_shape) * block_size) - logger.info("allocating shared buffer %s for %s (%s) matrices" % - (block_name, buffer_size, omx_shape, )) + csz = buffer_size * np.dtype(skim_dtype).itemsize + logger.info("allocating shared buffer %s for %s (%s) matrices (%s)" % + (block_name, buffer_size, omx_shape, util.GB(csz))) if shared: if np.issubdtype(skim_dtype, np.float64): diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 2d8e030ff..34156ce4a 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -25,8 +25,9 @@ # array of chunk_size active CHUNK_LOG CHUNK_SIZE = [] -ELEMENTS_HWM = {} -BYTES_HWM = {} +ELEMENTS_HWM = [{}] +BYTES_HWM = [{}] +MEM_HWM = [{}] def GB(bytes): @@ -48,6 +49,10 @@ def log_open(trace_label, chunk_size): CHUNK_LOG[trace_label] = OrderedDict() CHUNK_SIZE.append(chunk_size) + ELEMENTS_HWM.append({}) + BYTES_HWM.append({}) + MEM_HWM.append({}) + def log_close(trace_label): @@ -55,29 +60,35 @@ def log_close(trace_label): logger.debug("log_close %s" % trace_label) - # if we are closing the root level - if len(CHUNK_LOG) == 1: - log_write() - # input("Return to continue: ") + _chunk_totals(logger) + log_write_hwm() label, _ = CHUNK_LOG.popitem(last=True) assert label == trace_label CHUNK_SIZE.pop() - if len(CHUNK_LOG) == 0: - ELEMENTS_HWM.clear() - BYTES_HWM.clear() + ELEMENTS_HWM.pop() + BYTES_HWM.pop() + MEM_HWM.pop() def log_df(trace_label, table_name, df): + # if df is None: + # mem.force_garbage_collect() + # return + cur_chunker = next(reversed(CHUNK_LOG)) + op = 'del' if df is None else 'add' + + prev_elements, prev_bytes = _chunk_totals() if df is None: - elements, bytes, shape = list(CHUNK_LOG.get(cur_chunker).pop(table_name)) - elements *= -1 - bytes *= -1 - df_trace_label = "%s.del_df.%s" % (trace_label, table_name) + CHUNK_LOG.get(cur_chunker).pop(table_name) + elements = bytes = 0 + shape = (0, 0) + + mem.force_garbage_collect() else: shape = df.shape @@ -95,62 +106,77 @@ def log_df(trace_label, table_name, df): CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes, shape) - df_trace_label = "%s.add_df.%s" % (trace_label, table_name) + # log this df + logger.debug("%s %s df %s %s %s : %s " % + (op, table_name, elements, shape, GB(bytes), trace_label)) + + total_elements, total_bytes = _chunk_totals() + cur_mem = mem.track_memory_info() - logger.debug("%s df %s %s %s : %s " % (table_name, elements, shape, GB(bytes), df_trace_label)) + # log current totals + logger.debug("total elements: %s (%s) bytes: %s (%s) mem: %s " % + (total_elements, total_elements-prev_elements, + GB(total_bytes), GB(total_bytes - prev_bytes), + GB(cur_mem), )) - total_elements, total_bytes = _log_totals(table_name) - logger.debug("total elements %s %s : %s" % (total_elements, GB(total_bytes), df_trace_label)) + # - check high_water_marks + hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) + hwm_info = "elements: %s bytes: %s mem: %s" % (total_elements, GB(total_bytes), GB(cur_mem)) + for hwm in ELEMENTS_HWM: + if total_elements > hwm.get('mark', 0): + hwm['mark'] = total_elements + hwm['trace_label'] = hwm_trace_label + hwm['info'] = hwm_info - cur_mem = mem.track_memory_info(trace_label) - logger.debug("current memory %s : %s" % (GB(cur_mem), df_trace_label)) + for hwm in BYTES_HWM: + if total_bytes > hwm.get('mark', 0): + hwm['mark'] = total_bytes + hwm['trace_label'] = hwm_trace_label + hwm['info'] = hwm_info + for hwm in MEM_HWM: + if cur_mem > hwm.get('mark', 0): + hwm['mark'] = cur_mem + hwm['trace_label'] = hwm_trace_label + hwm['info'] = hwm_info + + +def _chunk_totals(alogger=None): -def _log_totals(table_name): total_elements = 0 total_bytes = 0 for label in CHUNK_LOG: tables = CHUNK_LOG[label] for table_name in tables: elements, bytes, shape = tables[table_name] + if alogger: + alogger.debug("%s table %s %s %s %s" % + (label, table_name, shape, elements, GB(bytes))) total_elements += elements total_bytes += bytes - if total_elements > ELEMENTS_HWM.get('mark', 0): - ELEMENTS_HWM['mark'] = total_elements - ELEMENTS_HWM['chunker'] = CHUNK_LOG.keys() - ELEMENTS_HWM['table_name'] = table_name - - if total_bytes > BYTES_HWM.get('mark', 0): - BYTES_HWM['mark'] = total_bytes - BYTES_HWM['chunker'] = CHUNK_LOG.keys() - BYTES_HWM['table_name'] = table_name - - # if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: - # logger.warning("total_elements (%s) > chunk_size (%s)" % (total_elements, CHUNK_SIZE[0])) + if alogger: + alogger.debug("%s total elements %s %s" % + (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) return total_elements, total_bytes -def log_write(): - total_elements = 0 - total_bytes = 0 - for label in CHUNK_LOG: - tables = CHUNK_LOG[label] - for table_name in tables: - elements, bytes, shape = tables[table_name] - logger.debug("%s table %s %s %s %s" % (label, table_name, shape, elements, GB(bytes))) - total_elements += elements - total_bytes += bytes +def log_write_hwm(): - logger.debug("%s total elements %s %s" % (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) + hwm = ELEMENTS_HWM[-1] + if 'mark' in hwm: + logger.info("elements high_water_mark: %s (%s) in %s" % + (hwm['mark'], hwm['info'], hwm['trace_label']), ) + hwm = BYTES_HWM[-1] + if 'mark' in hwm: + logger.info("bytes high_water_mark: %s (%s) in %s" % + (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) - if 'mark' in ELEMENTS_HWM: - logger.info("chunk high_water_mark total_elements: %s in %s" % - (ELEMENTS_HWM['mark'], ELEMENTS_HWM['chunker']), ) - if 'mark' in BYTES_HWM: - logger.info("chunk high_water_mark total_bytes: %s in %s" % - (GB(BYTES_HWM['mark']), BYTES_HWM['chunker']), ) + hwm = MEM_HWM[-1] + if 'mark' in hwm: + logger.info("mem high_water_mark: %s (%s) in %s" % + (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index a43d9622a..084562999 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -242,7 +242,10 @@ def _interaction_sample( # interaction_utilities is a df with one utility column and one row per interaction_df row interaction_utilities, trace_eval_results \ = eval_interaction_utilities(spec, interaction_df, locals_d, trace_label, trace_rows) - chunk.log_df(trace_label, 'interaction_utils', interaction_utilities) + chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) + + del interaction_df + chunk.log_df(trace_label, 'interaction_df', None) if have_trace_targets: tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, @@ -261,6 +264,9 @@ def _interaction_sample( index=choosers.index) chunk.log_df(trace_label, 'utilities', utilities) + del interaction_utilities + chunk.log_df(trace_label, 'interaction_utilities', None) + if have_trace_targets: tracing.trace_df(utilities, tracing.extend_trace_label(trace_label, 'utilities'), column_labels=['alternative', 'utility']) @@ -273,6 +279,9 @@ def _interaction_sample( trace_label=trace_label, trace_choosers=choosers) chunk.log_df(trace_label, 'probs', probs) + del utilities + chunk.log_df(trace_label, 'utilities', None) + if have_trace_targets: tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), column_labels=['alternative', 'probability']) @@ -285,6 +294,9 @@ def _interaction_sample( chunk.log_df(trace_label, 'choices_df', choices_df) + del probs + chunk.log_df(trace_label, 'probs', None) + # make_sample_choices should return choosers index as choices_df column assert choosers.index.name in choices_df.columns @@ -303,6 +315,7 @@ def _interaction_sample( # drop the duplicates choices_df = choices_df[~choices_df['pick_dup']] del choices_df['pick_dup'] + chunk.log_df(trace_label, 'choices_df', choices_df) # set index after groupby so we can trace on it choices_df.set_index(choosers.index.name, inplace=True) @@ -317,6 +330,7 @@ def _interaction_sample( # don't need this after tracing del choices_df['rand'] + chunk.log_df(trace_label, 'choices_df', choices_df) # - NARROW choices_df['prob'] = choices_df['prob'].astype(np.float32) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index ae52deb7c..3afd49166 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -101,6 +101,20 @@ def to_series(x): for expr, coefficient in zip(spec.index, spec.iloc[:, 0]): try: + # fixme - remove this? (used only in trip_destination_sample.csv) + # allow temps of form _od_DIST@od_skim['DIST'] + if expr.startswith('_'): + target = expr[:expr.index('@')] + rhs = expr[expr.index('@') + 1:] + v = to_series(eval(rhs, globals(), locals_d)) + + # update locals to allows us to ref previously assigned targets + locals_d[target] = v + + if trace_eval_results is not None: + trace_eval_results[expr] = v[trace_rows] + continue + if expr.startswith('@'): v = to_series(eval(expr[1:], globals(), locals_d)) else: diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 776a4936b..a82c65225 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -251,7 +251,7 @@ def interaction_dataset(choosers, alternatives, sample_size=None): else: sample = np.tile(alts_idx, numchoosers) - alts_sample = alternatives.take(sample) + alts_sample = alternatives.take(sample).copy() alts_sample['chooser_idx'] = np.repeat(choosers.index.values, sample_size) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 0f8e3a4a5..712fa8b3d 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -14,49 +14,23 @@ logger = logging.getLogger(__name__) -MEM = {} - def force_garbage_collect(): gc.collect() -def GB(bytes): - gb = (bytes / (1024 * 1024 * 1024.0)) - return "%s GB" % (round(gb, 2), ) - - -def format_elapsed_time(t): - return "%s seconds (%s minutes)" % (round(t, 3), round(t / 60.0, 1)) - - -def set_pause_threshold(gb): - bytes = gb * 1024 * 1024 * 1024 - MEM['pause'] = bytes +# def GB(bytes): +# gb = (bytes / (1024 * 1024 * 1024.0)) +# return "%s GB" % (round(gb, 2), ) -def track_memory_info(trace_label): +def track_memory_info(): mi = psutil.Process().memory_info() # logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % # (GB(mi.rss), GB(mi.vms), trace_label)) - cur_mem = mi.vms - - if cur_mem > MEM.get('high_water_mark', 0): - MEM['high_water_mark'] = cur_mem - MEM['high_water_mark_trace_label'] = trace_label - logger.debug( - "memory_info new high_water_mark: %s trace_label: %s" % (GB(cur_mem), trace_label,)) - - if 'pause' in MEM and cur_mem > MEM['pause']: - MEM['pause'] = cur_mem - input("Return to continue: ") + # cur_mem = mi.vms + cur_mem = mi.rss return cur_mem - - -def log_mem_high_water_mark(): - if 'high_water_mark' in MEM: - logger.info("mem high_water_mark %s in %s" % - (GB(MEM['high_water_mark']), MEM['high_water_mark_trace_label']), ) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 1f5c586d2..c74c9dd3e 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -365,13 +365,12 @@ def run_simulation(queue, step_info, resume_after, skim_buffer): t1 = tracing.print_elapsed_time() pipeline.run_model(model) - tracing.print_elapsed_time("run_model %s %s" % (step_label, model,), t1) queue.put({'model': model, 'time': time.time()-t1}) t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) - mem.log_mem_high_water_mark() + chunk.log_write_hwm() pipeline.close_pipeline() @@ -692,12 +691,20 @@ def get_run_list(): resume_after = inject.get_injectable('resume_after', None) or setting('resume_after', None) multiprocess = inject.get_injectable('multiprocess', False) or setting('multiprocess', False) global_chunk_size = setting('chunk_size', 0) + default_mp_processes = setting('num_processes', 0) or int(1 + multiprocessing.cpu_count() / 2.0) + default_stagger = setting('stagger', 0) multiprocess_steps = setting('multiprocess_steps', []) if multiprocess and multiprocessing.cpu_count() == 1: logger.warning("Can't multiprocess because there is only 1 cpu") multiprocess = False + # if not multiprocess: + # multiprocess_steps = [ + # {'name': 'mp_simulation', 'begin': models[0]} + # ] + # multiprocess = True + run_list = { 'models': models, 'resume_after': resume_after, @@ -745,9 +752,8 @@ def get_run_list(): if 'slice' in step: if num_processes == 0: - logger.info("Setting num_processes = %s for step %s", - num_processes, name) - num_processes = multiprocessing.cpu_count() + logger.info("Setting num_processes = %s for step %s", num_processes, name) + num_processes = default_mp_processes if num_processes == 1: raise RuntimeError("num_processes = 1 but found slice info for step %s" " in multiprocess_steps" % name) @@ -775,7 +781,7 @@ def get_run_list(): multiprocess_steps[istep]['chunk_size'] = chunk_size # - validate stagger and assign default - multiprocess_steps[istep]['stagger'] = max(int(step.get('stagger', 0)), 0) + multiprocess_steps[istep]['stagger'] = max(int(step.get('stagger', default_stagger)), 0) # - determine index in models list of step starts start_tag = 'begin' diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index dcc1cd016..264eb8209 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -140,6 +140,60 @@ def read_model_spec(model_settings=None, file_name=None, spec_dir=None, return spec +def eval_utilities(spec, choosers, locals_d=None, trace_label=None): + + # fixme - restore tracing and _check_for_variability + + t0 = tracing.print_elapsed_time() + + # if False: #fixme SLOWER + # expression_values = eval_variables(spec.index, choosers, locals_d) + # # chunk.log_df(trace_label, 'expression_values', expression_values) + # # if trace_label and tracing.has_trace_targets(choosers): + # # tracing.trace_df(expression_values, '%s.expression_values' % trace_label, + # # column_labels=['expression', None]) + # # if config.setting('check_for_variability'): + # # _check_for_variability(expression_values, trace_label) + # utilities = compute_utilities(expression_values, spec) + # + # # chunk.log_df(trace_label, 'expression_values', None) + # t0 = tracing.print_elapsed_time(" eval_utilities SLOWER", t0) + # + # return utilities + + # - eval spec expressions + + # avoid altering caller's passed-in locals_d parameter (they may be looping) + locals_dict = assign.local_utilities() + if locals_d is not None: + locals_dict.update(locals_d) + globals_dict = {} + + locals_dict['df'] = choosers + + exprs = spec.index + expression_values = np.empty((spec.shape[0], choosers.shape[0])) + i = 0 + for expr in exprs: + try: + if expr.startswith('@'): + expression_values[i] = eval(expr[1:], globals_dict, locals_dict) + else: + expression_values[i] = choosers.eval(expr) + i += 1 + except Exception as err: + logger.exception("Variable evaluation failed for: %s" % str(expr)) + raise err + + # - compute_utilities + utilities = np.dot(expression_values.transpose(), spec.astype(np.float64).values) + utilities = pd.DataFrame(data=utilities, index=choosers.index, columns=spec.columns) + + t0 = tracing.print_elapsed_time(" eval_utilities", t0) + + return utilities + + def eval_variables(exprs, df, locals_d=None): """ Evaluate a set of variable expressions from a spec in the context @@ -191,6 +245,8 @@ def to_array(x): # assert x.index.equals(df.index) # save a little RAM a = x.values + else: + a = x # FIXME - for performance, it is essential that spec and expression_values # FIXME - not contain booleans when dotted with spec values @@ -482,27 +538,9 @@ def eval_mnl(choosers, spec, locals_d, custom_chooser, if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - expression_values = eval_variables(spec.index, choosers, locals_d) - chunk.log_df(trace_label, 'expression_values', expression_values) - - if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - - if config.setting('check_for_variability'): - _check_for_variability(expression_values, trace_label) - - # matrix product of spec expression_values with utility coefficients of alternatives - # sums the partial utilities (represented by each spec row) of the alternatives - # resulting in a dataframe with one row per chooser and one column per alternative - # pandas.dot depends on column names of expression_values matching spec index values - - utilities = compute_utilities(expression_values, spec) + utilities = eval_utilities(spec, choosers, locals_d, trace_label) chunk.log_df(trace_label, "utilities", utilities) - del expression_values - chunk.log_df(trace_label, 'expression_values', None) - if have_trace_targets: tracing.trace_df(utilities, '%s.utilities' % trace_label, column_labels=['alternative', 'utility']) @@ -577,28 +615,13 @@ def eval_nl(choosers, spec, nest_spec, locals_d, custom_chooser, if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - # column names of expression_values match spec index values - expression_values = eval_variables(spec.index, choosers, locals_d) - chunk.log_df(trace_label, "expression_values", expression_values) - - if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - - if config.setting('check_for_variability'): - _check_for_variability(expression_values, trace_label) - - # raw utilities of all the leaves - raw_utilities = compute_utilities(expression_values, spec) + raw_utilities = eval_utilities(spec, choosers, locals_d, trace_label) chunk.log_df(trace_label, "raw_utilities", raw_utilities) if have_trace_targets: tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, column_labels=['alternative', 'utility']) - del expression_values - chunk.log_df(trace_label, 'expression_values', None) - # exponentiated utilities of leaves and nests nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) @@ -829,21 +852,12 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - expression_values = eval_variables(spec.index, choosers, locals_d) - chunk.log_df(trace_label, 'expression_values', expression_values) + utilities = eval_utilities(spec, choosers, locals_d, trace_label) + chunk.log_df(trace_label, "utilities", utilities) if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - if config.setting('check_for_variability'): - _check_for_variability(expression_values, trace_label) - - # utility values - utilities = compute_utilities(expression_values, spec) - chunk.log_df(trace_label, "utilities", utilities,) - - del expression_values # done with expression_values - chunk.log_df(trace_label, 'expression_values', None) + tracing.trace_df(utilities, '%s.raw_utilities' % trace_label, + column_labels=['alternative', 'utility']) # - logsums # logsum is log of exponentiated utilities summed across columns of each chooser row @@ -853,10 +867,6 @@ def eval_mnl_logsums(choosers, spec, locals_d, trace_label=None): # trace utilities if have_trace_targets: - # add logsum to utilities for tracing - utilities['logsum'] = logsums - tracing.trace_df(utilities, '%s.utilities' % trace_label, - column_labels=['alternative', 'utility']) tracing.trace_df(logsums, '%s.logsums' % trace_label, column_labels=['alternative', 'logsum']) @@ -880,29 +890,13 @@ def eval_nl_logsums(choosers, spec, nest_spec, locals_d, trace_label=None): if have_trace_targets: tracing.trace_df(choosers, '%s.choosers' % trace_label) - # - eval spec expressions - # column names of expression_values match spec index values - expression_values = eval_variables(spec.index, choosers, locals_d) - chunk.log_df(trace_label, 'expression_values', expression_values) - - if have_trace_targets: - tracing.trace_df(expression_values, '%s.expression_values' % trace_label, - column_labels=['expression', None]) - - if config.setting('check_for_variability'): - _check_for_variability(expression_values, trace_label) - - # - raw utilities of all the leaves - raw_utilities = compute_utilities(expression_values, spec) + raw_utilities = eval_utilities(spec, choosers, locals_d, trace_label) chunk.log_df(trace_label, "raw_utilities", raw_utilities) if have_trace_targets: tracing.trace_df(raw_utilities, '%s.raw_utilities' % trace_label, column_labels=['alternative', 'utility']) - del expression_values # done with expression_values - chunk.log_df(trace_label, 'expression_values', None) - # - exponentiated utilities of leaves and nests nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) chunk.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 1aab3f879..9db4d8cd4 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -55,7 +55,7 @@ def set_offset_int(self, offset_int): assert self.offset_series is None if self.offset_int is None: - self.offset_int = offset_int + self.offset_int = int(offset_int) else: # make sure it is the same assert offset_int == self.offset_int @@ -71,8 +71,6 @@ def map(self, zone_ids): offsets = np.asanyarray(quick_loc_series(zone_ids, self.offset_series)) elif self.offset_int: - # should be some kind of integer - assert int(self.offset_int) == self.offset_int assert (self.offset_series is None) offsets = zone_ids + self.offset_int else: @@ -116,27 +114,44 @@ def get(self, orig, dest): values : 1D array """ - # only working with numpy in here - orig = np.asanyarray(orig) - dest = np.asanyarray(dest) - out_shape = orig.shape - # filter orig and dest to only the real-number pairs - notnan = ~(np.isnan(orig) | np.isnan(dest)) - orig = orig[notnan].astype('int') - dest = dest[notnan].astype('int') + # if False: #fixme SLOWER + # # fixme - I don't think we need to support nan orig, dest values + # + # # only working with numpy in here + # orig = np.asanyarray(orig) + # dest = np.asanyarray(dest) + # out_shape = orig.shape + # + # # filter orig and dest to only the real-number pairs + # notnan = ~(np.isnan(orig) | np.isnan(dest)) + # + # orig = orig[notnan].astype('int') + # dest = dest[notnan].astype('int') + # + # orig = self.offset_mapper.map(orig) + # dest = self.offset_mapper.map(dest) + # + # result = self.data[orig, dest] + # + # # add the nans back to the result + # # (np.empty ensures result type is np.float64 to support nans) + # out = np.empty(out_shape) + # out[notnan] = result + # out[~notnan] = np.nan + # + # return out + + # only working with numpy in here + orig = np.asanyarray(orig).astype(int) + dest = np.asanyarray(dest).astype(int) orig = self.offset_mapper.map(orig) dest = self.offset_mapper.map(dest) result = self.data[orig, dest] - # add the nans back to the result - out = np.empty(out_shape) - out[notnan] = result - out[~notnan] = np.nan - - return out + return result class SkimDict(object): diff --git a/activitysim/core/test/test_skim.py b/activitysim/core/test/test_skim.py index ed01e8cb0..f37564e6f 100644 --- a/activitysim/core/test/test_skim.py +++ b/activitysim/core/test/test_skim.py @@ -58,15 +58,16 @@ def test_offset_list(data): [52, 99, 16]) -def test_skim_nans(data): - sk = skim.SkimWrapper(data) - - orig = [5, np.nan, 1, 2] - dest = [np.nan, 9, 6, 4] - - npt.assert_array_equal( - sk.get(orig, dest), - [np.nan, np.nan, 16, 24]) +# fixme - nan support disabled in skim.py (not sure we need it?) +# def test_skim_nans(data): +# sk = skim.SkimWrapper(data) +# +# orig = [5, np.nan, 1, 2] +# dest = [np.nan, 9, 6, 4] +# +# npt.assert_array_equal( +# sk.get(orig, dest), +# [np.nan, np.nan, 16, 24]) def test_skims(data): @@ -97,7 +98,7 @@ def test_skims(data): pd.Series( [12, 93, 47], index=[0, 1, 2] - ).astype('float64') + ).astype(data.dtype) ) pdt.assert_series_equal( @@ -105,7 +106,7 @@ def test_skims(data): pd.Series( [120, 930, 470], index=[0, 1, 2] - ).astype('float64') + ).astype(data.dtype) ) diff --git a/example/configs/trip_destination_sample.csv b/example/configs/trip_destination_sample.csv index 5e603d0a1..5029242b3 100644 --- a/example/configs/trip_destination_sample.csv +++ b/example/configs/trip_destination_sample.csv @@ -1,4 +1,6 @@ Description,Expression,work,univ,school,escort,shopping,eatout,othmaint,social,othdiscr,atwork +,_od_DIST@od_skims['DIST'],1,1,1,1,1,1,1,1,1,1 +,_dp_DIST@dp_skims['DIST'],1,1,1,1,1,1,1,1,1,1 Not available if walk tour not within walking distance,@(df.tour_mode=='WALK') & (od_skims['DISTWALK'] > max_walk_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 Not available if walk tour not within walking distance,@(df.tour_mode=='WALK') & (dp_skims['DISTWALK'] > max_walk_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 Not available if bike tour not within biking distance,@(df.tour_mode=='BIKE') & (od_skims['DISTBIKE'] > max_bike_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 @@ -7,10 +9,26 @@ Not available if bike tour not within biking distance,@(df.tour_mode=='BIKE') & size term,"@np.log1p(size_terms.get(df.dest_taz, df.purpose))",1,1,1,1,1,1,1,1,1,1 no attractions,"@size_terms.get(df.dest_taz, df.purpose) == 0",-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 #stop zone CBD area type,"@reindex(land_use.area_type, df.dest_taz) < setting('cbd_threshold')",,,,,,,,,, -distance (calibration adjustment individual - inbound),@(~df.is_joint & ~df.outbound) * (od_skims['DIST'] + dp_skims['DIST']),-0.04972591574229,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 -distance (calibration adjustment individual - outbound),@(~df.is_joint & df.outbound) * (od_skims['DIST'] + dp_skims['DIST']),0.147813278663948,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 -distance (calibration adjustment joint),@df.is_joint * (od_skims['DIST'] + dp_skims['DIST']),0,0,0,-0.1238,-0.1238,-0.1238,-0.1238,-0.1238,-0.123801985,0 -stop proximity to home (outbound),@df.outbound * od_skims['DIST'],-0.3800,0,0,0,0,0,0,0,0,0 -stop proximity to home (inbound),@~df.outbound * dp_skims['DIST'],-0.1500,0,0,0,0,0,0,0,0,0 -stop proximity to main destination (outbound),@df.outbound * dp_skims['DIST'],-0.26,,,,,,,,, -stop proximity to main destination (inbound),@~df.outbound * od_skims['DIST'],0,,,,,,,,, +distance (calibration adjustment individual - inbound),@(~df.is_joint & ~df.outbound) * (_od_DIST + _dp_DIST),-0.04972591574229,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 +distance (calibration adjustment individual - outbound),@(~df.is_joint & df.outbound) * (_od_DIST + _dp_DIST),0.147813278663948,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 +distance (calibration adjustment joint),@df.is_joint * (_od_DIST + _dp_DIST),0,0,0,-0.1238,-0.1238,-0.1238,-0.1238,-0.1238,-0.123801985,0 +stop proximity to home (outbound),@df.outbound * _od_DIST,-0.3800,0,0,0,0,0,0,0,0,0 +stop proximity to home (inbound),@~df.outbound * _od_DIST,-0.1500,0,0,0,0,0,0,0,0,0 +stop proximity to main destination (outbound),@df.outbound * _dp_DIST,-0.26,,,,,,,,, +stop proximity to main destination (inbound),@~df.outbound * _od_DIST,0,,,,,,,,, +#,, +#Not available if walk tour not within walking distance,@(df.tour_mode=='WALK') & (od_skims['DISTWALK'] > max_walk_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 +#Not available if walk tour not within walking distance,@(df.tour_mode=='WALK') & (dp_skims['DISTWALK'] > max_walk_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 +#Not available if bike tour not within biking distance,@(df.tour_mode=='BIKE') & (od_skims['DISTBIKE'] > max_bike_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 +#Not available if bike tour not within biking distance,@(df.tour_mode=='BIKE') & (dp_skims['DISTBIKE'] > max_bike_distance),-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 +##If transit tour is not in walk sub-zone it must be walkable,,,,,,,,,,, +#size term,"@np.log1p(size_terms.get(df.dest_taz, df.purpose))",1,1,1,1,1,1,1,1,1,1 +#no attractions,"@size_terms.get(df.dest_taz, df.purpose) == 0",-999,-999,-999,-999,-999,-999,-999,-999,-999,-999 +##stop zone CBD area type,"@reindex(land_use.area_type, df.dest_taz) < setting('cbd_threshold')",,,,,,,,,, +#distance (calibration adjustment individual - inbound),@(~df.is_joint & ~df.outbound) * (od_skims['DIST'] + dp_skims['DIST']),-0.04972591574229,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 +#distance (calibration adjustment individual - outbound),@(~df.is_joint & df.outbound) * (od_skims['DIST'] + dp_skims['DIST']),0.147813278663948,-0.0613,-0.1056,-0.1491,-0.1192,-0.1029,-0.0962,-0.1329,-0.126172224,-0.122334597 +#distance (calibration adjustment joint),@df.is_joint * (od_skims['DIST'] + dp_skims['DIST']),0,0,0,-0.1238,-0.1238,-0.1238,-0.1238,-0.1238,-0.123801985,0 +#stop proximity to home (outbound),@df.outbound * od_skims['DIST'],-0.3800,0,0,0,0,0,0,0,0,0 +#stop proximity to home (inbound),@~df.outbound * dp_skims['DIST'],-0.1500,0,0,0,0,0,0,0,0,0 +#stop proximity to main destination (outbound),@df.outbound * dp_skims['DIST'],-0.26,,,,,,,,, +#stop proximity to main destination (inbound),@~df.outbound * od_skims['DIST'],0,,,,,,,,, diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 2bdd728d9..17e015fa6 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -10,13 +10,32 @@ inherit_settings: True #trace_hh_id: None #trace_od: None +# full +#households_sample_size: 0 +#multiprocess: True +#chunk_size: 15000000000 +#num_processes: 10 +#stagger: 30 +#profile: False +#strict: False +#trace_hh_id: None +#trace_od: None + +# example +#households_sample_size: 273272 households_sample_size: 20000 -chunk_size: 1500000000 + multiprocess: False +chunk_size: 1500000000 +num_processes: 3 +stagger: 5 profile: True strict: True +trace_hh_id: 1482966 +trace_od: [5, 11] - +# to resume after last successful checkpoint, specify resume_after: _ +#resume_after: trip_purpose models: - initialize_landuse @@ -42,30 +61,29 @@ models: - non_mandatory_tour_frequency - non_mandatory_tour_destination - non_mandatory_tour_scheduling -# - tour_mode_choice_simulate -# - atwork_subtour_frequency -# - _atwork_subtour_destination_sample -# - _atwork_subtour_destination_logsums -# - atwork_subtour_destination_simulate -# - atwork_subtour_scheduling -# - atwork_subtour_mode_choice -# - stop_frequency -# - trip_purpose -# - trip_destination -# - trip_purpose_and_destination -# - trip_scheduling -# - trip_mode_choice -# - write_data_dictionary -# - write_tables - + - tour_mode_choice_simulate + - atwork_subtour_frequency + - _atwork_subtour_destination_sample + - _atwork_subtour_destination_logsums + - atwork_subtour_destination_simulate + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + - write_data_dictionary + - write_tables multiprocess_steps: - name: mp_initialize begin: initialize_landuse - name: mp_households begin: _school_location_sample - num_processes: 3 - stagger: 5 + #num_processes: 10 + #stagger: 30 #chunk_size: 1000000000 slice: tables: diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 261444be6..a09c4ca21 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -45,6 +45,7 @@ def run(run_list, injectables=None): logger.info("run single process simulation") pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) pipeline.close_pipeline() + chunk.log_write_hwm() if __name__ == '__main__': @@ -81,5 +82,3 @@ def run(run_list, injectables=None): run(run_list, injectables) t0 = tracing.print_elapsed_time("everything", t0) - - mem.log_mem_high_water_mark() From 28009652b40743bc17e0e44bdd4a8c4167ff44f7 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 31 Oct 2018 17:35:41 -0400 Subject: [PATCH 038/122] trace_memory_info --- activitysim/core/chunk.py | 50 ++++++++++++------------------ activitysim/core/mem.py | 53 +++++++++++++++++++++++++++----- activitysim/core/mp_tasks.py | 46 +++++++++++++++++++-------- activitysim/core/tracing.py | 1 + example/configs/settings.yaml | 8 ++--- example_mp/configs/settings.yaml | 9 +++--- 6 files changed, 109 insertions(+), 58 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 34156ce4a..5949a31ab 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -30,13 +30,13 @@ MEM_HWM = [{}] -def GB(bytes): - if bytes < (1024 * 1024 * 1024.0): - mb = (bytes / (1024 * 1024.0)) - return "(%s MB)" % round(mb, 2) +def GB(bytes, sign=False): + if bytes < (1024 * 1024): + return ("%+.2f KB" if sign else "%.2f KB") % (bytes / 1024) + elif bytes < (1024 * 1024 * 1024): + return ("%+.2f MB" if sign else "%.2f MB") % (bytes / (1024 * 1024)) else: - gb = (bytes / (1024 * 1024 * 1024.0)) - return "(%s GB)" % round(gb, 2) + return ("%+.2f GB" if sign else "%.2f GB") % (bytes / (1024 * 1024 * 1024)) def log_open(trace_label, chunk_size): @@ -60,7 +60,6 @@ def log_close(trace_label): logger.debug("log_close %s" % trace_label) - _chunk_totals(logger) log_write_hwm() label, _ = CHUNK_LOG.popitem(last=True) @@ -81,8 +80,6 @@ def log_df(trace_label, table_name, df): cur_chunker = next(reversed(CHUNK_LOG)) op = 'del' if df is None else 'add' - prev_elements, prev_bytes = _chunk_totals() - if df is None: CHUNK_LOG.get(cur_chunker).pop(table_name) elements = bytes = 0 @@ -111,37 +108,37 @@ def log_df(trace_label, table_name, df): (op, table_name, elements, shape, GB(bytes), trace_label)) total_elements, total_bytes = _chunk_totals() - cur_mem = mem.track_memory_info() + cur_mem = mem.get_memory_info() - # log current totals - logger.debug("total elements: %s (%s) bytes: %s (%s) mem: %s " % - (total_elements, total_elements-prev_elements, - GB(total_bytes), GB(total_bytes - prev_bytes), - GB(cur_mem), )) + # # log current totals + # logger.debug("%s %s total elements: %d (%+d) bytes: %s (%s) mem: %s " % + # (op, table_name, + # total_elements, total_elements-prev_elements, + # GB(total_bytes), GB(total_bytes - prev_bytes, sign=True), + # GB(cur_mem), )) # - check high_water_marks hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) - hwm_info = "elements: %s bytes: %s mem: %s" % (total_elements, GB(total_bytes), GB(cur_mem)) for hwm in ELEMENTS_HWM: if total_elements > hwm.get('mark', 0): hwm['mark'] = total_elements hwm['trace_label'] = hwm_trace_label - hwm['info'] = hwm_info + hwm['info'] = "bytes: %s mem: %s" % (GB(total_bytes), GB(cur_mem)) for hwm in BYTES_HWM: if total_bytes > hwm.get('mark', 0): hwm['mark'] = total_bytes hwm['trace_label'] = hwm_trace_label - hwm['info'] = hwm_info + hwm['info'] = "elements: %s mem: %s" % (total_elements, GB(cur_mem)) for hwm in MEM_HWM: if cur_mem > hwm.get('mark', 0): hwm['mark'] = cur_mem hwm['trace_label'] = hwm_trace_label - hwm['info'] = hwm_info + hwm['info'] = "elements: %s bytes: %s" % (total_elements, GB(total_bytes)) -def _chunk_totals(alogger=None): +def _chunk_totals(): total_elements = 0 total_bytes = 0 @@ -149,16 +146,9 @@ def _chunk_totals(alogger=None): tables = CHUNK_LOG[label] for table_name in tables: elements, bytes, shape = tables[table_name] - if alogger: - alogger.debug("%s table %s %s %s %s" % - (label, table_name, shape, elements, GB(bytes))) total_elements += elements total_bytes += bytes - if alogger: - alogger.debug("%s total elements %s %s" % - (CHUNK_LOG.keys(), total_elements, GB(total_bytes))) - return total_elements, total_bytes @@ -166,16 +156,16 @@ def log_write_hwm(): hwm = ELEMENTS_HWM[-1] if 'mark' in hwm: - logger.info("elements high_water_mark: %s (%s) in %s" % + logger.info("high_water_mark elements: %s (%s) in %s" % (hwm['mark'], hwm['info'], hwm['trace_label']), ) hwm = BYTES_HWM[-1] if 'mark' in hwm: - logger.info("bytes high_water_mark: %s (%s) in %s" % + logger.info("high_water_mark bytes: %s (%s) in %s" % (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) hwm = MEM_HWM[-1] if 'mark' in hwm: - logger.info("mem high_water_mark: %s (%s) in %s" % + logger.info("high_water_mark mem: %s (%s) in %s" % (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 712fa8b3d..317381bf6 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -6,29 +6,68 @@ from future.standard_library import install_aliases install_aliases() # noqa: E402 -from builtins import input +import time import psutil import logging import gc +import sys + +from activitysim.core import config logger = logging.getLogger(__name__) +MEM = {'tick': 0} +TICK_LEN = 5 + def force_garbage_collect(): gc.collect() -# def GB(bytes): -# gb = (bytes / (1024 * 1024 * 1024.0)) -# return "%s GB" % (round(gb, 2), ) +def GB(bytes): + return "%.2f" % (bytes / (1024 * 1024 * 1024.0)) + + +def trace_memory_info(event=''): + + last_tick = MEM['tick'] + + t = time.time() + if (t - last_tick < TICK_LEN) and not event: + return + + vmi = psutil.virtual_memory() + + if last_tick == 0: + MEM['baseline_tick'] = t + MEM['baseline_used'] = vmi.used + mode = 'wb' if sys.version_info < (3,) else 'w' + with open(config.output_file_path('mem.csv'), mode) as file: + print("seconds,delta_used,used,available,percent,event", file=file) + + MEM['tick'] = t + baseline_tick = MEM['baseline_tick'] + baseline_used = MEM['baseline_used'] + + # logger.debug("memory_info: rss: %s available: %s percent: %s" + # % (GB(mi.rss), GB(vmi.available), GB(vmi.percent))) + + mode = 'ab' if sys.version_info < (3,) else 'a' + with open(config.output_file_path('mem.csv'), mode) as file: + + print("%s, %s, %s, %s, %s%%, %s" % + (int(t - baseline_tick), + GB(vmi.used - baseline_used), + GB(vmi.used), + GB(vmi.available), + vmi.percent, + event), file=file) -def track_memory_info(): +def get_memory_info(): mi = psutil.Process().memory_info() - # logger.debug("memory_info: rss: %s vms: %s trace_label: %s" % - # (GB(mi.rss), GB(mi.vms), trace_label)) # cur_mem = mi.vms cur_mem = mi.rss diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index c74c9dd3e..63d2fcc09 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -304,6 +304,13 @@ def load_skim_data(skim_buffer): def allocate_shared_skim_buffer(): + """ + This is called by the main process and allocate memory buffer to share with subprocs + + Returns + ------- + multiprocessing.RawArray + """ logger.info("allocate_shared_skim_buffer") @@ -449,6 +456,7 @@ def log_queued_messages(): msg = queue.get(block=False) logger.info("%s %s : %s", process.name, msg['model'], tracing.format_elapsed_time(msg['time'])) + mem.trace_memory_info("%s.%s.completed" % (process.name, msg['model'])) def check_proc_status(): # we want to drop 'completed' breadcrumb when it happens, lest we terminate @@ -460,20 +468,24 @@ def check_proc_status(): if p.name not in completed: logger.info("process %s completed", p.name) completed.add(p.name) - drob_breadcrumb(step_name, 'completed', list(completed)) + drop_breadcrumb(step_name, 'completed', list(completed)) + mem.trace_memory_info("%s.completed" % p.name) else: # process failed if p.name not in failed: logger.info("process %s failed with exitcode %s", p.name, p.exitcode) failed.add(p.name) + mem.trace_memory_info("%s.failed" % p.name) def idle(seconds): log_queued_messages() check_proc_status() + mem.trace_memory_info() for _ in range(seconds): time.sleep(1) log_queued_messages() check_proc_status() + mem.trace_memory_info() step_name = step_info['name'] @@ -498,7 +510,7 @@ def idle(seconds): completed = set(previously_completed) failed = set([]) # so we can log process failure when it happens - drob_breadcrumb(step_name, 'completed', list(completed)) + drop_breadcrumb(step_name, 'completed', list(completed)) for process_name in process_names: q = multiprocessing.Queue() @@ -515,6 +527,7 @@ def idle(seconds): idle(seconds=stagger_starts) logger.info("start process %s", p.name) p.start() + mem.trace_memory_info("%s.start" % p.name) # - idle logging queued messages and proc completion while multiprocessing.active_children(): @@ -534,7 +547,7 @@ def idle(seconds): logger.error("Process %s completed with exitcode %s", p.name, p.exitcode) assert p.name in completed - t0 = tracing.print_elapsed_time('run_sub_simulations step %s' % step_name) + t0 = tracing.print_elapsed_time('run_sub_simulations step %s' % step_name, t0) return list(completed) @@ -542,18 +555,22 @@ def idle(seconds): def run_sub_task(p): logger.info("running sub_process %s", p.name) + mem.trace_memory_info("%s.start" % p.name) + t0 = tracing.print_elapsed_time() p.start() p.join() - t0 = tracing.print_elapsed_time('sub_process %s' % p.name) + t0 = tracing.print_elapsed_time('sub_process %s' % p.name, t0) # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) + mem.trace_memory_info("%s.completed" % p.name) + if p.exitcode: logger.error("Process %s returned exitcode %s", p.name, p.exitcode) raise RuntimeError("Process %s returned exitcode %s" % (p.name, p.exitcode)) -def drob_breadcrumb(step_name, crumb, value=True): +def drop_breadcrumb(step_name, crumb, value=True): breadcrumbs = inject.get_injectable('breadcrumbs', OrderedDict()) breadcrumbs.setdefault(step_name, {'name': step_name})[crumb] = value inject.add_injectable('breadcrumbs', breadcrumbs) @@ -562,6 +579,8 @@ def drob_breadcrumb(step_name, crumb, value=True): def run_multiprocess(run_list, injectables): + mem.trace_memory_info("run_multiprocess.start") + if not run_list['multiprocess']: raise RuntimeError("run_multiprocess called but multiprocess flag is %s" % run_list['multiprocess']) @@ -580,6 +599,7 @@ def find_breadcrumb(crumb, default=None): t0 = tracing.print_elapsed_time() shared_skim_buffer = allocate_shared_skim_buffer() t0 = tracing.print_elapsed_time('allocate shared skim buffer', t0) + mem.trace_memory_info("allocate_shared_skim_buffer.completed") # - mp_setup_skims run_sub_task( @@ -608,7 +628,7 @@ def find_breadcrumb(crumb, default=None): target=mp_apportion_pipeline, name='%s_apportion' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - drob_breadcrumb(step_name, 'apportion') + drop_breadcrumb(step_name, 'apportion') # - run_sub_simulations if not skip_phase('simulate'): @@ -622,7 +642,7 @@ def find_breadcrumb(crumb, default=None): if len(completed) != num_processes: raise RuntimeError("%s processes failed in step %s" % (num_processes - len(completed), step_name)) - drob_breadcrumb(step_name, 'simulate') + drop_breadcrumb(step_name, 'simulate') # - mp_coalesce_pipelines if not skip_phase('coalesce') and num_processes > 1: @@ -631,7 +651,7 @@ def find_breadcrumb(crumb, default=None): target=mp_coalesce_pipelines, name='%s_coalesce' % step_name, args=(injectables, sub_proc_names, slice_info)) ) - drob_breadcrumb(step_name, 'coalesce') + drop_breadcrumb(step_name, 'coalesce') def get_breadcrumbs(run_list): @@ -699,11 +719,11 @@ def get_run_list(): logger.warning("Can't multiprocess because there is only 1 cpu") multiprocess = False - # if not multiprocess: - # multiprocess_steps = [ - # {'name': 'mp_simulation', 'begin': models[0]} - # ] - # multiprocess = True + if not multiprocess and setting('singleprocess_as_subtask', False): + multiprocess_steps = [ + {'name': 'mp_simulation', 'begin': models[0]} + ] + multiprocess = True run_list = { 'models': models, diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 82318238b..cb9acf524 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -52,6 +52,7 @@ def format_elapsed_time(t): def print_elapsed_time(msg=None, t0=None, debug=False): t1 = time.time() if msg: + assert t0 is not None t = t1 - (t0 or t1) msg = "Time to execute %s : %s" % (msg, format_elapsed_time(t)) if debug: diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index d7be91c1e..cbbc80023 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -7,13 +7,13 @@ households_sample_size: 100 # simulate all households #households_sample_size: 0 -#trace household id; comment out or None for no trace +#trace household id; comment out or leave empty for no trace #trace_hh_id: 1482966 -trace_hh_id: None +trace_hh_id: -# trace origin, destination in accessibility calculation; comment out or None for no trace +# trace origin, destination in accessibility calculation; comment out or leave empty for no trace #trace_od: [5, 11] -trace_od: None +trace_od: chunk_size: 0 diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 17e015fa6..6ece5b566 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -18,14 +18,15 @@ inherit_settings: True #stagger: 30 #profile: False #strict: False -#trace_hh_id: None -#trace_od: None +#trace_hh_id: +#trace_od: # example -#households_sample_size: 273272 -households_sample_size: 20000 +#households_sample_size: 273272 +households_sample_size: 40000 multiprocess: False +singleprocess_as_subtask: True chunk_size: 1500000000 num_processes: 3 stagger: 5 From 0a9f5de0e2d86badaf9a671d1114e62fa7878f26 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 31 Oct 2018 23:10:10 -0400 Subject: [PATCH 039/122] trace_memory_info sums rss of children --- example_mp/configs/settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 6ece5b566..8039cafc0 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -30,7 +30,7 @@ singleprocess_as_subtask: True chunk_size: 1500000000 num_processes: 3 stagger: 5 -profile: True +profile: False strict: True trace_hh_id: 1482966 trace_od: [5, 11] From 86569c795b921ad378605e0d3f0a9d730f1e9696 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 31 Oct 2018 23:11:39 -0400 Subject: [PATCH 040/122] trace_memory_info sums rss of children --- activitysim/core/chunk.py | 20 +++++++------------ .../core/interaction_sample_simulate.py | 3 --- activitysim/core/mem.py | 20 ++++++++++++------- activitysim/core/mp_tasks.py | 2 +- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 5949a31ab..5c9dd9d0b 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -78,18 +78,19 @@ def log_df(trace_label, table_name, df): # return cur_chunker = next(reversed(CHUNK_LOG)) - op = 'del' if df is None else 'add' if df is None: CHUNK_LOG.get(cur_chunker).pop(table_name) - elements = bytes = 0 - shape = (0, 0) + op = 'del' + + logger.debug("del %s df : %s " % (table_name, trace_label)) mem.force_garbage_collect() else: shape = df.shape elements = np.prod(shape) + op = 'add' if isinstance(df, pd.Series): bytes = df.memory_usage(index=True) @@ -103,20 +104,13 @@ def log_df(trace_label, table_name, df): CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes, shape) - # log this df - logger.debug("%s %s df %s %s %s : %s " % - (op, table_name, elements, shape, GB(bytes), trace_label)) + # log this df + logger.debug("add %s df %s %s %s : %s " % + (table_name, elements, shape, GB(bytes), trace_label)) total_elements, total_bytes = _chunk_totals() cur_mem = mem.get_memory_info() - # # log current totals - # logger.debug("%s %s total elements: %d (%+d) bytes: %s (%s) mem: %s " % - # (op, table_name, - # total_elements, total_elements-prev_elements, - # GB(total_bytes), GB(total_bytes - prev_bytes, sign=True), - # GB(cur_mem), )) - # - check high_water_marks hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) for hwm in ELEMENTS_HWM: diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index 1943e4981..ba0434f21 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -167,16 +167,13 @@ def _interaction_sample_simulate( # (we want to insert dummy utilities at the END of the list of alternative utilities) # inserts is a list of the indices at which we want to do the insertions inserts = np.repeat(last_row_offsets, max_sample_count - sample_counts) - chunk.log_df(trace_label, 'inserts', inserts) del sample_counts chunk.log_df(trace_label, 'sample_counts', None) # insert the zero-prob utilities to pad each alternative set to same size padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) - del inserts - chunk.log_df(trace_label, 'inserts', None) del interaction_utilities chunk.log_df(trace_label, 'interaction_utilities', None) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 317381bf6..f6d7f991a 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -8,6 +8,7 @@ import time +import datetime import psutil import logging import gc @@ -40,15 +41,20 @@ def trace_memory_info(event=''): vmi = psutil.virtual_memory() if last_tick == 0: - MEM['baseline_tick'] = t - MEM['baseline_used'] = vmi.used mode = 'wb' if sys.version_info < (3,) else 'w' with open(config.output_file_path('mem.csv'), mode) as file: - print("seconds,delta_used,used,available,percent,event", file=file) + print("time,rss,used,available,percent,event", file=file) MEM['tick'] = t - baseline_tick = MEM['baseline_tick'] - baseline_used = MEM['baseline_used'] + + current_process = psutil.Process() + rss = current_process.memory_info().rss + for child in current_process.children(recursive=True): + try: + # + rss += child.memory_info().rss + except: + pass # logger.debug("memory_info: rss: %s available: %s percent: %s" # % (GB(mi.rss), GB(vmi.available), GB(vmi.percent))) @@ -57,8 +63,8 @@ def trace_memory_info(event=''): with open(config.output_file_path('mem.csv'), mode) as file: print("%s, %s, %s, %s, %s%%, %s" % - (int(t - baseline_tick), - GB(vmi.used - baseline_used), + (datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + GB(rss), GB(vmi.used), GB(vmi.available), vmi.percent, diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 63d2fcc09..5cb54f553 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -544,7 +544,7 @@ def idle(seconds): logger.error("Process %s failed with exitcode %s", p.name, p.exitcode) assert p.name in failed else: - logger.error("Process %s completed with exitcode %s", p.name, p.exitcode) + logger.info("Process %s completed with exitcode %s", p.name, p.exitcode) assert p.name in completed t0 = tracing.print_elapsed_time('run_sub_simulations step %s' % step_name, t0) From 65f69c61b1b55da42d5617a423092f01e6db6b19 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 31 Oct 2018 23:36:35 -0400 Subject: [PATCH 041/122] pycodestype bare except becomes except psutil.NoSuchProcess --- activitysim/core/mem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index f6d7f991a..c1dd8a9c4 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -51,9 +51,8 @@ def trace_memory_info(event=''): rss = current_process.memory_info().rss for child in current_process.children(recursive=True): try: - # rss += child.memory_info().rss - except: + except psutil.NoSuchProcess: pass # logger.debug("memory_info: rss: %s available: %s percent: %s" From dcb90cebbae4498e2e010c07972eff216964eb7e Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 1 Nov 2018 11:53:21 -0400 Subject: [PATCH 042/122] log_df log mem --- activitysim/core/chunk.py | 39 +++++++++++++++++++------------- activitysim/core/mem.py | 3 ++- activitysim/core/mp_tasks.py | 9 +++++++- activitysim/core/tracing.py | 5 ---- example_mp/configs/settings.yaml | 10 +++++--- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 5c9dd9d0b..66fe31dd6 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -30,13 +30,22 @@ MEM_HWM = [{}] -def GB(bytes, sign=False): - if bytes < (1024 * 1024): - return ("%+.2f KB" if sign else "%.2f KB") % (bytes / 1024) - elif bytes < (1024 * 1024 * 1024): - return ("%+.2f MB" if sign else "%.2f MB") % (bytes / (1024 * 1024)) - else: - return ("%+.2f GB" if sign else "%.2f GB") % (bytes / (1024 * 1024 * 1024)) +# def GB(bytes): +# if bytes < (1024 * 1024): +# return ("%.2f KB") % (bytes / 1024) +# elif bytes < (1024 * 1024 * 1024): +# return ("%.2f MB") % (bytes / (1024 * 1024)) +# else: +# return ("%.2f GB") % (bytes / (1024 * 1024 * 1024)) + +def GB(bytes): + # symbols = ('', 'K', 'M', 'G', 'T') + symbols = ('', ' KB', ' MB', ' GB', ' TB') + fmt = "%.1f%s" + for i, s in enumerate(symbols): + units = 1 << i * 10 + if bytes < units * 1024: + return fmt % (bytes / units, s) def log_open(trace_label, chunk_size): @@ -73,19 +82,18 @@ def log_close(trace_label): def log_df(trace_label, table_name, df): - # if df is None: - # mem.force_garbage_collect() - # return + if df is None: + mem.force_garbage_collect() cur_chunker = next(reversed(CHUNK_LOG)) + cur_mem = mem.get_memory_info() if df is None: CHUNK_LOG.get(cur_chunker).pop(table_name) op = 'del' - logger.debug("del %s df : %s " % (table_name, trace_label)) + logger.debug("log_df del %s df cur_mem: %s : %s " % (table_name, GB(cur_mem), trace_label)) - mem.force_garbage_collect() else: shape = df.shape @@ -105,11 +113,10 @@ def log_df(trace_label, table_name, df): CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes, shape) # log this df - logger.debug("add %s df %s %s %s : %s " % - (table_name, elements, shape, GB(bytes), trace_label)) + logger.debug("log_df add %s df %s %s bytes: %s mem: %s : %s " % + (table_name, elements, shape, GB(bytes), GB(cur_mem), trace_label)) - total_elements, total_bytes = _chunk_totals() - cur_mem = mem.get_memory_info() + total_elements, total_bytes = _chunk_totals() # new chunk totals # - check high_water_marks hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index c1dd8a9c4..4c46c445a 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -43,7 +43,7 @@ def trace_memory_info(event=''): if last_tick == 0: mode = 'wb' if sys.version_info < (3,) else 'w' with open(config.output_file_path('mem.csv'), mode) as file: - print("time,rss,used,available,percent,event", file=file) + print("time,rss,uss,available,percent,event", file=file) MEM['tick'] = t @@ -53,6 +53,7 @@ def trace_memory_info(event=''): try: rss += child.memory_info().rss except psutil.NoSuchProcess: + # print("NoSuchProcess %s %s" % (child.name(), child.status())) pass # logger.debug("memory_info: rss: %s available: %s percent: %s" diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 5cb54f553..837930cdb 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -559,7 +559,14 @@ def run_sub_task(p): t0 = tracing.print_elapsed_time() p.start() - p.join() + + while multiprocessing.active_children(): + mem.trace_memory_info() + time.sleep(1) + + # no need to join explicitly since multiprocessing.active_children joins completed procs + # p.join() + t0 = tracing.print_elapsed_time('sub_process %s' % p.name, t0) # logger.info('%s.exitcode = %s' % (p.name, p.exitcode)) diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index cb9acf524..8f762273c 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -34,11 +34,6 @@ logger = logging.getLogger(__name__) -def log_file_path(file_name): - # FIXME - for compatability with v0.7 - return config.log_file_path(file_name) - - def extend_trace_label(trace_label, extension): if trace_label: trace_label = "%s.%s" % (trace_label, extension) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 8039cafc0..703020547 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -11,10 +11,14 @@ inherit_settings: True #trace_od: None # full -#households_sample_size: 0 #multiprocess: True +#households_sample_size: 0 #chunk_size: 15000000000 #num_processes: 10 +## 50% sample +##households_sample_size: 1366361 +##chunk_size: 7500000000 +##num_processes: 5 #stagger: 30 #profile: False #strict: False @@ -23,7 +27,7 @@ inherit_settings: True # example #households_sample_size: 273272 -households_sample_size: 40000 +households_sample_size: 1000 multiprocess: False singleprocess_as_subtask: True @@ -83,7 +87,7 @@ multiprocess_steps: begin: initialize_landuse - name: mp_households begin: _school_location_sample - #num_processes: 10 + #num_processes: 9 #stagger: 30 #chunk_size: 1000000000 slice: From 4563a2388d932958ddb932b9248636bbfce0dff1 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 1 Nov 2018 23:50:16 -0400 Subject: [PATCH 043/122] better chunk logging --- activitysim/abm/models/trip_scheduling.py | 34 +++++++++++++++-- .../models/util/vectorize_tour_scheduling.py | 3 +- activitysim/core/chunk.py | 38 +++++++++++++------ .../core/interaction_sample_simulate.py | 13 +++++-- activitysim/core/interaction_simulate.py | 7 +++- activitysim/core/mem.py | 25 ++++++++---- activitysim/core/mp_tasks.py | 3 ++ example/configs/tour_scheduling_school.csv | 18 +++++++-- example/configs/tour_scheduling_work.csv | 17 +++++++-- example_mp/configs/settings.yaml | 5 +-- 10 files changed, 120 insertions(+), 43 deletions(-) diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 30c7abf82..2120eb396 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -211,6 +211,10 @@ def schedule_nth_trips( # left join trips to probs (there may be multiple rows per trip for multiple depart ranges) choosers = pd.merge(trips.reset_index(), probs_spec, on=probs_join_cols, how='left').set_index('trip_id') + chunk.log_df(trace_label, "choosers", choosers) + + if trace_hh_id and tracing.has_trace_targets(trips): + tracing.trace_df(choosers, '%s.choosers' % trace_label) # choosers should now match trips row for row assert choosers.index.is_unique @@ -219,6 +223,8 @@ def schedule_nth_trips( # zero out probs outside earliest-latest window chooser_probs = clip_probs(trips, choosers[probs_cols], model_settings) + chunk.log_df(trace_label, "chooser_probs", chooser_probs) + if first_trip_in_leg: # probs should sum to 1 unless all zero chooser_probs = chooser_probs.div(chooser_probs.sum(axis=1), axis=0).fillna(0) @@ -226,14 +232,26 @@ def schedule_nth_trips( # probs should sum to 1 with residual probs resulting in choice of 'fail' chooser_probs['fail'] = 1 - chooser_probs.sum(axis=1).clip(0, 1) + if trace_hh_id and tracing.has_trace_targets(trips): + tracing.trace_df(chooser_probs, '%s.chooser_probs' % trace_label) + choices, rands = logit.make_choices( chooser_probs, trace_label=trace_label, trace_choosers=choosers) + chunk.log_df(trace_label, "choices", choices) + chunk.log_df(trace_label, "rands", rands) + + if trace_hh_id and tracing.has_trace_targets(trips): + tracing.trace_df(choices, '%s.choices' % trace_label, columns=[None, 'depart']) + tracing.trace_df(rands, '%s.rands' % trace_label, columns=[None, 'rand']) + # convert alt choice index to depart time (setting failed choices to -1) failed = (choices == chooser_probs.columns.get_loc('fail')) choices = (choices + depart_alt_base).where(~failed, -1) + chunk.log_df(trace_label, "failed", failed) + # report failed trips while we have the best diagnostic info if report_failed_trips and failed.any(): report_bad_choices( @@ -245,8 +263,6 @@ def schedule_nth_trips( # trace before removing failures if trace_hh_id and tracing.has_trace_targets(trips): - tracing.trace_df(choosers, '%s.choosers' % trace_label) - tracing.trace_df(chooser_probs, '%s.chooser_probs' % trace_label) tracing.trace_df(choices, '%s.choices' % trace_label, columns=[None, 'depart']) tracing.trace_df(rands, '%s.rands' % trace_label, columns=[None, 'rand']) @@ -321,6 +337,8 @@ def schedule_trips_in_leg( nth_trace_label = tracing.extend_trace_label(trace_label, 'num_%s' % i) + chunk.log_open(nth_trace_label, chunk_size=0) + choices = schedule_nth_trips( nth_trips, probs_spec, @@ -330,6 +348,8 @@ def schedule_trips_in_leg( trace_hh_id=trace_hh_id, trace_label=nth_trace_label) + chunk.log_close(nth_trace_label) + # if outbound, this trip's depart constrains next trip's earliest depart option # if inbound, we are handling in reverse order, so it constrains latest depart instead ADJUST_NEXT_DEPART_COL = 'earliest' if outbound else 'latest' @@ -415,6 +435,8 @@ def run_trip_scheduling( else: chunk_trace_label = trace_label + leg_trace_label = tracing.extend_trace_label(chunk_trace_label, 'outbound') + chunk.log_open(leg_trace_label, chunk_size) choices = \ schedule_trips_in_leg( outbound=True, @@ -423,9 +445,12 @@ def run_trip_scheduling( model_settings=model_settings, last_iteration=last_iteration, trace_hh_id=trace_hh_id, - trace_label=tracing.extend_trace_label(chunk_trace_label, 'outbound')) + trace_label=leg_trace_label) result_list.append(choices) + chunk.log_close(leg_trace_label) + leg_trace_label = tracing.extend_trace_label(chunk_trace_label, 'inbound') + chunk.log_open(leg_trace_label, chunk_size) choices = \ schedule_trips_in_leg( outbound=False, @@ -434,8 +459,9 @@ def run_trip_scheduling( model_settings=model_settings, last_iteration=last_iteration, trace_hh_id=trace_hh_id, - trace_label=tracing.extend_trace_label(chunk_trace_label, 'inbound')) + trace_label=leg_trace_label) result_list.append(choices) + chunk.log_close(leg_trace_label) choices = pd.concat(result_list) diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index ffc904392..74d4da1eb 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -173,8 +173,7 @@ def _schedule_tours( assert not tours[window_id_col].duplicated().any() # merge previous tour columns (join on index) - previous_tour_info = get_previous_tour_by_tourid(tours[tour_owner_id_col], previous_tour, alts) - tours = tours.join(previous_tour_info) + tours = tours.join(get_previous_tour_by_tourid(tours[tour_owner_id_col], previous_tour, alts)) chunk.log_df(tour_trace_label, "tours", tours) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 66fe31dd6..8b81bd208 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) # dict of table_dicts keyed by trace_label -# table_dicts are dicts tuples of (elements, bytes, shape) keyed by table_name +# table_dicts are dicts tuples of (elements, bytes, mem) keyed by table_name CHUNK_LOG = OrderedDict() # array of chunk_size active CHUNK_LOG @@ -50,11 +50,13 @@ def GB(bytes): def log_open(trace_label, chunk_size): + # nested chunkers should be unchunked if len(CHUNK_LOG) > 0: assert chunk_size == 0 - logger.debug("log_open nested chunker %s" % trace_label) assert trace_label not in CHUNK_LOG + logger.debug("log_open chunker %s chunk_size %s" % (trace_label, chunk_size)) + CHUNK_LOG[trace_label] = OrderedDict() CHUNK_SIZE.append(chunk_size) @@ -69,7 +71,9 @@ def log_close(trace_label): logger.debug("log_close %s" % trace_label) - log_write_hwm() + # if we are closing base level chunker + if len(CHUNK_LOG) == 1: + log_write_hwm() label, _ = CHUNK_LOG.popitem(last=True) assert label == trace_label @@ -86,13 +90,12 @@ def log_df(trace_label, table_name, df): mem.force_garbage_collect() cur_chunker = next(reversed(CHUNK_LOG)) - cur_mem = mem.get_memory_info() if df is None: CHUNK_LOG.get(cur_chunker).pop(table_name) op = 'del' - logger.debug("log_df del %s df cur_mem: %s : %s " % (table_name, GB(cur_mem), trace_label)) + logger.debug("log_df del %s df : %s " % (table_name, trace_label)) else: @@ -110,16 +113,27 @@ def log_df(trace_label, table_name, df): logger.error("log_df %s unknown type: %s" % (table_name, type(df))) assert False - CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes, shape) + CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) # log this df - logger.debug("log_df add %s df %s %s bytes: %s mem: %s : %s " % - (table_name, elements, shape, GB(bytes), GB(cur_mem), trace_label)) + logger.debug("log_df add %s df %s %s bytes: %s : %s " % + (table_name, elements, shape, GB(bytes), trace_label)) total_elements, total_bytes = _chunk_totals() # new chunk totals + cur_mem = mem.get_memory_info() + hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) + + if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: + logger.warn("total_elements (%s) > chunk_size (%s) : %s " % + (total_elements, CHUNK_SIZE[0], hwm_trace_label)) + + logger.debug("total_elements: %s, total_bytes: %s cur_mem: %s: %s " % + (total_elements, GB(total_bytes), GB(cur_mem), hwm_trace_label)) + + mem.trace_memory_info(hwm_trace_label) # - check high_water_marks - hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) + for hwm in ELEMENTS_HWM: if total_elements > hwm.get('mark', 0): hwm['mark'] = total_elements @@ -146,7 +160,7 @@ def _chunk_totals(): for label in CHUNK_LOG: tables = CHUNK_LOG[label] for table_name in tables: - elements, bytes, shape = tables[table_name] + elements, bytes = tables[table_name] total_elements += elements total_bytes += bytes @@ -181,13 +195,13 @@ def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): rpc = min(rpc, num_choosers) # chunks = int(ceil(num_choosers / float(rpc))) - # effective_chunk_size = row_size * rpc + effective_chunk_size = row_size * rpc # # logger.debug("%s #chunk_calc chunk_size %s" % (trace_label, chunk_size)) # logger.debug("%s #chunk_calc num_choosers %s" % (trace_label, num_choosers)) # logger.debug("%s #chunk_calc total row_size %s" % (trace_label, row_size)) # logger.debug("%s #chunk_calc rows_per_chunk %s" % (trace_label, rpc)) - # logger.debug("%s #chunk_calc effective_chunk_size %s" % (trace_label, effective_chunk_size)) + logger.debug("%s #chunk_calc effective_chunk_size %s" % (trace_label, effective_chunk_size)) # logger.debug("%s #chunk_calc chunks %s" % (trace_label, chunks)) return rpc diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index ba0434f21..bc6855843 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -7,6 +7,8 @@ import logging +import gc + import numpy as np import pandas as pd @@ -109,10 +111,13 @@ def _interaction_sample_simulate( # so we just need to left join alternatives with choosers assert alternatives.index.name == choosers.index.name - interaction_df = pd.merge( - alternatives, choosers, - left_index=True, right_index=True, - suffixes=('', '_r')) + # interaction_df = pd.merge( + # alternatives, choosers, + # left_index=True, right_index=True, + # suffixes=('', '_r')) + + interaction_df = alternatives.join(choosers, how='left', rsuffix='_r') + chunk.log_df(trace_label, 'interaction_df', interaction_df) if have_trace_targets: diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 3afd49166..cd4941354 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -101,8 +101,7 @@ def to_series(x): for expr, coefficient in zip(spec.index, spec.iloc[:, 0]): try: - # fixme - remove this? (used only in trip_destination_sample.csv) - # allow temps of form _od_DIST@od_skim['DIST'] + # - allow temps of form _od_DIST@od_skim['DIST'] if expr.startswith('_'): target = expr[:expr.index('@')] rhs = expr[expr.index('@') + 1:] @@ -113,6 +112,8 @@ def to_series(x): if trace_eval_results is not None: trace_eval_results[expr] = v[trace_rows] + + # mem.trace_memory_info("eval_interaction_utilities TEMP: %s" % expr) continue if expr.startswith('@'): @@ -146,6 +147,8 @@ def to_series(x): logger.exception("Variable evaluation failed for: %s" % str(expr)) raise err + # mem.trace_memory_info("eval_interaction_utilities: %s" % expr) + if no_variability > 0: logger.warning("%s: %s columns have no variability" % (trace_label, no_variability)) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 4c46c445a..319fa2e04 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -18,8 +18,7 @@ logger = logging.getLogger(__name__) -MEM = {'tick': 0} -TICK_LEN = 5 +MEM = {} def force_garbage_collect(): @@ -30,20 +29,31 @@ def GB(bytes): return "%.2f" % (bytes / (1024 * 1024 * 1024.0)) +def init_trace(tick_len=5, file_name="mem.csv"): + MEM['tick'] = 0 + if file_name is not None: + MEM['file_name'] = file_name + if tick_len is not None: + MEM['tick_len'] = tick_len + + def trace_memory_info(event=''): + if not MEM: + return + last_tick = MEM['tick'] t = time.time() - if (t - last_tick < TICK_LEN) and not event: + if (t - last_tick < MEM['tick_len']) and not event: return vmi = psutil.virtual_memory() if last_tick == 0: mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path('mem.csv'), mode) as file: - print("time,rss,uss,available,percent,event", file=file) + with open(config.output_file_path(MEM['file_name']), mode) as file: + print("time,rss,used,available,percent,event", file=file) MEM['tick'] = t @@ -52,15 +62,14 @@ def trace_memory_info(event=''): for child in current_process.children(recursive=True): try: rss += child.memory_info().rss - except psutil.NoSuchProcess: - # print("NoSuchProcess %s %s" % (child.name(), child.status())) + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: pass # logger.debug("memory_info: rss: %s available: %s percent: %s" # % (GB(mi.rss), GB(vmi.available), GB(vmi.percent))) mode = 'ab' if sys.version_info < (3,) else 'a' - with open(config.output_file_path('mem.csv'), mode) as file: + with open(config.output_file_path(MEM['file_name']), mode) as file: print("%s, %s, %s, %s, %s%%, %s" % (datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"), diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 837930cdb..3c22f5088 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -399,6 +399,8 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): setup_injectables_and_logging(injectables) + mem.init_trace(file_name="mem_%s.csv" % multiprocessing.current_process().name) + if step_info['num_processes'] > 1: pipeline_prefix = multiprocessing.current_process().name logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) @@ -586,6 +588,7 @@ def drop_breadcrumb(step_name, crumb, value=True): def run_multiprocess(run_list, injectables): + mem.init_trace() mem.trace_memory_info("run_multiprocess.start") if not run_list['multiprocess']: diff --git a/example/configs/tour_scheduling_school.csv b/example/configs/tour_scheduling_school.csv index b58fc8065..aa0d0bfbe 100644 --- a/example/configs/tour_scheduling_school.csv +++ b/example/configs/tour_scheduling_school.csv @@ -23,10 +23,20 @@ School+work tours by worker- duration<6 hrs,work_and_school_and_student & (durat #,, Previously-scheduled tour ends in this departure hour,"@tt.previous_tour_ends(df.person_id, df.start)",-0.5995 Previously-scheduled tour begins in this arrival hour,"@tt.previous_tour_begins(df.person_id, df.end)",-1.102 -Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_before(df.person_id, df.start)",0.08975 -Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.003049 -Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_before(df.person_id, df.start)",-0.44 -Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.5271 +#,, +#,, FIXME - use temps as timetable ops can be very time-consuming +#Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_before(df.person_id, df.start)",0.08975 +#Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.003049 +#Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_before(df.person_id, df.start)",-0.44 +#Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.5271 +#,, +,"_adjacent_window_before@tt.adjacent_window_before(df.person_id, df.start)",1 +,"_adjacent_window_after@tt.adjacent_window_after(df.person_id, df.end)",1 +Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & _adjacent_window_before",0.08975 +Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & _adjacent_window_after",-0.003049 +Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & _adjacent_window_before",-0.44 +Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & _adjacent_window_after",-0.5271 +#,, Remaining work/school tours to be scheduled / number of unscheduled hours,"@((df.tour_count>1) & (df.tour_num == 1)) * 1.0 / tt.remaining_periods_available(df.person_id, df.start, df.end)",-16.67 #,, Departure Constants -- Early (up to 5),start < 6,-3.820662404 diff --git a/example/configs/tour_scheduling_work.csv b/example/configs/tour_scheduling_work.csv index 51624a5ac..f97452b7c 100644 --- a/example/configs/tour_scheduling_work.csv +++ b/example/configs/tour_scheduling_work.csv @@ -31,10 +31,19 @@ School+work tours by student- duration<8 hrs,(mandatory_tour_frequency == 'work_ #,, Previously-scheduled tour ends in this departure hour,"@tt.previous_tour_ends(df.person_id, df.start)",-0.8935 Previously-scheduled tour begins in this arrival hour,"@tt.previous_tour_begins(df.person_id, df.end)",-1.334 -Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_before(df.person_id, df.start)",0.1771 -Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_after(df.person_id, df.end)",0.3627 -Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_before(df.person_id, df.start)",-0.2123 -Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.1012 +#,, +#,, FIXME - use temps as timetable ops can be very time-consuming +#Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_before(df.person_id, df.start)",0.1771 +#Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & tt.adjacent_window_after(df.person_id, df.end)",0.3627 +#Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_before(df.person_id, df.start)",-0.2123 +#Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & tt.adjacent_window_after(df.person_id, df.end)",-0.1012 +,"_adjacent_window_before@tt.adjacent_window_before(df.person_id, df.start)",1 +,"_adjacent_window_after@tt.adjacent_window_after(df.person_id, df.end)",1 +Adjacent window exists before this departure hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & _adjacent_window_before",0.1771 +Adjacent window exists after this arrival hour - first tour interaction,"@(df.tour_count>1) & (df.tour_num == 1) & _adjacent_window_after",0.3627 +Adjacent window exists before this departure hour - second+ tour interaction,"@(df.tour_num > 1) & _adjacent_window_before",-0.2123 +Adjacent window exists after this arrival hour - second+ tour interaction,"@(df.tour_num > 1) & _adjacent_window_after",-0.1012 +#,, Remaining work/school tours to be scheduled / number of unscheduled hours,"@((df.tour_count>1) & (df.tour_num == 1)) * 1.0 / tt.remaining_periods_available(df.person_id, df.start, df.end)",-18.68 #,, Departure Constants -- Early (up to 5),start < 6,-0.95272527 diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 703020547..740853c29 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -26,8 +26,7 @@ inherit_settings: True #trace_od: # example -#households_sample_size: 273272 -households_sample_size: 1000 +households_sample_size: 273272 multiprocess: False singleprocess_as_subtask: True @@ -40,7 +39,7 @@ trace_hh_id: 1482966 trace_od: [5, 11] # to resume after last successful checkpoint, specify resume_after: _ -#resume_after: trip_purpose +#resume_after: mandatory_tour_frequency models: - initialize_landuse From 1c0abab5620d843a300a215fd3d3a32c67706b7f Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Sat, 3 Nov 2018 14:10:13 -0400 Subject: [PATCH 044/122] effective_chunk_size in chunk logging --- activitysim/abm/models/trip_purpose.py | 11 +- activitysim/abm/models/trip_scheduling.py | 17 +-- activitysim/abm/models/util/cdap.py | 10 +- .../models/util/vectorize_tour_scheduling.py | 18 ++- activitysim/core/chunk.py | 134 ++++++++++-------- activitysim/core/config.py | 46 +++--- activitysim/core/interaction_sample.py | 10 +- .../core/interaction_sample_simulate.py | 11 +- activitysim/core/interaction_simulate.py | 11 +- activitysim/core/mem.py | 49 ++++++- activitysim/core/mp_tasks.py | 28 ++-- activitysim/core/simulate.py | 23 ++- activitysim/core/skim.py | 3 + example_mp/configs/settings.yaml | 55 ++++--- 14 files changed, 225 insertions(+), 201 deletions(-) diff --git a/activitysim/abm/models/trip_purpose.py b/activitysim/abm/models/trip_purpose.py index 88ba8065c..c1fd5a8f5 100644 --- a/activitysim/abm/models/trip_purpose.py +++ b/activitysim/abm/models/trip_purpose.py @@ -36,8 +36,8 @@ def trip_purpose_rpc(chunk_size, choosers, spec, trace_label): num_choosers = len(choosers.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = len(choosers.columns) @@ -149,12 +149,9 @@ def run_trip_purpose( locals_dict=locals_dict, trace_label=trace_label) - rows_per_chunk = \ + rows_per_chunk, effective_chunk_size = \ trip_purpose_rpc(chunk_size, trips_df, probs_spec, trace_label=trace_label) - logger.info("%s rows_per_chunk %s num_choosers %s" % - (trace_label, rows_per_chunk, len(trips_df.index))) - for i, num_chunks, trips_chunk in chunk.chunked_choosers(trips_df, rows_per_chunk): logger.info("Running chunk %s of %s size %d", i, num_chunks, len(trips_chunk)) @@ -162,7 +159,7 @@ def run_trip_purpose( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = choose_intermediate_trip_purpose( trips_chunk, diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 2120eb396..d11a3e173 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -337,7 +337,7 @@ def schedule_trips_in_leg( nth_trace_label = tracing.extend_trace_label(trace_label, 'num_%s' % i) - chunk.log_open(nth_trace_label, chunk_size=0) + chunk.log_open(nth_trace_label, chunk_size=0, effective_chunk_size=0) choices = schedule_nth_trips( nth_trips, @@ -379,15 +379,14 @@ def schedule_trips_in_leg( return choices -# calc_rows_per_chunk(chunk_size, persons, by_chunk_id=True) def trip_scheduling_rpc(chunk_size, choosers, spec, trace_label): # NOTE we chunk chunk_id num_choosers = choosers['chunk_id'].max() + 1 # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 # extra columns from spec extra_columns = spec.shape[1] @@ -421,10 +420,8 @@ def run_trip_scheduling( set_tour_hour(trips, tours) - rows_per_chunk = trip_scheduling_rpc(chunk_size, trips, probs_spec, trace_label) - - # logger.info("%s rows_per_chunk %s num_choosers %s" % - # (trace_label, rows_per_chunk, len(trips.index))) + rows_per_chunk, effective_chunk_size = \ + trip_scheduling_rpc(chunk_size, trips, probs_spec, trace_label) result_list = [] for i, num_chunks, trips_chunk in chunk.chunked_choosers_by_chunk_id(trips, rows_per_chunk): @@ -436,7 +433,7 @@ def run_trip_scheduling( chunk_trace_label = trace_label leg_trace_label = tracing.extend_trace_label(chunk_trace_label, 'outbound') - chunk.log_open(leg_trace_label, chunk_size) + chunk.log_open(leg_trace_label, chunk_size, effective_chunk_size) choices = \ schedule_trips_in_leg( outbound=True, @@ -450,7 +447,7 @@ def run_trip_scheduling( chunk.log_close(leg_trace_label) leg_trace_label = tracing.extend_trace_label(chunk_trace_label, 'inbound') - chunk.log_open(leg_trace_label, chunk_size) + chunk.log_open(leg_trace_label, chunk_size, effective_chunk_size) choices = \ schedule_trips_in_leg( outbound=False, diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 14c599e44..1ca07f963 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -868,15 +868,14 @@ def _run_cdap( return cdap_results -# calc_rows_per_chunk(chunk_size, persons, by_chunk_id=True) def calc_rows_per_chunk(chunk_size, choosers, trace_label=None): # NOTE we chunk chunk_id num_choosers = choosers['chunk_id'].max() + 1 # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = choosers.shape[1] @@ -937,7 +936,8 @@ def run_cdap( trace_label = tracing.extend_trace_label(trace_label, 'cdap') - rows_per_chunk = calc_rows_per_chunk(chunk_size, persons, trace_label=trace_label) + rows_per_chunk, effective_chunk_size = \ + calc_rows_per_chunk(chunk_size, persons, trace_label=trace_label) result_list = [] # segment by person type and pick the right spec for each person type @@ -947,7 +947,7 @@ def run_cdap( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _run_cdap(persons_chunk, cdap_indiv_spec, diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index 74d4da1eb..d2a38d1f5 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -213,8 +213,8 @@ def calc_rows_per_chunk(chunk_size, tours, persons_merged, alternatives, trace_ num_choosers = len(tours.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = tours.shape[1] sample_size = alternatives.shape[0] @@ -227,10 +227,10 @@ def calc_rows_per_chunk(chunk_size, tours, persons_merged, alternatives, trace_ row_size = (chooser_row_size + extra_chooser_columns + alt_row_size) * sample_size - logger.debug("%s #chunk_calc choosers %s" % (trace_label, tours.shape)) - logger.debug("%s #chunk_calc extra_chooser_columns %s" % (trace_label, extra_chooser_columns)) - logger.debug("%s #chunk_calc alternatives %s" % (trace_label, alternatives.shape)) - logger.debug("%s #chunk_calc alt_row_size %s" % (trace_label, alt_row_size)) + # logger.debug("%s #chunk_calc choosers %s" % (trace_label, tours.shape)) + # logger.debug("%s #chunk_calc extra_chooser_columns %s" % (trace_label, extra_chooser_columns)) + # logger.debug("%s #chunk_calc alternatives %s" % (trace_label, alternatives.shape)) + # logger.debug("%s #chunk_calc alt_row_size %s" % (trace_label, alt_row_size)) return chunk.rows_per_chunk(chunk_size, row_size, num_choosers, trace_label) @@ -262,11 +262,9 @@ def schedule_tours( else: assert not tours[timetable_window_id_col].duplicated().any() - rows_per_chunk = \ + rows_per_chunk, effective_chunk_size = \ calc_rows_per_chunk(chunk_size, tours, persons_merged, alts, trace_label=tour_trace_label) - logger.info("chunk_size %s rows_per_chunk %s" % (chunk_size, rows_per_chunk)) - result_list = [] for i, num_chunks, chooser_chunk \ in chunk.chunked_choosers(tours, rows_per_chunk): @@ -276,7 +274,7 @@ def schedule_tours( chunk_trace_label = tracing.extend_trace_label(tour_trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else tour_trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _schedule_tours(chooser_chunk, persons_merged, alts, spec, constants, timetable, timetable_window_id_col, diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 8b81bd208..4abe72c25 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -24,20 +24,11 @@ # array of chunk_size active CHUNK_LOG CHUNK_SIZE = [] +EFFECTIVE_CHUNK_SIZE = [] -ELEMENTS_HWM = [{}] -BYTES_HWM = [{}] -MEM_HWM = [{}] +HWM = [{}] -# def GB(bytes): -# if bytes < (1024 * 1024): -# return ("%.2f KB") % (bytes / 1024) -# elif bytes < (1024 * 1024 * 1024): -# return ("%.2f MB") % (bytes / (1024 * 1024)) -# else: -# return ("%.2f GB") % (bytes / (1024 * 1024 * 1024)) - def GB(bytes): # symbols = ('', 'K', 'M', 'G', 'T') symbols = ('', ' KB', ' MB', ' GB', ' TB') @@ -48,21 +39,32 @@ def GB(bytes): return fmt % (bytes / units, s) -def log_open(trace_label, chunk_size): +def commas(x): + x = int(x) + if x < 10000: + return str(x) + result = '' + while x >= 1000: + x, r = divmod(x, 1000) + result = ",%03d%s" % (r, result) + return "%d%s" % (x, result) + + +def log_open(trace_label, chunk_size, effective_chunk_size): # nested chunkers should be unchunked if len(CHUNK_LOG) > 0: assert chunk_size == 0 assert trace_label not in CHUNK_LOG - logger.debug("log_open chunker %s chunk_size %s" % (trace_label, chunk_size)) + logger.debug("log_open chunker %s chunk_size %s effective_chunk_size %s" % + (trace_label, commas(chunk_size), commas(effective_chunk_size))) CHUNK_LOG[trace_label] = OrderedDict() CHUNK_SIZE.append(chunk_size) + EFFECTIVE_CHUNK_SIZE.append(effective_chunk_size) - ELEMENTS_HWM.append({}) - BYTES_HWM.append({}) - MEM_HWM.append({}) + HWM.append({}) def log_close(trace_label): @@ -78,10 +80,9 @@ def log_close(trace_label): label, _ = CHUNK_LOG.popitem(last=True) assert label == trace_label CHUNK_SIZE.pop() + EFFECTIVE_CHUNK_SIZE.pop() - ELEMENTS_HWM.pop() - BYTES_HWM.pop() - MEM_HWM.pop() + HWM.pop() def log_df(trace_label, table_name, df): @@ -95,7 +96,7 @@ def log_df(trace_label, table_name, df): CHUNK_LOG.get(cur_chunker).pop(table_name) op = 'del' - logger.debug("log_df del %s df : %s " % (table_name, trace_label)) + logger.debug("log_df del %s : %s " % (table_name, trace_label)) else: @@ -116,41 +117,27 @@ def log_df(trace_label, table_name, df): CHUNK_LOG.get(cur_chunker)[table_name] = (elements, bytes) # log this df - logger.debug("log_df add %s df %s %s bytes: %s : %s " % - (table_name, elements, shape, GB(bytes), trace_label)) + logger.debug("log_df add %s elements: %s bytes: %s shape: %s : %s " % + (table_name, commas(elements), GB(bytes), shape, trace_label)) total_elements, total_bytes = _chunk_totals() # new chunk totals cur_mem = mem.get_memory_info() hwm_trace_label = "%s.%s.%s" % (trace_label, op, table_name) - if CHUNK_SIZE[0] and total_elements > CHUNK_SIZE[0]: - logger.warn("total_elements (%s) > chunk_size (%s) : %s " % - (total_elements, CHUNK_SIZE[0], hwm_trace_label)) - - logger.debug("total_elements: %s, total_bytes: %s cur_mem: %s: %s " % - (total_elements, GB(total_bytes), GB(cur_mem), hwm_trace_label)) + # logger.debug("total_elements: %s, total_bytes: %s cur_mem: %s: %s " % + # (total_elements, GB(total_bytes), GB(cur_mem), hwm_trace_label)) mem.trace_memory_info(hwm_trace_label) # - check high_water_marks - for hwm in ELEMENTS_HWM: - if total_elements > hwm.get('mark', 0): - hwm['mark'] = total_elements - hwm['trace_label'] = hwm_trace_label - hwm['info'] = "bytes: %s mem: %s" % (GB(total_bytes), GB(cur_mem)) + info = "elements: %s bytes: %s mem: %s chunk_size: %s effective_chunk_size: %s" % \ + (commas(total_elements), GB(total_bytes), GB(cur_mem), + commas(CHUNK_SIZE[0]), commas(EFFECTIVE_CHUNK_SIZE[0])) - for hwm in BYTES_HWM: - if total_bytes > hwm.get('mark', 0): - hwm['mark'] = total_bytes - hwm['trace_label'] = hwm_trace_label - hwm['info'] = "elements: %s mem: %s" % (total_elements, GB(cur_mem)) - - for hwm in MEM_HWM: - if cur_mem > hwm.get('mark', 0): - hwm['mark'] = cur_mem - hwm['trace_label'] = hwm_trace_label - hwm['info'] = "elements: %s bytes: %s" % (total_elements, GB(total_bytes)) + check_hwm('elements', total_elements, info, hwm_trace_label) + check_hwm('bytes', total_bytes, info, hwm_trace_label) + check_hwm('mem', cur_mem, info, hwm_trace_label) def _chunk_totals(): @@ -167,29 +154,49 @@ def _chunk_totals(): return total_elements, total_bytes +def check_hwm(tag, value, info, trace_label): + + for d in HWM: + + hwm = d.setdefault(tag, {}) + + if value > hwm.get('mark', 0): + hwm['mark'] = value + hwm['info'] = info + hwm['trace_label'] = trace_label + + def log_write_hwm(): - hwm = ELEMENTS_HWM[-1] - if 'mark' in hwm: - logger.info("high_water_mark elements: %s (%s) in %s" % - (hwm['mark'], hwm['info'], hwm['trace_label']), ) - hwm = BYTES_HWM[-1] - if 'mark' in hwm: - logger.info("high_water_mark bytes: %s (%s) in %s" % - (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) + d = HWM[0] + for tag in d: + hwm = d[tag] + logger.info("high_water_mark %s: %s (%s) in %s" % + (tag, hwm['mark'], hwm['info'], hwm['trace_label']), ) - hwm = MEM_HWM[-1] - if 'mark' in hwm: - logger.info("high_water_mark mem: %s (%s) in %s" % - (GB(hwm['mark']), hwm['info'], hwm['trace_label']), ) + # - elements shouldn't exceed chunk_size or effective_chunk_size of base chunker + def check_chunk_size(hwm, chunk_size, label, max_leeway): + elements = hwm['mark'] + if chunk_size and max_leeway and elements > chunk_size * max_leeway: # too high + logger.warn("total_elements (%s) > %s (%s) %s : %s " % + (commas(elements), label, commas(chunk_size), + hwm['info'], hwm['trace_label'])) + # if we are in a chunker + if len(HWM) > 1 and HWM[1]: + assert 'elements' in HWM[1] # expect an 'elements' hwm dict for base chunker + hwm = HWM[1].get('elements') + check_chunk_size(hwm, EFFECTIVE_CHUNK_SIZE[0], 'effective_chunk_size', max_leeway=1) + check_chunk_size(hwm, CHUNK_SIZE[0], 'chunk_size', max_leeway=1) -def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): - # closest number of chooser rows to achieve chunk_size - rpc = int(round(chunk_size / float(row_size))) +def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): - rpc = int(chunk_size / float(row_size)) + if chunk_size > 0: + # closest number of chooser rows to achieve chunk_size without exceeding + rpc = int(chunk_size / float(row_size)) + else: + rpc = num_choosers rpc = max(rpc, 1) rpc = min(rpc, num_choosers) @@ -201,10 +208,13 @@ def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): # logger.debug("%s #chunk_calc num_choosers %s" % (trace_label, num_choosers)) # logger.debug("%s #chunk_calc total row_size %s" % (trace_label, row_size)) # logger.debug("%s #chunk_calc rows_per_chunk %s" % (trace_label, rpc)) - logger.debug("%s #chunk_calc effective_chunk_size %s" % (trace_label, effective_chunk_size)) + # logger.debug("%s #chunk_calc effective_chunk_size %s" % (trace_label, effective_chunk_size)) # logger.debug("%s #chunk_calc chunks %s" % (trace_label, chunks)) - return rpc + logger.info("%s #chunk_calc rows_per_chunk %s, effective_chunk_size %s, num_choosers %s" % + (trace_label, rpc, effective_chunk_size, num_choosers)) + + return rpc, effective_chunk_size def chunked_choosers(choosers, rows_per_chunk): diff --git a/activitysim/core/config.py b/activitysim/core/config.py index ebaa2a309..b4a322034 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -19,21 +19,21 @@ """ -@inject.injectable() +@inject.injectable(cache=True) def configs_dir(): if not os.path.exists('configs'): raise RuntimeError("configs_dir: directory does not exist") return 'configs' -@inject.injectable() +@inject.injectable(cache=True) def data_dir(): if not os.path.exists('data'): raise RuntimeError("data_dir: directory does not exist") return 'data' -@inject.injectable() +@inject.injectable(cache=True) def output_dir(): if not os.path.exists('output'): raise RuntimeError("output_dir: directory does not exist") @@ -45,11 +45,6 @@ def output_file_prefix(): return '' -@inject.injectable(cache=True) -def settings(): - return read_settings_file('settings.yaml', mandatory=True) - - @inject.injectable(cache=True) def pipeline_file_name(settings): @@ -72,6 +67,11 @@ def str2bool(v): raise argparse.ArgumentTypeError('Boolean value expected.') +@inject.injectable(cache=True) +def settings(): + return read_settings_file('settings.yaml', mandatory=True) + + def handle_standard_args(parser=None): """ Adds 'standard' activitysim arguments: @@ -115,34 +115,26 @@ def handle_standard_args(parser=None): if not os.path.exists(args.data): raise IOError("Could not find data dir '%s'" % args.data) inject.add_injectable("data_dir", args.data) - if args.resume: - inject.add_injectable('resume_after', args.resume) - if args.multiprocess: - inject.add_injectable('multiprocess', args.multiprocess) + + # - do these after potentially overriding configs_dir + if args.resume is not None: + override_setting('resume_after', args.resume) + if args.multiprocess is not None: + override_setting('multiprocess', args.multiprocess) return args -def setting(key, default=None): +def override_setting(key, value): settings = inject.get_injectable('settings') + settings[key] = value + inject.add_injectable('settings', settings) - # explicit setting in settings file takes precedence - s = settings.get(key, None) - - # if no setting, try injectable - if s is None: - s = inject.get_injectable(key, None) - if s: - # this happens when handle_standard_args overrides a setting with an injectable - logger.info("read setting %s from injectable" % key) - - # otherwise fall back to supplied default - if s is None: - s = default +def setting(key, default=None): - return s + return inject.get_injectable('settings').get(key, default) def read_model_settings(file_name, mandatory=False): diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 084562999..cf453053a 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -345,8 +345,8 @@ def calc_rows_per_chunk(chunk_size, choosers, alternatives, trace_label): num_choosers = choosers.shape[0] # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 # all columns from choosers chooser_row_size = choosers.shape[1] @@ -437,10 +437,8 @@ def interaction_sample( assert sample_size > 0 sample_size = min(sample_size, len(alternatives.index)) - rows_per_chunk = \ + rows_per_chunk, effective_chunk_size = \ calc_rows_per_chunk(chunk_size, choosers, alternatives, trace_label) - logger.info("interaction_sample chunk_size %s num_choosers %s rows_per_chunk %s" % - (chunk_size, choosers.shape[0], rows_per_chunk)) result_list = [] for i, num_chunks, chooser_chunk in chunk.chunked_choosers(choosers, rows_per_chunk): @@ -450,7 +448,7 @@ def interaction_sample( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _interaction_sample(chooser_chunk, alternatives, spec, sample_size, alt_col_name, allow_zero_probs, diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index bc6855843..599a14e0c 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -267,8 +267,8 @@ def calc_rows_per_chunk(chunk_size, choosers, alt_sample, spec, trace_label=None num_choosers = len(choosers.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = len(choosers.columns) @@ -342,12 +342,9 @@ def interaction_sample_simulate( trace_label = tracing.extend_trace_label(trace_label, 'interaction_sample_simulate') - rows_per_chunk = \ + rows_per_chunk, effective_chunk_size = \ calc_rows_per_chunk(chunk_size, choosers, alternatives, spec=spec, trace_label=trace_label) - logger.info("interaction_sample_simulate chunk_size %s num_choosers %s" - % (chunk_size, len(choosers.index))) - result_list = [] for i, num_chunks, chooser_chunk, alternative_chunk \ in chunk.chunked_choosers_and_alts(choosers, alternatives, rows_per_chunk): @@ -357,7 +354,7 @@ def interaction_sample_simulate( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _interaction_sample_simulate( chooser_chunk, alternative_chunk, spec, choice_column, diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index cd4941354..ef5824184 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -333,8 +333,8 @@ def calc_rows_per_chunk(chunk_size, choosers, alternatives, sample_size, skims, num_choosers = len(choosers.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = len(choosers.columns) @@ -414,14 +414,11 @@ def interaction_simulate( assert len(choosers) > 0 - rows_per_chunk = \ + rows_per_chunk, effective_chunk_size = \ calc_rows_per_chunk(chunk_size, choosers, alternatives=alternatives, sample_size=sample_size, skims=skims, trace_label=trace_label) - logger.info("interaction_simulate chunk_size %s num_choosers %s" % - (chunk_size, len(choosers.index))) - result_list = [] for i, num_chunks, chooser_chunk in chunk.chunked_choosers(choosers, rows_per_chunk): @@ -430,7 +427,7 @@ def interaction_simulate( chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _interaction_simulate(chooser_chunk, alternatives, spec, skims, locals_d, sample_size, diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 319fa2e04..888743dc6 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -19,6 +19,8 @@ logger = logging.getLogger(__name__) MEM = {} +HWM = {} +DEFAULT_TICK_LEN = 30 def force_garbage_collect(): @@ -26,16 +28,45 @@ def force_garbage_collect(): def GB(bytes): - return "%.2f" % (bytes / (1024 * 1024 * 1024.0)) + return (bytes / (1024 * 1024 * 1024.0)) -def init_trace(tick_len=5, file_name="mem.csv"): +def init_trace(tick_len=None, file_name="mem.csv"): MEM['tick'] = 0 if file_name is not None: MEM['file_name'] = file_name - if tick_len is not None: + if tick_len is None: + MEM['tick_len'] = DEFAULT_TICK_LEN + else: MEM['tick_len'] = tick_len + logger.info("init_trace file_name %s" % file_name) + + +def trace_hwm(tag, value, timestamp, label): + + hwm = HWM.setdefault(tag, {}) + + if value > hwm.get('mark', 0): + hwm['mark'] = value + hwm['timestamp'] = timestamp + hwm['label'] = label + + +def log_hwm(): + + for tag in HWM: + hwm = HWM[tag] + logger.info("high water mark %s: %s timestamp: %s label: %s" % + (tag, hwm['mark'], hwm['timestamp'], hwm['label'])) + + mode = 'ab' if sys.version_info < (3,) else 'a' + with open(config.output_file_path(MEM['file_name']), mode) as file: + for tag in HWM: + hwm = HWM[tag] + print("high water mark %s: %.2f timestamp: %s label: %s" % + (tag, hwm['mark'], hwm['timestamp'], hwm['label']), file=file) + def trace_memory_info(event=''): @@ -43,9 +74,10 @@ def trace_memory_info(event=''): return last_tick = MEM['tick'] + tick_len = MEM['tick_len'] or float('inf') t = time.time() - if (t - last_tick < MEM['tick_len']) and not event: + if (t - last_tick < tick_len) and not event: return vmi = psutil.virtual_memory() @@ -65,14 +97,19 @@ def trace_memory_info(event=''): except (psutil.NoSuchProcess, psutil.AccessDenied) as e: pass + timestamp = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + trace_hwm('rss', GB(rss), timestamp, event) + trace_hwm('used', GB(vmi.used), timestamp, event) + # logger.debug("memory_info: rss: %s available: %s percent: %s" # % (GB(mi.rss), GB(vmi.available), GB(vmi.percent))) mode = 'ab' if sys.version_info < (3,) else 'a' with open(config.output_file_path(MEM['file_name']), mode) as file: - print("%s, %s, %s, %s, %s%%, %s" % - (datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + print("%s, %.2f, %.2f, %.2f, %s%%, %s" % + (timestamp, GB(rss), GB(vmi.used), GB(vmi.available), diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 3c22f5088..f04e9335d 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -377,8 +377,6 @@ def run_simulation(queue, step_info, resume_after, skim_buffer): t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) - chunk.log_write_hwm() - pipeline.close_pipeline() @@ -399,7 +397,8 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): setup_injectables_and_logging(injectables) - mem.init_trace(file_name="mem_%s.csv" % multiprocessing.current_process().name) + mem.init_trace(setting('mem_tick'), + file_name="mem_%s.csv" % multiprocessing.current_process().name) if step_info['num_processes'] > 1: pipeline_prefix = multiprocessing.current_process().name @@ -412,6 +411,9 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): else: run_simulation(queue, step_info, resume_after, skim_buffer) + chunk.log_write_hwm() + mem.log_hwm() + def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): setup_injectables_and_logging(injectables) @@ -588,7 +590,7 @@ def drop_breadcrumb(step_name, crumb, value=True): def run_multiprocess(run_list, injectables): - mem.init_trace() + mem.init_trace(setting('mem_tick')) mem.trace_memory_info("run_multiprocess.start") if not run_list['multiprocess']: @@ -663,6 +665,8 @@ def find_breadcrumb(crumb, default=None): ) drop_breadcrumb(step_name, 'coalesce') + mem.log_hwm() + def get_breadcrumbs(run_list): @@ -729,11 +733,11 @@ def get_run_list(): logger.warning("Can't multiprocess because there is only 1 cpu") multiprocess = False - if not multiprocess and setting('singleprocess_as_subtask', False): - multiprocess_steps = [ - {'name': 'mp_simulation', 'begin': models[0]} - ] - multiprocess = True + # if not multiprocess and setting('singleprocess_as_subtask', False): + # multiprocess_steps = [ + # {'name': 'mp_simulation', 'begin': models[0]} + # ] + # multiprocess = True run_list = { 'models': models, @@ -784,9 +788,9 @@ def get_run_list(): if num_processes == 0: logger.info("Setting num_processes = %s for step %s", num_processes, name) num_processes = default_mp_processes - if num_processes == 1: - raise RuntimeError("num_processes = 1 but found slice info for step %s" - " in multiprocess_steps" % name) + # if num_processes == 1: + # raise RuntimeError("num_processes = 1 but found slice info for step %s" + # " in multiprocess_steps" % name) if num_processes > multiprocessing.cpu_count(): logger.warning("num_processes setting (%s) greater than cpu count (%s", num_processes, multiprocessing.cpu_count()) diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index 264eb8209..02f5be15c 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -755,8 +755,8 @@ def simple_simulate_rpc(chunk_size, choosers, spec, nest_spec, trace_label): num_choosers = len(choosers.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = len(choosers.columns) @@ -796,10 +796,8 @@ def simple_simulate(choosers, spec, nest_spec, skims=None, locals_d=None, assert len(choosers) > 0 - rows_per_chunk = simple_simulate_rpc(chunk_size, choosers, spec, nest_spec, trace_label) - - logger.info("simple_simulate rows_per_chunk %s num_choosers %s" % - (rows_per_chunk, len(choosers.index))) + rows_per_chunk, effective_chunk_size = \ + simple_simulate_rpc(chunk_size, choosers, spec, nest_spec, trace_label) result_list = [] # segment by person type and pick the right spec for each person type @@ -810,7 +808,7 @@ def simple_simulate(choosers, spec, nest_spec, skims=None, locals_d=None, chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) choices = _simple_simulate( chooser_chunk, spec, nest_spec, @@ -953,8 +951,8 @@ def simple_simulate_logsums_rpc(chunk_size, choosers, spec, nest_spec, trace_lab num_choosers = len(choosers.index) # if not chunking, then return num_choosers - if chunk_size == 0: - return num_choosers + # if chunk_size == 0: + # return num_choosers, 0 chooser_row_size = len(choosers.columns) @@ -994,9 +992,8 @@ def simple_simulate_logsums(choosers, spec, nest_spec, assert len(choosers) > 0 - rows_per_chunk = simple_simulate_logsums_rpc(chunk_size, choosers, spec, nest_spec, trace_label) - logger.info("%s chunk_size %s num_choosers %s, rows_per_chunk %s" % - (trace_label, chunk_size, len(choosers.index), rows_per_chunk)) + rows_per_chunk, effective_chunk_size = \ + simple_simulate_logsums_rpc(chunk_size, choosers, spec, nest_spec, trace_label) result_list = [] # segment by person type and pick the right spec for each person type @@ -1007,7 +1004,7 @@ def simple_simulate_logsums(choosers, spec, nest_spec, chunk_trace_label = tracing.extend_trace_label(trace_label, 'chunk_%s' % i) \ if num_chunks > 1 else trace_label - chunk.log_open(chunk_trace_label, chunk_size) + chunk.log_open(chunk_trace_label, chunk_size, effective_chunk_size) logsums = _simple_simulate_logsums( chooser_chunk, spec, nest_spec, diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index 9db4d8cd4..e3189e49c 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -142,6 +142,9 @@ def get(self, orig, dest): # # return out + # fixme - remove? + assert not (np.isnan(orig) | np.isnan(dest)).any() + # only working with numpy in here orig = np.asanyarray(orig).astype(int) dest = np.asanyarray(dest).astype(int) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 740853c29..81b07ddc4 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,45 +1,42 @@ inherit_settings: True -#households_sample_size: 0 -#multiprocess: False -#profile: False -#strict: False -#chunk_size: 0 -#check_for_variability: False -#trace_hh_id: None -#trace_od: None - -# full +# - production config #multiprocess: True -#households_sample_size: 0 -#chunk_size: 15000000000 #num_processes: 10 -## 50% sample -##households_sample_size: 1366361 -##chunk_size: 7500000000 -##num_processes: 5 #stagger: 30 +#mem_tick: 30 #profile: False #strict: False -#trace_hh_id: -#trace_od: -# example -households_sample_size: 273272 +# - dev config +multiprocess: True +num_processes: 1 +stagger: 30 +mem_tick: 5 +profile: False +strict: False -multiprocess: False -singleprocess_as_subtask: True +# - full sample +#households_sample_size: 0 +#chunk_size: 15000000000 + +# - 50% sample +#households_sample_size: 1366361 +#chunk_size: 7500000000 + +# - 10% sample +households_sample_size: 273272 chunk_size: 1500000000 -num_processes: 3 -stagger: 5 -profile: False -strict: True -trace_hh_id: 1482966 -trace_od: [5, 11] + +# - tracing +trace_hh_id: +trace_od: +#trace_hh_id: 1482966 +#trace_od: [5, 11] # to resume after last successful checkpoint, specify resume_after: _ -#resume_after: mandatory_tour_frequency +#resume_after: trip_purpose_and_destination models: - initialize_landuse From 5c0e3d9edd219518724b577ddbfa9fdf2bfee88e Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 6 Nov 2018 09:20:03 -0500 Subject: [PATCH 045/122] output_dir log subdir --- .../abm/models/atwork_subtour_scheduling.py | 2 +- activitysim/core/chunk.py | 23 ++++------- activitysim/core/config.py | 21 +++++++++- activitysim/core/mem.py | 17 ++++---- activitysim/core/mp_tasks.py | 9 ++-- activitysim/core/steps/output.py | 30 +++++++------- activitysim/core/tracing.py | 41 ++++++++++--------- example_mp/configs/settings.yaml | 17 ++++++-- example_mp/simulation.py | 21 +++++++++- 9 files changed, 111 insertions(+), 70 deletions(-) diff --git a/activitysim/abm/models/atwork_subtour_scheduling.py b/activitysim/abm/models/atwork_subtour_scheduling.py index 48bd5a536..77d31b46f 100644 --- a/activitysim/abm/models/atwork_subtour_scheduling.py +++ b/activitysim/abm/models/atwork_subtour_scheduling.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -DUMP = True +DUMP = False @inject.step() diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 4abe72c25..fd03390b7 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) # dict of table_dicts keyed by trace_label -# table_dicts are dicts tuples of (elements, bytes, mem) keyed by table_name +# table_dicts are dicts tuples of {table_name: (elements, bytes, mem), ...} CHUNK_LOG = OrderedDict() # array of chunk_size active CHUNK_LOG @@ -171,14 +171,14 @@ def log_write_hwm(): d = HWM[0] for tag in d: hwm = d[tag] - logger.info("high_water_mark %s: %s (%s) in %s" % + logger.info("#chunk_hwm high_water_mark %s: %s (%s) in %s" % (tag, hwm['mark'], hwm['info'], hwm['trace_label']), ) # - elements shouldn't exceed chunk_size or effective_chunk_size of base chunker def check_chunk_size(hwm, chunk_size, label, max_leeway): elements = hwm['mark'] if chunk_size and max_leeway and elements > chunk_size * max_leeway: # too high - logger.warn("total_elements (%s) > %s (%s) %s : %s " % + logger.warn("#chunk_hwm total_elements (%s) > %s (%s) %s : %s " % (commas(elements), label, commas(chunk_size), hwm['info'], hwm['trace_label'])) @@ -186,7 +186,7 @@ def check_chunk_size(hwm, chunk_size, label, max_leeway): if len(HWM) > 1 and HWM[1]: assert 'elements' in HWM[1] # expect an 'elements' hwm dict for base chunker hwm = HWM[1].get('elements') - check_chunk_size(hwm, EFFECTIVE_CHUNK_SIZE[0], 'effective_chunk_size', max_leeway=1) + check_chunk_size(hwm, EFFECTIVE_CHUNK_SIZE[0], 'effective_chunk_size', max_leeway=1.05) check_chunk_size(hwm, CHUNK_SIZE[0], 'chunk_size', max_leeway=1) @@ -203,16 +203,11 @@ def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): # chunks = int(ceil(num_choosers / float(rpc))) effective_chunk_size = row_size * rpc - # - # logger.debug("%s #chunk_calc chunk_size %s" % (trace_label, chunk_size)) - # logger.debug("%s #chunk_calc num_choosers %s" % (trace_label, num_choosers)) - # logger.debug("%s #chunk_calc total row_size %s" % (trace_label, row_size)) - # logger.debug("%s #chunk_calc rows_per_chunk %s" % (trace_label, rpc)) - # logger.debug("%s #chunk_calc effective_chunk_size %s" % (trace_label, effective_chunk_size)) - # logger.debug("%s #chunk_calc chunks %s" % (trace_label, chunks)) - - logger.info("%s #chunk_calc rows_per_chunk %s, effective_chunk_size %s, num_choosers %s" % - (trace_label, rpc, effective_chunk_size, num_choosers)) + num_chunks = (num_choosers // rpc) + (num_choosers % rpc > 0) + + logger.info("#chunk_calc num_chunks: %s, rows_per_chunk: %s, " + "effective_chunk_size: %s, num_choosers: %s : %s" % + (num_chunks, rpc, effective_chunk_size, num_choosers, trace_label)) return rpc, effective_chunk_size diff --git a/activitysim/core/config.py b/activitysim/core/config.py index b4a322034..2e5444805 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -8,6 +8,7 @@ import argparse import os import yaml +import sys import logging from activitysim.core import inject @@ -230,6 +231,7 @@ def trace_file_path(file_name): output_dir = inject.get_injectable('output_dir') + # - check for optional trace subfolder if os.path.exists(os.path.join(output_dir, 'trace')): output_dir = os.path.join(output_dir, 'trace') else: @@ -241,8 +243,25 @@ def trace_file_path(file_name): def log_file_path(file_name): + output_dir = inject.get_injectable('output_dir') + + # - check for optional log subfolder + if os.path.exists(os.path.join(output_dir, 'log')): + output_dir = os.path.join(output_dir, 'log') + + # - check for optional process name prefix prefix = inject.get_injectable('log_file_prefix', None) - return build_output_file_path(file_name, use_prefix=prefix) + if prefix: + file_name = "%s-%s" % (prefix, file_name) + + file_path = os.path.join(output_dir, file_name) + + return file_path + + +def open_log_file(file_name, mode): + mode = mode + 'b' if sys.version_info < (3,) else mode + return open(log_file_path(file_name), mode) def pipeline_file_path(file_name): diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 888743dc6..30a3f3a53 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -12,7 +12,7 @@ import psutil import logging import gc -import sys + from activitysim.core import config @@ -60,12 +60,11 @@ def log_hwm(): logger.info("high water mark %s: %s timestamp: %s label: %s" % (tag, hwm['mark'], hwm['timestamp'], hwm['label'])) - mode = 'ab' if sys.version_info < (3,) else 'a' - with open(config.output_file_path(MEM['file_name']), mode) as file: + with config.open_log_file(MEM['file_name'], 'a') as log_file: for tag in HWM: hwm = HWM[tag] print("high water mark %s: %.2f timestamp: %s label: %s" % - (tag, hwm['mark'], hwm['timestamp'], hwm['label']), file=file) + (tag, hwm['mark'], hwm['timestamp'], hwm['label']), file=log_file) def trace_memory_info(event=''): @@ -83,9 +82,8 @@ def trace_memory_info(event=''): vmi = psutil.virtual_memory() if last_tick == 0: - mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path(MEM['file_name']), mode) as file: - print("time,rss,used,available,percent,event", file=file) + with config.open_log_file(MEM['file_name'], 'w') as log_file: + print("time,rss,used,available,percent,event", file=log_file) MEM['tick'] = t @@ -105,8 +103,7 @@ def trace_memory_info(event=''): # logger.debug("memory_info: rss: %s available: %s percent: %s" # % (GB(mi.rss), GB(vmi.available), GB(vmi.percent))) - mode = 'ab' if sys.version_info < (3,) else 'a' - with open(config.output_file_path(MEM['file_name']), mode) as file: + with config.open_log_file(MEM['file_name'], 'a') as output_file: print("%s, %.2f, %.2f, %.2f, %s%%, %s" % (timestamp, @@ -114,7 +111,7 @@ def trace_memory_info(event=''): GB(vmi.used), GB(vmi.available), vmi.percent, - event), file=file) + event), file=output_file) def get_memory_info(): diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index f04e9335d..f529bacfc 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -397,12 +397,11 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): setup_injectables_and_logging(injectables) - mem.init_trace(setting('mem_tick'), - file_name="mem_%s.csv" % multiprocessing.current_process().name) + mem.init_trace(setting('mem_tick')) if step_info['num_processes'] > 1: pipeline_prefix = multiprocessing.current_process().name - logger.info("injecting pipeline_file_prefix '%s'", pipeline_prefix) + logger.debug("injecting pipeline_file_prefix '%s'", pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) if setting('profile', False): @@ -875,8 +874,8 @@ def get_run_list(): multiprocess_steps[istep]['resume_after'] = resume_after # - write run list to output dir - mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path('run_list.txt'), mode) as f: + # use log_file_path so we use (optional) log subdir and prefix process name + with config.open_log_file('run_list.txt', 'w') as f: print_run_list(run_list, f) return run_list diff --git a/activitysim/core/steps/output.py b/activitysim/core/steps/output.py index 6677d459a..6182729b5 100644 --- a/activitysim/core/steps/output.py +++ b/activitysim/core/steps/output.py @@ -41,39 +41,39 @@ def track_skim_usage(output_dir): skim_stack = inject.get_injectable('skim_stack', None) mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path('skim_usage.txt'), mode) as file: + with open(config.output_file_path('skim_usage.txt'), mode) as output_file: - print("\n### skim_dict usage", file=file) + print("\n### skim_dict usage", file=output_file) for key in skim_dict.usage: - print(key, file=file) + print(key, file=output_file) if skim_stack is None: unused_keys = {k for k in skim_dict.skim_info['omx_keys']} - \ {k for k in skim_dict.usage} - print("\n### unused skim keys", file=file) + print("\n### unused skim keys", file=output_file) for key in unused_keys: - print(key, file=file) + print(key, file=output_file) else: - print("\n### skim_stack usage", file=file) + print("\n### skim_stack usage", file=output_file) for key in skim_stack.usage: - print(key, file=file) + print(key, file=output_file) unused = {k for k in skim_dict.skim_info['omx_keys'] if not isinstance(k, tuple)} - \ {k for k in skim_dict.usage if not isinstance(k, tuple)} - print("\n### unused skim str keys", file=file) + print("\n### unused skim str keys", file=output_file) for key in unused: - print(key, file=file) + print(key, file=output_file) unused = {k[0] for k in skim_dict.skim_info['omx_keys'] if isinstance(k, tuple)} - \ {k[0] for k in skim_dict.usage if isinstance(k, tuple)} - \ {k for k in skim_stack.usage} - print("\n### unused skim dim3 keys", file=file) + print("\n### unused skim dim3 keys", file=output_file) for key in unused: - print(key, file=file) + print(key, file=output_file) def write_data_dictionary(output_dir): @@ -93,13 +93,13 @@ def write_data_dictionary(output_dir): # write data dictionary for all checkpointed_tables mode = 'wb' if sys.version_info < (3,) else 'w' - with open(config.output_file_path('data_dict.txt'), mode) as file: + with open(config.output_file_path('data_dict.txt'), mode) as output_file: for table_name in output_tables: df = inject.get_table(table_name, None).to_frame() - print("\n### %s %s" % (table_name, df.shape), file=file) - print('index:', df.index.name, df.index.dtype, file=file) - print(df.dtypes, file=file) + print("\n### %s %s" % (table_name, df.shape), file=output_file) + print('index:', df.index.name, df.index.dtype, file=output_file) + print(df.dtypes, file=output_file) # def xwrite_data_dictionary(output_dir): diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 8f762273c..1d7968114 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -73,30 +73,33 @@ def delete_output_files(file_type, ignore=None, subdir=None): output_dir = inject.get_injectable('output_dir') - if subdir: - output_dir = os.path.join(output_dir, subdir) - if not os.path.exists(output_dir): - logger.warn("delete_output_files: No subdirectory %s" % (file_type, output_dir)) - return + directories = ['', 'log', 'trace'] + + for subdir in directories: + + dir = os.path.join(output_dir, subdir) if subdir else output_dir + + if not os.path.exists(dir): + continue - if ignore: - ignore = [os.path.realpath(p) for p in ignore] + if ignore: + ignore = [os.path.realpath(p) for p in ignore] - logger.debug("Deleting %s files in output_dir %s" % (file_type, output_dir)) + # logger.debug("Deleting %s files in output dir %s" % (file_type, dir)) - for the_file in os.listdir(output_dir): - if the_file.endswith(file_type): - file_path = os.path.join(output_dir, the_file) + for the_file in os.listdir(dir): + if the_file.endswith(file_type): + file_path = os.path.join(dir, the_file) - if ignore and os.path.realpath(file_path) in ignore: - logger.info("delete_output_files ignoring %s" % file_path) - continue + if ignore and os.path.realpath(file_path) in ignore: + logger.debug("delete_output_files ignoring %s" % file_path) + continue - try: - if os.path.isfile(file_path): - os.unlink(file_path) - except Exception as e: - print(e) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception as e: + print(e) def delete_csv_files(): diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 81b07ddc4..9cd7c8a49 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -11,9 +11,9 @@ inherit_settings: True # - dev config multiprocess: True -num_processes: 1 +num_processes: 3 stagger: 30 -mem_tick: 5 +mem_tick: 30 profile: False strict: False @@ -25,9 +25,18 @@ strict: False #households_sample_size: 1366361 #chunk_size: 7500000000 +# - 20% sample +#households_sample_size: 546544 +#chunk_size: 3000000000 + # - 10% sample -households_sample_size: 273272 -chunk_size: 1500000000 +#households_sample_size: 273272 +#chunk_size: 1500000000 + +# - 3.3% sample +households_sample_size: 91091 +chunk_size: 500000000 + # - tracing trace_hh_id: diff --git a/example_mp/simulation.py b/example_mp/simulation.py index a09c4ca21..532f13e43 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -30,7 +30,6 @@ def cleanup_output_files(): tracing.delete_output_files('h5') tracing.delete_output_files('csv') - tracing.delete_output_files('csv', subdir='trace') tracing.delete_output_files('txt') tracing.delete_output_files('yaml') tracing.delete_output_files('prof') @@ -48,6 +47,24 @@ def run(run_list, injectables=None): chunk.log_write_hwm() +def log_settings(): + + settings = [ + 'households_sample_size', + 'chunk_size', + 'multiprocess', + 'num_processes', + 'resume_after', + ] + + for k in settings: + logger.info("setting %s: %s" % (k, config.setting(k))) + + injectables = ['data_dir', 'configs_dir', 'output_dir'] + for k in injectables: + logger.info("injectable %s: %s" % (k, inject.get_injectable(k))) + + if __name__ == '__main__': # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') @@ -59,6 +76,8 @@ def run(run_list, injectables=None): mp_tasks.filter_warnings() tracing.config_logger() + log_settings() + t0 = tracing.print_elapsed_time() # cleanup if not resuming From 4dd33880387d0009020ca42680769eb23d0e8041 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 14 Nov 2018 10:44:52 -0500 Subject: [PATCH 046/122] coalesce and slice --- .gitignore | 1 + activitysim/abm/misc.py | 15 +++++++++ activitysim/abm/tables/households.py | 28 +++++++++++++++- activitysim/abm/tables/persons.py | 6 +--- activitysim/core/config.py | 31 +++++++++++++++++- activitysim/core/mem.py | 2 +- activitysim/core/mp_tasks.py | 28 +++++++++------- activitysim/core/pipeline.py | 2 ++ example_mp/coalesce.py | 49 ++++++++++++++++++++++++++++ example_mp/configs/settings.yaml | 44 ++++++++++++++++++++++--- example_mp/output/log/.gitignore | 2 ++ 11 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 example_mp/coalesce.py create mode 100644 example_mp/output/log/.gitignore diff --git a/.gitignore b/.gitignore index 229e6dcac..6ccc5f8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ example/data/* .ipynb_checkpoints .coverage* .pytest_cache +.vagrant # Byte-compiled / optimized / DLL files diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index 6b91d1b16..ff321f82b 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -28,6 +28,21 @@ def households_sample_size(settings, override_hh_ids): return len(override_hh_ids) +@inject.injectable(cache=True) +def households_sample_stride(settings): + + stride = settings.get('households_sample_stride', None) + + if stride and not (isinstance(stride, list) and + len(stride) == 2 and + all(isinstance(x, int) for x in stride)): + logger.warning("setting households_sample_stride should be a list of length 2, but was %s" % + stride) + stride = None + + return stride + + @inject.injectable(cache=True) def override_hh_ids(settings): diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index 6840a8664..40e6eea9b 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -9,6 +9,7 @@ import logging import pandas as pd +import numpy as np from activitysim.core import tracing from activitysim.core import pipeline @@ -20,9 +21,10 @@ @inject.table() -def households(households_sample_size, override_hh_ids, trace_hh_id): +def households(households_sample_size, households_sample_stride, override_hh_ids, trace_hh_id): df_full = read_input_table("households") + households_sliced = False # only using households listed in override_hh_ids if override_hh_ids is not None: @@ -31,6 +33,7 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): logger.info("override household list containing %s households" % len(override_hh_ids)) df = df_full[df_full.index.isin(override_hh_ids)] + households_sliced = True if df.shape[0] < len(override_hh_ids): logger.info("found %s of %s households in override household list" % @@ -44,6 +47,7 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): # df contains only trace_hh (or empty if not in full store) df = tracing.slice_ids(df_full, 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: @@ -62,6 +66,7 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): prng = pipeline.get_rn_generator().get_external_rng('sample_households') df = df_full.take(prng.choice(len(df_full), size=households_sample_size, replace=False)) + households_sliced = True # if tracing and we missed trace_hh in sample, but it is in full store if trace_hh_id and trace_hh_id not in df.index and trace_hh_id in df_full.index: @@ -74,6 +79,27 @@ def households(households_sample_size, override_hh_ids, trace_hh_id): else: df = df_full + if households_sample_stride: + + # - possibly resampling + if households_sliced: + df_full = df + if override_hh_ids is not None: + logger.warning("Applying stride slice to override_hh_ids households.") + if households_sample_size > 0: + logger.warning("Applying stride slice to sampled households.") + + stride_len, offset, = households_sample_stride + + df = df_full[np.asanyarray(list(range(df_full.shape[0]))) % stride_len == offset] + households_sliced = True + + logger.info("stride (%s,%s) sliced %s of %s households" % + (stride_len, offset, df.shape[0], df_full.shape[0])) + + # persons table + inject.add_injectable('households_sliced', households_sliced) + logger.info("loaded households %s" % (df.shape,)) df.index.name = 'household_id' diff --git a/activitysim/abm/tables/persons.py b/activitysim/abm/tables/persons.py index 330f9d8d9..4cf84e076 100644 --- a/activitysim/abm/tables/persons.py +++ b/activitysim/abm/tables/persons.py @@ -18,13 +18,9 @@ def read_raw_persons(households): - # we only use these to know whether we need to slice persons - households_sample_size = inject.get_injectable('households_sample_size') - override_hh_ids = inject.get_injectable('override_hh_ids') - df = read_input_table("persons") - if (households_sample_size > 0) or override_hh_ids: + if inject.get_injectable('households_sliced', False): # keep all persons in the sampled households df = df[df.household_id.isin(households.index)] diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 2e5444805..5e988117c 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -49,7 +49,7 @@ def output_file_prefix(): @inject.injectable(cache=True) def pipeline_file_name(settings): - pipeline_file_name = settings.get('pipeline', 'pipeline.h5') + pipeline_file_name = settings.get('pipeline_file_name', 'pipeline.h5') return pipeline_file_name @@ -68,6 +68,25 @@ def str2bool(v): raise argparse.ArgumentTypeError('Boolean value expected.') +def str2stride(v): + + try: + stride_len, offset = v.split(',', 1) + stride_len = int(stride_len) + offset = int(offset) + + except Exception as err: + raise argparse.ArgumentTypeError('int list of length two expected.') + + if stride_len == 0: + raise argparse.ArgumentTypeError('stride_len cannot be 0.') + + if offset >= stride_len: + raise argparse.ArgumentTypeError('offset cannot be greater than stride_len.') + + return [stride_len, offset] + + @inject.injectable(cache=True) def settings(): return read_settings_file('settings.yaml', mandatory=True) @@ -101,6 +120,10 @@ def handle_standard_args(parser=None): parser.add_argument("-m", "--multiprocess", type=str2bool, nargs='?', const=True, help="run multiprocess (boolean flag, no arg defaults to true)") + parser.add_argument("-s", "--stride", type=str2stride, + help="households_sample_stride stride_len and offset -e.g. --stride=4,0") + parser.add_argument("-p", "--pipeline", help="pipeline file name") + args = parser.parse_args() if args.config: @@ -117,6 +140,12 @@ def handle_standard_args(parser=None): raise IOError("Could not find data dir '%s'" % args.data) inject.add_injectable("data_dir", args.data) + # FIXME - should these be settings? + if args.stride: + inject.add_injectable("households_sample_stride", args.stride) + if args.pipeline: + inject.add_injectable("pipeline_file_name", args.pipeline) + # - do these after potentially overriding configs_dir if args.resume is not None: override_setting('resume_after', args.resume) diff --git a/activitysim/core/mem.py b/activitysim/core/mem.py index 30a3f3a53..7c2dcfe51 100644 --- a/activitysim/core/mem.py +++ b/activitysim/core/mem.py @@ -57,7 +57,7 @@ def log_hwm(): for tag in HWM: hwm = HWM[tag] - logger.info("high water mark %s: %s timestamp: %s label: %s" % + logger.info("high water mark %s: %.2f timestamp: %s label: %s" % (tag, hwm['mark'], hwm['timestamp'], hwm['label'])) with config.open_log_file(MEM['file_name'], 'a') as log_file: diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index f529bacfc..8b3a4a3c1 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -234,7 +234,20 @@ def apportion_pipeline(sub_job_names, slice_info): pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] = checkpoints_df -def coalesce_pipelines(sub_process_names, slice_info): +def coalesce_pipelines(sub_process_names, slice_info, use_prefix=True): + + def sub_pipeline_path(name): + if use_prefix: + # name is prefix + path = config.build_output_file_path(pipeline_file_name, use_prefix=name) + elif os.path.exists(name): + # check if name is valid path + path = name + else: + # otherwise expect to find it on output dir + path = config.build_output_file_path(name) + + return path pipeline_file_name = inject.get_injectable('pipeline_file_name') @@ -245,8 +258,7 @@ def coalesce_pipelines(sub_process_names, slice_info): tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) # read all tables from first process pipeline - pipeline_path = \ - config.build_output_file_path(pipeline_file_name, use_prefix=sub_process_names[0]) + pipeline_path = sub_pipeline_path(sub_process_names[0]) with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: # hdf5_keys is a dict mapping table_name to pipeline hdf5_key @@ -269,7 +281,7 @@ def coalesce_pipelines(sub_process_names, slice_info): # concat omnibus tables from all sub_processes omnibus_tables = {table_name: [] for table_name in omnibus_keys} for process_name in sub_process_names: - pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) + pipeline_path = sub_pipeline_path(process_name) logger.info("coalesce pipeline %s", pipeline_path) with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: @@ -730,13 +742,6 @@ def get_run_list(): if multiprocess and multiprocessing.cpu_count() == 1: logger.warning("Can't multiprocess because there is only 1 cpu") - multiprocess = False - - # if not multiprocess and setting('singleprocess_as_subtask', False): - # multiprocess_steps = [ - # {'name': 'mp_simulation', 'begin': models[0]} - # ] - # multiprocess = True run_list = { 'models': models, @@ -870,6 +875,7 @@ def get_run_list(): if breadcrumbs: run_list['breadcrumbs'] = breadcrumbs # - add resume_after to resume_step + # FIXME - are we assuming it is in last step? istep = len(breadcrumbs) - 1 multiprocess_steps[istep]['resume_after'] = resume_after diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index aa6becd04..6eb6bcf4f 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -348,6 +348,8 @@ def load_checkpoint(checkpoint_name): checkpoints = checkpoints.loc[:i] except IndexError: msg = "Couldn't find checkpoint '%s' in checkpoints" % (checkpoint_name,) + print(checkpoints[CHECKPOINT_NAME]) + bug logger.error(msg) raise RuntimeError(msg) diff --git a/example_mp/coalesce.py b/example_mp/coalesce.py new file mode 100644 index 000000000..8c20e5078 --- /dev/null +++ b/example_mp/coalesce.py @@ -0,0 +1,49 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +import sys +import logging +import argparse +import os + +from activitysim.core import mem +from activitysim.core import inject +from activitysim.core import tracing +from activitysim.core import config +from activitysim.core import pipeline +from activitysim.core import mp_tasks +from activitysim.core import chunk + +# from activitysim import abm + + +logger = logging.getLogger('activitysim') + + +if __name__ == '__main__': + + + inject.add_injectable('configs_dir', ['configs', '../example/configs']) + + config.handle_standard_args() + + mp_tasks.filter_warnings() + tracing.config_logger() + + t0 = tracing.print_elapsed_time() + + coalesce_rules = config.setting('coalesce') + + #run_list = mp_tasks.get_run_list() + + mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) + + checkpoints_df = pipeline.get_checkpoints() + file_path = config.output_file_path('coalesce_checkpoints.csv') + checkpoints_df.to_csv(file_path, index=True) + + t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 9cd7c8a49..a6b499069 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -3,19 +3,36 @@ inherit_settings: True # - production config #multiprocess: True +#mem_tick: 30 +#profile: False +#strict: False #num_processes: 10 #stagger: 30 + +# azure 432GB 64 processors +#multiprocess: True +#mem_tick: 30 +#profile: False +#strict: False +#num_processes: 30 +#stagger: 15 + +# - dev config 3% +#multiprocess: True #mem_tick: 30 #profile: False #strict: False +#num_processes: 3 +#stagger: 30 -# - dev config +# - dev config mini multiprocess: True -num_processes: 3 -stagger: 30 mem_tick: 30 profile: False strict: False +num_processes: 3 +stagger: 5 + # - full sample #households_sample_size: 0 @@ -34,9 +51,16 @@ strict: False #chunk_size: 1500000000 # - 3.3% sample -households_sample_size: 91091 -chunk_size: 500000000 +#households_sample_size: 91091 +#chunk_size: 500000000 +# - mini sample +#households_sample_size: 300 +households_sample_size: 200 +#households_sample_stride: [3, 0] + + +chunk_size: 0 # - tracing trace_hh_id: @@ -138,3 +162,13 @@ output_tables: # - persons # - trips # - tours + + +coalesce: + names: + - pipeline_0.h5 + - pipeline_1.h5 + slice: + tables: + - households + - persons diff --git a/example_mp/output/log/.gitignore b/example_mp/output/log/.gitignore new file mode 100644 index 000000000..031b1db6c --- /dev/null +++ b/example_mp/output/log/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.log From ba66a1bd3840a1825cd3a7f713ea092c6484cab9 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 14 Nov 2018 15:09:57 -0500 Subject: [PATCH 047/122] example_2mp --- example_2mp/coalesce.py | 49 +++++++++++ example_2mp/configs_report/logging.yaml | 68 +++++++++++++++ example_2mp/configs_report/settings.yaml | 43 ++++++++++ example_2mp/configs_sim/logging.yaml | 68 +++++++++++++++ example_2mp/configs_sim/settings.yaml | 92 ++++++++++++++++++++ example_2mp/output/.gitignore | 7 ++ example_2mp/output/log/.gitignore | 2 + example_2mp/output/trace/.gitignore | 1 + example_2mp/output0/.gitignore | 7 ++ example_2mp/output0/log/.gitignore | 2 + example_2mp/output0/trace/.gitignore | 1 + example_2mp/output1/.gitignore | 7 ++ example_2mp/output1/log/.gitignore | 2 + example_2mp/output1/trace/.gitignore | 1 + example_2mp/script.txt | 10 +++ example_2mp/simulation.py | 103 +++++++++++++++++++++++ 16 files changed, 463 insertions(+) create mode 100644 example_2mp/coalesce.py create mode 100644 example_2mp/configs_report/logging.yaml create mode 100644 example_2mp/configs_report/settings.yaml create mode 100644 example_2mp/configs_sim/logging.yaml create mode 100644 example_2mp/configs_sim/settings.yaml create mode 100644 example_2mp/output/.gitignore create mode 100644 example_2mp/output/log/.gitignore create mode 100644 example_2mp/output/trace/.gitignore create mode 100644 example_2mp/output0/.gitignore create mode 100644 example_2mp/output0/log/.gitignore create mode 100644 example_2mp/output0/trace/.gitignore create mode 100644 example_2mp/output1/.gitignore create mode 100644 example_2mp/output1/log/.gitignore create mode 100644 example_2mp/output1/trace/.gitignore create mode 100644 example_2mp/script.txt create mode 100644 example_2mp/simulation.py diff --git a/example_2mp/coalesce.py b/example_2mp/coalesce.py new file mode 100644 index 000000000..8c20e5078 --- /dev/null +++ b/example_2mp/coalesce.py @@ -0,0 +1,49 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +import sys +import logging +import argparse +import os + +from activitysim.core import mem +from activitysim.core import inject +from activitysim.core import tracing +from activitysim.core import config +from activitysim.core import pipeline +from activitysim.core import mp_tasks +from activitysim.core import chunk + +# from activitysim import abm + + +logger = logging.getLogger('activitysim') + + +if __name__ == '__main__': + + + inject.add_injectable('configs_dir', ['configs', '../example/configs']) + + config.handle_standard_args() + + mp_tasks.filter_warnings() + tracing.config_logger() + + t0 = tracing.print_elapsed_time() + + coalesce_rules = config.setting('coalesce') + + #run_list = mp_tasks.get_run_list() + + mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) + + checkpoints_df = pipeline.get_checkpoints() + file_path = config.output_file_path('coalesce_checkpoints.csv') + checkpoints_df.to_csv(file_path, index=True) + + t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_2mp/configs_report/logging.yaml b/example_2mp/configs_report/logging.yaml new file mode 100644 index 000000000..f326f3095 --- /dev/null +++ b/example_2mp/configs_report/logging.yaml @@ -0,0 +1,68 @@ +# Config for logging +# ------------------ +# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema + +logging: + version: 1 + disable_existing_loggers: true + + + # Configuring the default (root) logger is highly recommended + root: + level: DEBUG + handlers: [console, logfile] + + loggers: + + tasks: + level: DEBUG + handlers: [mp_console, logfile] + propagate: false + + activitysim: + level: DEBUG + handlers: [console, logfile] + propagate: false + + orca: + level: WARNING + handlers: [console, logfile] + propagate: false + + handlers: + + logfile: + class: logging.FileHandler + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] + mode: w + formatter: fileFormatter + level: NOTSET + + console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + #level: WARNING + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] + + mp_console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + level: NOTSET + + formatters: + + simpleFormatter: + class: logging.Formatter + #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' + format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ + '%(processName)-10s %(levelname)s - %(name)s - %(message)s', + '%(levelname)s - %(name)s - %(message)s'] + datefmt: '%d/%m/%Y %H:%M:%S' + + fileFormatter: + class: logging.Formatter + format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' + datefmt: '%d/%m/%Y %H:%M:%S' + diff --git a/example_2mp/configs_report/settings.yaml b/example_2mp/configs_report/settings.yaml new file mode 100644 index 000000000..089066138 --- /dev/null +++ b/example_2mp/configs_report/settings.yaml @@ -0,0 +1,43 @@ + +inherit_settings: True + +multiprocess: False +mem_tick: 30 +profile: False +strict: False +num_processes: 1 + +households_sample_size: 0 +chunk_size: 0 + +# - tracing +trace_hh_id: +trace_od: + +resume_after: trip_mode_choice + +models: + - write_data_dictionary + - write_tables + +output_tables: + action: include + prefix: final_ + tables: + - checkpoints +# - accessibility +# - land_use +# - households +# - persons +# - trips +# - tours + + +coalesce: + names: + - pipeline_0.h5 + - pipeline_1.h5 + slice: + tables: + - households + - persons diff --git a/example_2mp/configs_sim/logging.yaml b/example_2mp/configs_sim/logging.yaml new file mode 100644 index 000000000..f326f3095 --- /dev/null +++ b/example_2mp/configs_sim/logging.yaml @@ -0,0 +1,68 @@ +# Config for logging +# ------------------ +# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema + +logging: + version: 1 + disable_existing_loggers: true + + + # Configuring the default (root) logger is highly recommended + root: + level: DEBUG + handlers: [console, logfile] + + loggers: + + tasks: + level: DEBUG + handlers: [mp_console, logfile] + propagate: false + + activitysim: + level: DEBUG + handlers: [console, logfile] + propagate: false + + orca: + level: WARNING + handlers: [console, logfile] + propagate: false + + handlers: + + logfile: + class: logging.FileHandler + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] + mode: w + formatter: fileFormatter + level: NOTSET + + console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + #level: WARNING + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] + + mp_console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + level: NOTSET + + formatters: + + simpleFormatter: + class: logging.Formatter + #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' + format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ + '%(processName)-10s %(levelname)s - %(name)s - %(message)s', + '%(levelname)s - %(name)s - %(message)s'] + datefmt: '%d/%m/%Y %H:%M:%S' + + fileFormatter: + class: logging.Formatter + format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' + datefmt: '%d/%m/%Y %H:%M:%S' + diff --git a/example_2mp/configs_sim/settings.yaml b/example_2mp/configs_sim/settings.yaml new file mode 100644 index 000000000..5d0c11af0 --- /dev/null +++ b/example_2mp/configs_sim/settings.yaml @@ -0,0 +1,92 @@ + +inherit_settings: True + +# - production config +#multiprocess: True +#mem_tick: 30 +#profile: False +#strict: False +#num_processes: 10 +#stagger: 30 + +# azure 432GB 64 processors +#multiprocess: True +#mem_tick: 30 +#profile: False +#strict: False +#num_processes: 30 +#stagger: 15 + +# - dev config 3% +#multiprocess: True +#mem_tick: 30 +#profile: False +#strict: False +#num_processes: 3 +#stagger: 30 + +# - dev config mini +multiprocess: True +mem_tick: 30 +profile: False +strict: False +num_processes: 2 +stagger: 15 + + +households_sample_size: 1000 +#households_sample_stride: [2, 0] + +chunk_size: 0 + +# - tracing +trace_hh_id: +trace_od: + +models: + - initialize_landuse + - compute_accessibility + - initialize_households + - _school_location_sample + - _school_location_logsums + - school_location_simulate + - _workplace_location_sample + - _workplace_location_logsums + - workplace_location_simulate + - auto_ownership_simulate + - cdap_simulate + - mandatory_tour_frequency + - mandatory_tour_scheduling + - joint_tour_frequency + - joint_tour_composition + - joint_tour_participation + - _joint_tour_destination_sample + - _joint_tour_destination_logsums + - joint_tour_destination_simulate + - joint_tour_scheduling + - non_mandatory_tour_frequency + - non_mandatory_tour_destination + - non_mandatory_tour_scheduling + - tour_mode_choice_simulate + - atwork_subtour_frequency + - _atwork_subtour_destination_sample + - _atwork_subtour_destination_logsums + - atwork_subtour_destination_simulate + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + +multiprocess_steps: + - name: mp_initialize + begin: initialize_landuse + - name: mp_households + begin: _school_location_sample + slice: + tables: + - households + - persons diff --git a/example_2mp/output/.gitignore b/example_2mp/output/.gitignore new file mode 100644 index 000000000..c925c5c3d --- /dev/null +++ b/example_2mp/output/.gitignore @@ -0,0 +1,7 @@ +*.csv +*.log +*.prof +*.h5 +*.txt +*.yaml + diff --git a/example_2mp/output/log/.gitignore b/example_2mp/output/log/.gitignore new file mode 100644 index 000000000..031b1db6c --- /dev/null +++ b/example_2mp/output/log/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.log diff --git a/example_2mp/output/trace/.gitignore b/example_2mp/output/trace/.gitignore new file mode 100644 index 000000000..afed0735d --- /dev/null +++ b/example_2mp/output/trace/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/example_2mp/output0/.gitignore b/example_2mp/output0/.gitignore new file mode 100644 index 000000000..c925c5c3d --- /dev/null +++ b/example_2mp/output0/.gitignore @@ -0,0 +1,7 @@ +*.csv +*.log +*.prof +*.h5 +*.txt +*.yaml + diff --git a/example_2mp/output0/log/.gitignore b/example_2mp/output0/log/.gitignore new file mode 100644 index 000000000..031b1db6c --- /dev/null +++ b/example_2mp/output0/log/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.log diff --git a/example_2mp/output0/trace/.gitignore b/example_2mp/output0/trace/.gitignore new file mode 100644 index 000000000..afed0735d --- /dev/null +++ b/example_2mp/output0/trace/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/example_2mp/output1/.gitignore b/example_2mp/output1/.gitignore new file mode 100644 index 000000000..c925c5c3d --- /dev/null +++ b/example_2mp/output1/.gitignore @@ -0,0 +1,7 @@ +*.csv +*.log +*.prof +*.h5 +*.txt +*.yaml + diff --git a/example_2mp/output1/log/.gitignore b/example_2mp/output1/log/.gitignore new file mode 100644 index 000000000..031b1db6c --- /dev/null +++ b/example_2mp/output1/log/.gitignore @@ -0,0 +1,2 @@ +*.csv +*.log diff --git a/example_2mp/output1/trace/.gitignore b/example_2mp/output1/trace/.gitignore new file mode 100644 index 000000000..afed0735d --- /dev/null +++ b/example_2mp/output1/trace/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/example_2mp/script.txt b/example_2mp/script.txt new file mode 100644 index 000000000..8cf5b36a8 --- /dev/null +++ b/example_2mp/script.txt @@ -0,0 +1,10 @@ +-d ~/work/activitysim-data/mtc_tm1/data + +cp -r output output0 +python simulation.py -s 2,0 -o output0 -c configs_sim -c ../example/configs + +cp -r output output1 +python simulation.py -s 2,1 -o output1 -c configs_sim -c ../example/configs + +cp -r output output_report +python coalesce diff --git a/example_2mp/simulation.py b/example_2mp/simulation.py new file mode 100644 index 000000000..532f13e43 --- /dev/null +++ b/example_2mp/simulation.py @@ -0,0 +1,103 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +import sys +import logging + +from activitysim.core import mem +from activitysim.core import inject +from activitysim.core import tracing +from activitysim.core import config +from activitysim.core import pipeline +from activitysim.core import mp_tasks +from activitysim.core import chunk + +# from activitysim import abm + + +logger = logging.getLogger('activitysim') + + +def cleanup_output_files(): + + active_log_files = \ + [h.baseFilename for h in logger.root.handlers if isinstance(h, logging.FileHandler)] + tracing.delete_output_files('log', ignore=active_log_files) + + tracing.delete_output_files('h5') + tracing.delete_output_files('csv') + tracing.delete_output_files('txt') + tracing.delete_output_files('yaml') + tracing.delete_output_files('prof') + + +def run(run_list, injectables=None): + + if run_list['multiprocess']: + logger.info("run multiprocess simulation") + mp_tasks.run_multiprocess(run_list, injectables) + else: + logger.info("run single process simulation") + pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) + pipeline.close_pipeline() + chunk.log_write_hwm() + + +def log_settings(): + + settings = [ + 'households_sample_size', + 'chunk_size', + 'multiprocess', + 'num_processes', + 'resume_after', + ] + + for k in settings: + logger.info("setting %s: %s" % (k, config.setting(k))) + + injectables = ['data_dir', 'configs_dir', 'output_dir'] + for k in injectables: + logger.info("injectable %s: %s" % (k, inject.get_injectable(k))) + + +if __name__ == '__main__': + + # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') + inject.add_injectable('data_dir', '../example/data') + inject.add_injectable('configs_dir', ['configs', '../example/configs']) + + config.handle_standard_args() + + mp_tasks.filter_warnings() + tracing.config_logger() + + log_settings() + + t0 = tracing.print_elapsed_time() + + # cleanup if not resuming + if not config.setting('resume_after', False): + cleanup_output_files() + + run_list = mp_tasks.get_run_list() + + if run_list['multiprocess']: + # do this after config.handle_standard_args, as command line args may override injectables + injectables = ['data_dir', 'configs_dir', 'output_dir'] + injectables = {k: inject.get_injectable(k) for k in injectables} + else: + injectables = None + + if config.setting('profile', False): + import cProfile + cProfile.runctx('run(run_list, injectables)', + globals(), locals(), filename=config.output_file_path('simulation.prof')) + else: + run(run_list, injectables) + + t0 = tracing.print_elapsed_time("everything", t0) From e98364401c6c2437b1e08c52067cde419a7b1894 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 15 Nov 2018 09:28:43 -0500 Subject: [PATCH 048/122] handle_standard_args returns overrridden injectables --- activitysim/abm/models/initialize.py | 11 ++++---- activitysim/core/config.py | 20 +++++++++----- activitysim/core/mp_tasks.py | 2 +- example_2mp/.gitignore | 1 + example_2mp/coalesce.py | 3 -- example_2mp/configs_report/settings.yaml | 4 +-- example_2mp/configs_sim/settings.yaml | 35 +++++------------------- example_2mp/output0/.gitignore | 7 ----- example_2mp/output0/log/.gitignore | 2 -- example_2mp/output0/trace/.gitignore | 1 - example_2mp/output1/.gitignore | 7 ----- example_2mp/output1/log/.gitignore | 2 -- example_2mp/output1/trace/.gitignore | 1 - example_2mp/script.txt | 20 ++++++++++---- example_2mp/simulation.py | 8 ++---- example_mp/coalesce.py | 3 -- example_mp/simulation.py | 8 ++---- 17 files changed, 50 insertions(+), 85 deletions(-) create mode 100644 example_2mp/.gitignore delete mode 100644 example_2mp/output0/.gitignore delete mode 100644 example_2mp/output0/log/.gitignore delete mode 100644 example_2mp/output0/trace/.gitignore delete mode 100644 example_2mp/output1/.gitignore delete mode 100644 example_2mp/output1/log/.gitignore delete mode 100644 example_2mp/output1/trace/.gitignore diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 9ee70b4e0..3a77b4cbb 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -105,10 +105,11 @@ def preload_injectables(): t0 = tracing.print_elapsed_time() - if inject.get_injectable('skim_dict', None) is not None: - t0 = tracing.print_elapsed_time("preload skim_dict", t0, debug=True) - - if inject.get_injectable('skim_stack', None) is not None: - t0 = tracing.print_elapsed_time("preload skim_stack", t0, debug=True) + # FIXME - still want to do this? + # if inject.get_injectable('skim_dict', None) is not None: + # t0 = tracing.print_elapsed_time("preload skim_dict", t0, debug=True) + # + # if inject.get_injectable('skim_stack', None) is not None: + # t0 = tracing.print_elapsed_time("preload skim_stack", t0, debug=True) return True diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 5e988117c..7e16e3005 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -107,7 +107,7 @@ def handle_standard_args(parser=None): Returns ------- - args : parser.parse_args() result + injectables - array of injectables altered by args """ if parser is None: @@ -126,25 +126,31 @@ def handle_standard_args(parser=None): args = parser.parse_args() + injectables = [] + + def override_injectable(name, value): + inject.add_injectable(name, value) + injectables.append(name) + if args.config: for dir in args.config: if not os.path.exists(dir): raise IOError("Could not find configs dir '%s'" % dir) - inject.add_injectable("configs_dir", args.config) + override_injectable("configs_dir", args.config) if args.output: if not os.path.exists(args.output): raise IOError("Could not find output dir '%s'." % args.output) - inject.add_injectable("output_dir", args.output) + override_injectable("output_dir", args.output) if args.data: if not os.path.exists(args.data): raise IOError("Could not find data dir '%s'" % args.data) - inject.add_injectable("data_dir", args.data) + override_injectable("data_dir", args.data) # FIXME - should these be settings? if args.stride: - inject.add_injectable("households_sample_stride", args.stride) + override_injectable("households_sample_stride", args.stride) if args.pipeline: - inject.add_injectable("pipeline_file_name", args.pipeline) + override_injectable("pipeline_file_name", args.pipeline) # - do these after potentially overriding configs_dir if args.resume is not None: @@ -152,7 +158,7 @@ def handle_standard_args(parser=None): if args.multiprocess is not None: override_setting('multiprocess', args.multiprocess) - return args + return injectables def override_setting(key, value): diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 8b3a4a3c1..c9b32c0a6 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -753,7 +753,7 @@ def get_run_list(): if not models or not isinstance(models, list): raise RuntimeError('No models list in settings file') if resume_after not in models + ['_', None]: - raise RuntimeError("resume_after '%s' not in models list" % resume_after) + raise RuntimeError("resume_after '%s' not in models list" % resume_after) if resume_after == models[-1]: raise RuntimeError("resume_after '%s' is last model in models list" % resume_after) diff --git a/example_2mp/.gitignore b/example_2mp/.gitignore new file mode 100644 index 000000000..c80916134 --- /dev/null +++ b/example_2mp/.gitignore @@ -0,0 +1 @@ +output_*/ diff --git a/example_2mp/coalesce.py b/example_2mp/coalesce.py index 8c20e5078..5d4079358 100644 --- a/example_2mp/coalesce.py +++ b/example_2mp/coalesce.py @@ -26,7 +26,6 @@ if __name__ == '__main__': - inject.add_injectable('configs_dir', ['configs', '../example/configs']) config.handle_standard_args() @@ -38,8 +37,6 @@ coalesce_rules = config.setting('coalesce') - #run_list = mp_tasks.get_run_list() - mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) checkpoints_df = pipeline.get_checkpoints() diff --git a/example_2mp/configs_report/settings.yaml b/example_2mp/configs_report/settings.yaml index 089066138..0bfa50ec4 100644 --- a/example_2mp/configs_report/settings.yaml +++ b/example_2mp/configs_report/settings.yaml @@ -35,8 +35,8 @@ output_tables: coalesce: names: - - pipeline_0.h5 - - pipeline_1.h5 + - output_0/pipeline.h5 + - output_1/pipeline.h5 slice: tables: - households diff --git a/example_2mp/configs_sim/settings.yaml b/example_2mp/configs_sim/settings.yaml index 5d0c11af0..cc180220e 100644 --- a/example_2mp/configs_sim/settings.yaml +++ b/example_2mp/configs_sim/settings.yaml @@ -1,43 +1,22 @@ inherit_settings: True -# - production config -#multiprocess: True -#mem_tick: 30 -#profile: False -#strict: False -#num_processes: 10 -#stagger: 30 - -# azure 432GB 64 processors -#multiprocess: True -#mem_tick: 30 -#profile: False -#strict: False -#num_processes: 30 -#stagger: 15 - -# - dev config 3% -#multiprocess: True -#mem_tick: 30 -#profile: False -#strict: False -#num_processes: 3 -#stagger: 30 # - dev config mini multiprocess: True mem_tick: 30 profile: False strict: False -num_processes: 2 -stagger: 15 -households_sample_size: 1000 -#households_sample_stride: [2, 0] +households_sample_size: 0 +# so the following will get a 10 percent slice: +#households_sample_stride: [10, 0] + +chunk_size: 1500000000 +num_processes: 3 +stagger: 15 -chunk_size: 0 # - tracing trace_hh_id: diff --git a/example_2mp/output0/.gitignore b/example_2mp/output0/.gitignore deleted file mode 100644 index c925c5c3d..000000000 --- a/example_2mp/output0/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.csv -*.log -*.prof -*.h5 -*.txt -*.yaml - diff --git a/example_2mp/output0/log/.gitignore b/example_2mp/output0/log/.gitignore deleted file mode 100644 index 031b1db6c..000000000 --- a/example_2mp/output0/log/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.csv -*.log diff --git a/example_2mp/output0/trace/.gitignore b/example_2mp/output0/trace/.gitignore deleted file mode 100644 index afed0735d..000000000 --- a/example_2mp/output0/trace/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.csv diff --git a/example_2mp/output1/.gitignore b/example_2mp/output1/.gitignore deleted file mode 100644 index c925c5c3d..000000000 --- a/example_2mp/output1/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.csv -*.log -*.prof -*.h5 -*.txt -*.yaml - diff --git a/example_2mp/output1/log/.gitignore b/example_2mp/output1/log/.gitignore deleted file mode 100644 index 031b1db6c..000000000 --- a/example_2mp/output1/log/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.csv -*.log diff --git a/example_2mp/output1/trace/.gitignore b/example_2mp/output1/trace/.gitignore deleted file mode 100644 index afed0735d..000000000 --- a/example_2mp/output1/trace/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.csv diff --git a/example_2mp/script.txt b/example_2mp/script.txt index 8cf5b36a8..85d74e529 100644 --- a/example_2mp/script.txt +++ b/example_2mp/script.txt @@ -1,10 +1,18 @@ -d ~/work/activitysim-data/mtc_tm1/data -cp -r output output0 -python simulation.py -s 2,0 -o output0 -c configs_sim -c ../example/configs +rm -r output_0; cp -r output output_0 +python simulation.py -s 10,0 -o output_0 -c configs_sim -c ../example/configs -cp -r output output1 -python simulation.py -s 2,1 -o output1 -c configs_sim -c ../example/configs +rm -r output_1; cp -r output output_1 +python simulation.py -s 10,1 -o output_1 -c configs_sim -c ../example/configs + +rm -r output_report; cp -r output output_report +python coalesce.py -o output_report -c configs_report -c ../example/configs + +python simulation.py -o output_report -c configs_report -c ../example/configs + + +activate asim3 +cd E:\Projects\Clients\ASIM\activitysim\example_2mp +python simulation.py -s 10,1 -o output1 -c configs_sim -c ../example/configs -cp -r output output_report -python coalesce diff --git a/example_2mp/simulation.py b/example_2mp/simulation.py index 532f13e43..949cb252b 100644 --- a/example_2mp/simulation.py +++ b/example_2mp/simulation.py @@ -47,7 +47,7 @@ def run(run_list, injectables=None): chunk.log_write_hwm() -def log_settings(): +def log_settings(injectables): settings = [ 'households_sample_size', @@ -60,7 +60,6 @@ def log_settings(): for k in settings: logger.info("setting %s: %s" % (k, config.setting(k))) - injectables = ['data_dir', 'configs_dir', 'output_dir'] for k in injectables: logger.info("injectable %s: %s" % (k, inject.get_injectable(k))) @@ -71,12 +70,12 @@ def log_settings(): inject.add_injectable('data_dir', '../example/data') inject.add_injectable('configs_dir', ['configs', '../example/configs']) - config.handle_standard_args() + injectables = config.handle_standard_args() mp_tasks.filter_warnings() tracing.config_logger() - log_settings() + log_settings(injectables) t0 = tracing.print_elapsed_time() @@ -88,7 +87,6 @@ def log_settings(): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables - injectables = ['data_dir', 'configs_dir', 'output_dir'] injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None diff --git a/example_mp/coalesce.py b/example_mp/coalesce.py index 8c20e5078..5d4079358 100644 --- a/example_mp/coalesce.py +++ b/example_mp/coalesce.py @@ -26,7 +26,6 @@ if __name__ == '__main__': - inject.add_injectable('configs_dir', ['configs', '../example/configs']) config.handle_standard_args() @@ -38,8 +37,6 @@ coalesce_rules = config.setting('coalesce') - #run_list = mp_tasks.get_run_list() - mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) checkpoints_df = pipeline.get_checkpoints() diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 532f13e43..949cb252b 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -47,7 +47,7 @@ def run(run_list, injectables=None): chunk.log_write_hwm() -def log_settings(): +def log_settings(injectables): settings = [ 'households_sample_size', @@ -60,7 +60,6 @@ def log_settings(): for k in settings: logger.info("setting %s: %s" % (k, config.setting(k))) - injectables = ['data_dir', 'configs_dir', 'output_dir'] for k in injectables: logger.info("injectable %s: %s" % (k, inject.get_injectable(k))) @@ -71,12 +70,12 @@ def log_settings(): inject.add_injectable('data_dir', '../example/data') inject.add_injectable('configs_dir', ['configs', '../example/configs']) - config.handle_standard_args() + injectables = config.handle_standard_args() mp_tasks.filter_warnings() tracing.config_logger() - log_settings() + log_settings(injectables) t0 = tracing.print_elapsed_time() @@ -88,7 +87,6 @@ def log_settings(): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables - injectables = ['data_dir', 'configs_dir', 'output_dir'] injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None From 0938f7be59a4315e1005383ca41ab3cd2d2931ab Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 26 Nov 2018 13:12:02 -0500 Subject: [PATCH 049/122] example_2mp with stride script --- .travis.yml | 4 +- activitysim/abm/models/trip_destination.py | 2 +- activitysim/core/chunk.py | 1 + activitysim/core/mp_tasks.py | 4 +- example_2mp/script.txt | 18 -------- {example_2mp => example_mp}/.gitignore | 0 example_mp/coalesce.py | 46 ------------------- example_mp/configs/logging.yaml | 11 ----- example_mp/configs/settings.yaml | 11 +++-- example_mp/stride.txt | 0 example_mp_stride/.gitignore | 1 + .../coalesce.py | 0 .../configs_report/logging.yaml | 11 ----- .../configs_report/settings.yaml | 0 .../configs_sim/logging.yaml | 11 ----- .../configs_sim/settings.yaml | 6 +-- .../output/.gitignore | 0 .../output/log/.gitignore | 0 .../output/trace/.gitignore | 0 example_mp_stride/script.txt | 21 +++++++++ .../simulation.py | 0 pytest.ini | 3 ++ 22 files changed, 38 insertions(+), 112 deletions(-) delete mode 100644 example_2mp/script.txt rename {example_2mp => example_mp}/.gitignore (100%) delete mode 100644 example_mp/coalesce.py create mode 100644 example_mp/stride.txt create mode 100644 example_mp_stride/.gitignore rename {example_2mp => example_mp_stride}/coalesce.py (100%) rename {example_2mp => example_mp_stride}/configs_report/logging.yaml (86%) rename {example_2mp => example_mp_stride}/configs_report/settings.yaml (100%) rename {example_2mp => example_mp_stride}/configs_sim/logging.yaml (86%) rename {example_2mp => example_mp_stride}/configs_sim/settings.yaml (92%) rename {example_2mp => example_mp_stride}/output/.gitignore (100%) rename {example_2mp => example_mp_stride}/output/log/.gitignore (100%) rename {example_2mp => example_mp_stride}/output/trace/.gitignore (100%) create mode 100644 example_mp_stride/script.txt rename {example_2mp => example_mp_stride}/simulation.py (100%) create mode 100644 pytest.ini diff --git a/.travis.yml b/.travis.yml index d4438cbdc..6fa3f2276 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,9 @@ install: - conda update -q conda - conda info -a - | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION cytoolz numpy pandas pip pytables pyyaml toolz psutil + conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION cytoolz numpy pandas pip pytables pyyaml toolz psutil future - source activate test-environment -- pip install openmatrix zbox future +- pip install openmatrix zbox - pip install pytest pytest-cov coveralls pycodestyle - pip install sphinx numpydoc sphinx_rtd_theme - pip install . diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index b3478e797..2f81b220f 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -306,7 +306,7 @@ def choose_trip_destination( dropped_trips = ~trips.index.isin(destination_sample.index.unique()) if dropped_trips.any(): - logger.warning("%s trip_destination_ample %s trips " + logger.warning("%s trip_destination_sample %s trips " "without viable destination alternatives" % (trace_label, dropped_trips.sum())) trips = trips[~dropped_trips] diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index fd03390b7..2d905ff34 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -88,6 +88,7 @@ def log_close(trace_label): def log_df(trace_label, table_name, df): if df is None: + # FIXME force_garbage_collect on delete? mem.force_garbage_collect() cur_chunker = next(reversed(CHUNK_LOG)) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index c9b32c0a6..02395324c 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -38,7 +38,7 @@ from activitysim.abm.tables.skims import load_skims -logger = logging.getLogger('activitysim') +logger = logging.getLogger(__name__) """ @@ -752,8 +752,6 @@ def get_run_list(): if not models or not isinstance(models, list): raise RuntimeError('No models list in settings file') - if resume_after not in models + ['_', None]: - raise RuntimeError("resume_after '%s' not in models list" % resume_after) if resume_after == models[-1]: raise RuntimeError("resume_after '%s' is last model in models list" % resume_after) diff --git a/example_2mp/script.txt b/example_2mp/script.txt deleted file mode 100644 index 85d74e529..000000000 --- a/example_2mp/script.txt +++ /dev/null @@ -1,18 +0,0 @@ --d ~/work/activitysim-data/mtc_tm1/data - -rm -r output_0; cp -r output output_0 -python simulation.py -s 10,0 -o output_0 -c configs_sim -c ../example/configs - -rm -r output_1; cp -r output output_1 -python simulation.py -s 10,1 -o output_1 -c configs_sim -c ../example/configs - -rm -r output_report; cp -r output output_report -python coalesce.py -o output_report -c configs_report -c ../example/configs - -python simulation.py -o output_report -c configs_report -c ../example/configs - - -activate asim3 -cd E:\Projects\Clients\ASIM\activitysim\example_2mp -python simulation.py -s 10,1 -o output1 -c configs_sim -c ../example/configs - diff --git a/example_2mp/.gitignore b/example_mp/.gitignore similarity index 100% rename from example_2mp/.gitignore rename to example_mp/.gitignore diff --git a/example_mp/coalesce.py b/example_mp/coalesce.py deleted file mode 100644 index 5d4079358..000000000 --- a/example_mp/coalesce.py +++ /dev/null @@ -1,46 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -from __future__ import (absolute_import, division, print_function, ) -from future.standard_library import install_aliases -install_aliases() # noqa: E402 - -import sys -import logging -import argparse -import os - -from activitysim.core import mem -from activitysim.core import inject -from activitysim.core import tracing -from activitysim.core import config -from activitysim.core import pipeline -from activitysim.core import mp_tasks -from activitysim.core import chunk - -# from activitysim import abm - - -logger = logging.getLogger('activitysim') - - -if __name__ == '__main__': - - inject.add_injectable('configs_dir', ['configs', '../example/configs']) - - config.handle_standard_args() - - mp_tasks.filter_warnings() - tracing.config_logger() - - t0 = tracing.print_elapsed_time() - - coalesce_rules = config.setting('coalesce') - - mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) - - checkpoints_df = pipeline.get_checkpoints() - file_path = config.output_file_path('coalesce_checkpoints.csv') - checkpoints_df.to_csv(file_path, index=True) - - t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index f326f3095..e4da6d784 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -14,11 +14,6 @@ logging: loggers: - tasks: - level: DEBUG - handlers: [mp_console, logfile] - propagate: false - activitysim: level: DEBUG handlers: [console, logfile] @@ -45,12 +40,6 @@ logging: #level: WARNING level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] - mp_console: - class: logging.StreamHandler - stream: ext://sys.stdout - formatter: simpleFormatter - level: NOTSET - formatters: simpleFormatter: diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index a6b499069..fc3eff021 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -30,8 +30,8 @@ multiprocess: True mem_tick: 30 profile: False strict: False -num_processes: 3 -stagger: 5 +#num_processes: 3 +#stagger: 5 # - full sample @@ -56,11 +56,14 @@ stagger: 5 # - mini sample #households_sample_size: 300 -households_sample_size: 200 +#households_sample_size: 0 #households_sample_stride: [3, 0] -chunk_size: 0 +households_sample_size: 0 +chunk_size: 1000000000 +num_processes: 1 +households_sample_stride: [36,0] # - tracing trace_hh_id: diff --git a/example_mp/stride.txt b/example_mp/stride.txt new file mode 100644 index 000000000..e69de29bb diff --git a/example_mp_stride/.gitignore b/example_mp_stride/.gitignore new file mode 100644 index 000000000..c80916134 --- /dev/null +++ b/example_mp_stride/.gitignore @@ -0,0 +1 @@ +output_*/ diff --git a/example_2mp/coalesce.py b/example_mp_stride/coalesce.py similarity index 100% rename from example_2mp/coalesce.py rename to example_mp_stride/coalesce.py diff --git a/example_2mp/configs_report/logging.yaml b/example_mp_stride/configs_report/logging.yaml similarity index 86% rename from example_2mp/configs_report/logging.yaml rename to example_mp_stride/configs_report/logging.yaml index f326f3095..e4da6d784 100644 --- a/example_2mp/configs_report/logging.yaml +++ b/example_mp_stride/configs_report/logging.yaml @@ -14,11 +14,6 @@ logging: loggers: - tasks: - level: DEBUG - handlers: [mp_console, logfile] - propagate: false - activitysim: level: DEBUG handlers: [console, logfile] @@ -45,12 +40,6 @@ logging: #level: WARNING level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] - mp_console: - class: logging.StreamHandler - stream: ext://sys.stdout - formatter: simpleFormatter - level: NOTSET - formatters: simpleFormatter: diff --git a/example_2mp/configs_report/settings.yaml b/example_mp_stride/configs_report/settings.yaml similarity index 100% rename from example_2mp/configs_report/settings.yaml rename to example_mp_stride/configs_report/settings.yaml diff --git a/example_2mp/configs_sim/logging.yaml b/example_mp_stride/configs_sim/logging.yaml similarity index 86% rename from example_2mp/configs_sim/logging.yaml rename to example_mp_stride/configs_sim/logging.yaml index f326f3095..e4da6d784 100644 --- a/example_2mp/configs_sim/logging.yaml +++ b/example_mp_stride/configs_sim/logging.yaml @@ -14,11 +14,6 @@ logging: loggers: - tasks: - level: DEBUG - handlers: [mp_console, logfile] - propagate: false - activitysim: level: DEBUG handlers: [console, logfile] @@ -45,12 +40,6 @@ logging: #level: WARNING level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] - mp_console: - class: logging.StreamHandler - stream: ext://sys.stdout - formatter: simpleFormatter - level: NOTSET - formatters: simpleFormatter: diff --git a/example_2mp/configs_sim/settings.yaml b/example_mp_stride/configs_sim/settings.yaml similarity index 92% rename from example_2mp/configs_sim/settings.yaml rename to example_mp_stride/configs_sim/settings.yaml index cc180220e..a2e0e876e 100644 --- a/example_2mp/configs_sim/settings.yaml +++ b/example_mp_stride/configs_sim/settings.yaml @@ -8,11 +8,7 @@ mem_tick: 30 profile: False strict: False - -households_sample_size: 0 -# so the following will get a 10 percent slice: -#households_sample_stride: [10, 0] - +households_sample_size: 600 chunk_size: 1500000000 num_processes: 3 stagger: 15 diff --git a/example_2mp/output/.gitignore b/example_mp_stride/output/.gitignore similarity index 100% rename from example_2mp/output/.gitignore rename to example_mp_stride/output/.gitignore diff --git a/example_2mp/output/log/.gitignore b/example_mp_stride/output/log/.gitignore similarity index 100% rename from example_2mp/output/log/.gitignore rename to example_mp_stride/output/log/.gitignore diff --git a/example_2mp/output/trace/.gitignore b/example_mp_stride/output/trace/.gitignore similarity index 100% rename from example_2mp/output/trace/.gitignore rename to example_mp_stride/output/trace/.gitignore diff --git a/example_mp_stride/script.txt b/example_mp_stride/script.txt new file mode 100644 index 000000000..29a30a69d --- /dev/null +++ b/example_mp_stride/script.txt @@ -0,0 +1,21 @@ + +#DATA_DIR=E:\Projects\Clients\ASIM\full_example_input_data +DATA_DIR=~/work/activitysim-data/mtc_tm1_sf/data + +rm -r output_0; cp -r output output_0 +python simulation.py -s 2,0 -o output_0 -c configs_sim -c ../example/configs -d $DATA_DIR + +rm -r output_1; cp -r output output_1 +python simulation.py -s 2,1 -o output_1 -c configs_sim -c ../example/configs -d $DATA_DIR + +rm -r output_report; cp -r output output_report +python coalesce.py -o output_report -c configs_report -c ../example/configs + +# run final report tasks +python simulation.py -o output_report -c configs_report -c ../example/configs + + +rm -r output_0 +rm -r output_1 +rm -r output_report + diff --git a/example_2mp/simulation.py b/example_mp_stride/simulation.py similarity index 100% rename from example_2mp/simulation.py rename to example_mp_stride/simulation.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..0ad0d4249 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::tables.NaturalNameWarning From 0a846a0b081f416a94f7cde5c874546210ddd734 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Tue, 27 Nov 2018 14:03:55 -0500 Subject: [PATCH 050/122] add example_azure --- activitysim/core/config.py | 1 - example_azure/azure_env.txt | 9 ++ .../benchmarks/benchmarks_full_run.txt | 107 +++++++++++++ .../benchmarks/benchmarks_stride_run.txt | 18 +++ example_azure/example_mp/logging.yaml | 57 +++++++ example_azure/example_mp/settings.yaml | 80 ++++++++++ example_azure/step1_create_vm.txt | 148 ++++++++++++++++++ example_azure/step2_config_asim.txt | 49 ++++++ example_azure/step3_run_asim.txt | 52 ++++++ example_azure/util_create_smb_fileshare.txt | 38 +++++ example_azure/util_resize_managed_storage.txt | 59 +++++++ example_azure/vm_sizes.txt | 24 +++ example_mp/simulation.py | 1 + .../README.MD | 0 .../configs/best_transit_path.csv | 0 .../configs/best_transit_path.yaml | 0 .../configs/logging.yaml | 0 .../configs/settings.yaml | 0 .../data/skims.omx | Bin .../dump_data.py | 0 .../extensions/__init__.py | 0 .../extensions/los.py | 0 .../extensions/models.py | 0 .../extensions/skims.py | 0 .../import_data.py | 0 .../output/.gitignore | 0 .../simulation.py | 0 .../.gitignore | 0 .../coalesce.py | 0 .../configs_report/logging.yaml | 0 .../configs_report/settings.yaml | 0 .../configs_sim/logging.yaml | 0 .../configs_sim/settings.yaml | 0 .../output/.gitignore | 0 .../output/log/.gitignore | 0 .../output/trace/.gitignore | 0 .../script.txt | 0 .../simulation.py | 1 + 38 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 example_azure/azure_env.txt create mode 100644 example_azure/benchmarks/benchmarks_full_run.txt create mode 100644 example_azure/benchmarks/benchmarks_stride_run.txt create mode 100644 example_azure/example_mp/logging.yaml create mode 100644 example_azure/example_mp/settings.yaml create mode 100644 example_azure/step1_create_vm.txt create mode 100644 example_azure/step2_config_asim.txt create mode 100644 example_azure/step3_run_asim.txt create mode 100644 example_azure/util_create_smb_fileshare.txt create mode 100644 example_azure/util_resize_managed_storage.txt create mode 100644 example_azure/vm_sizes.txt rename {example_multi => example_multiple_zone}/README.MD (100%) rename {example_multi => example_multiple_zone}/configs/best_transit_path.csv (100%) rename {example_multi => example_multiple_zone}/configs/best_transit_path.yaml (100%) rename {example_multi => example_multiple_zone}/configs/logging.yaml (100%) rename {example_multi => example_multiple_zone}/configs/settings.yaml (100%) rename {example_multi => example_multiple_zone}/data/skims.omx (100%) rename {example_multi => example_multiple_zone}/dump_data.py (100%) rename {example_multi => example_multiple_zone}/extensions/__init__.py (100%) rename {example_multi => example_multiple_zone}/extensions/los.py (100%) rename {example_multi => example_multiple_zone}/extensions/models.py (100%) rename {example_multi => example_multiple_zone}/extensions/skims.py (100%) rename {example_multi => example_multiple_zone}/import_data.py (100%) rename {example_multi => example_multiple_zone}/output/.gitignore (100%) rename {example_multi => example_multiple_zone}/simulation.py (100%) rename {example_mp_stride => example_stride}/.gitignore (100%) rename {example_mp_stride => example_stride}/coalesce.py (100%) rename {example_mp_stride => example_stride}/configs_report/logging.yaml (100%) rename {example_mp_stride => example_stride}/configs_report/settings.yaml (100%) rename {example_mp_stride => example_stride}/configs_sim/logging.yaml (100%) rename {example_mp_stride => example_stride}/configs_sim/settings.yaml (100%) rename {example_mp_stride => example_stride}/output/.gitignore (100%) rename {example_mp_stride => example_stride}/output/log/.gitignore (100%) rename {example_mp_stride => example_stride}/output/trace/.gitignore (100%) rename {example_mp_stride => example_stride}/script.txt (100%) rename {example_mp_stride => example_stride}/simulation.py (96%) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 7e16e3005..728061d59 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -146,7 +146,6 @@ def override_injectable(name, value): raise IOError("Could not find data dir '%s'" % args.data) override_injectable("data_dir", args.data) - # FIXME - should these be settings? if args.stride: override_injectable("households_sample_stride", args.stride) if args.pipeline: diff --git a/example_azure/azure_env.txt b/example_azure/azure_env.txt new file mode 100644 index 000000000..10febb051 --- /dev/null +++ b/example_azure/azure_env.txt @@ -0,0 +1,9 @@ +AZ_VM_NUMBER=1 + +AZ_VM_NAME=ubuntu$AZ_VM_NUMBER +AZ_RESOURCE_GROUP=jeffdoyle +AZ_USERNAME=azureuser +AZ_LOCATION=eastus +AZ_VM_IMAGE=UbuntuLTS + +SHARE_NAME=myshare diff --git a/example_azure/benchmarks/benchmarks_full_run.txt b/example_azure/benchmarks/benchmarks_full_run.txt new file mode 100644 index 000000000..824a9256f --- /dev/null +++ b/example_azure/benchmarks/benchmarks_full_run.txt @@ -0,0 +1,107 @@ +------------ azure Windows 432GB 64 processors + +# run1 +mem_tick: 30 +profile: False +strict: False +households_sample_size: 0 +chunk_size: 20000000000 +num_processes: 40 +stagger: 15 + +INFO - activitysim.core.mem - high water mark used: 188.825035095 timestamp: 08/11/2018 18:10:42 label: +INFO - activitysim.core.mem - high water mark rss: 314.619289398 timestamp: 08/11/2018 18:10:42 label: +INFO - activitysim.core.tracing - Time to execute everything : 7731.622 seconds (128.9 minutes) + +# run2 +mem_tick: 30 +profile: False +strict: False +households_sample_size: 0 +chunk_size: 0 +num_processes: 30 +stagger: 15 + +INFO - activitysim.core.mem - high water mark used: 312.758975983 timestamp: 08/11/2018 20:17:16 label: +INFO - activitysim.core.mem - high water mark rss: 404.58372879 timestamp: 08/11/2018 20:17:16 label: +INFO - activitysim.core.tracing - Time to execute everything : 7880.258 seconds (131.3 minutes) + +------------ azure linux 432GB 64 processors + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +Standard_E64_v3 + +AZ_VM_SIZE=Standard_E64_v3 +az vm resize --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME --size $AZ_VM_SIZE + + +households_sample_size: 0 +chunk_size: 0 +num_processes: 30 +stagger: 15 + +INFO - activitysim.core.mem - high water mark used: 355.52 timestamp: 19/11/2018 21:06:08 label: +INFO - activitysim.core.mem - high water mark rss: 473.19 timestamp: 19/11/2018 21:06:08 label: +INFO - activitysim.core.tracing - Time to execute everything : 11756.813 seconds (195.9 minutes) + + +#################################################################################### + +export OMP_NUM_THREADS=1 + +multiprocess: True +mem_tick: 0 +profile: False +strict: False + +households_sample_size: 0 +chunk_size: 60000000000 +num_processes: 60 +stagger: 10 + + +INFO - activitysim.core.mem - high water mark used: 240.55 timestamp: 20/11/2018 15:21:53 label: mp_households_59.trip_purpose.completed +INFO - activitysim.core.mem - high water mark rss: 480.60 timestamp: 20/11/2018 15:21:53 label: mp_households_59.trip_purpose.completed +INFO - activitysim.core.tracing - Time to execute everything : 3609.947 seconds (60.2 minutes) + + +#################################################################################### + + +mem_tick: 0 +households_sample_size: 0 +chunk_size: 64000000000 +num_processes: 64 +stagger: 5 + +export MKL_NUM_THREADS=1 +export NUMEXPR_NUM_THREADS=1 +export OMP_NUM_THREADS=1 +python simulation.py -d /datadrive/work/data/full + +INFO - activitysim.core.mem - high water mark used: 223.78 timestamp: 20/11/2018 16:34:41 label: mp_households_57.joint_tour_scheduling.completed +INFO - activitysim.core.mem - high water mark rss: 478.95 timestamp: 20/11/2018 16:34:41 label: mp_households_56._joint_tour_destination_logsums.completed +INFO - activitysim.core.tracing - Time to execute everything : 3636.418 seconds (60.6 minutes) + + +#################################################################################### + +mem_tick: 0 +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +stagger: 5 + +INFO - activitysim.core.mem - high water mark used: 233.90 timestamp: 20/11/2018 17:43:50 label: mp_households_2.non_mandatory_tour_scheduling.completed +INFO - activitysim.core.mem - high water mark rss: 473.74 timestamp: 20/11/2018 17:43:50 label: mp_households_2.non_mandatory_tour_scheduling.completed +INFO - activitysim.core.tracing - Time to execute everything : 3596.278 seconds (59.9 minutes) + +*** Full Activitysim run on Azure in one hour costs $5.00 *** +Azure Standard_E64_v3 +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +Time to execute everything : 3596.278 seconds (59.9 minutes) +Max RAM 233.90GB + diff --git a/example_azure/benchmarks/benchmarks_stride_run.txt b/example_azure/benchmarks/benchmarks_stride_run.txt new file mode 100644 index 000000000..b1b9fba2d --- /dev/null +++ b/example_azure/benchmarks/benchmarks_stride_run.txt @@ -0,0 +1,18 @@ + +# 50% stride + +python simulation.py -s 2,0 -d /datadrive/work/data/full + +multiprocess: True +mem_tick: 0 +profile: False +strict: False + +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +stagger: 5 + +INFO - activitysim.core.mem - high water mark used: 118.81 timestamp: 20/11/2018 21:02:18 label: mp_households_59.trip_purpose.completed +INFO - activitysim.core.mem - high water mark rss: 359.20 timestamp: 20/11/2018 21:02:18 label: mp_households_59.trip_purpose.completed +INFO - activitysim.core.tracing - Time to execute everything : 2202.026 seconds (36.7 minutes) diff --git a/example_azure/example_mp/logging.yaml b/example_azure/example_mp/logging.yaml new file mode 100644 index 000000000..0e8ef6eef --- /dev/null +++ b/example_azure/example_mp/logging.yaml @@ -0,0 +1,57 @@ +# Config for logging +# ------------------ +# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema + +logging: + version: 1 + disable_existing_loggers: true + + + # Configuring the default (root) logger is highly recommended + root: + level: DEBUG + handlers: [console, logfile] + + loggers: + + activitysim: + level: DEBUG + handlers: [console, logfile] + propagate: false + + orca: + level: WARNING + handlers: [console, logfile] + propagate: false + + handlers: + + logfile: + class: logging.FileHandler + filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] + mode: w + formatter: fileFormatter + level: NOTSET + + console: + class: logging.StreamHandler + stream: ext://sys.stdout + formatter: simpleFormatter + #level: WARNING + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [WARNING, NOTSET] + + formatters: + + simpleFormatter: + class: logging.Formatter + #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' + format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ + '%(processName)-10s %(levelname)s - %(name)s - %(message)s', + '%(levelname)s - %(name)s - %(message)s'] + datefmt: '%d/%m/%Y %H:%M:%S' + + fileFormatter: + class: logging.Formatter + format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' + datefmt: '%d/%m/%Y %H:%M:%S' + diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml new file mode 100644 index 000000000..c5dbb7219 --- /dev/null +++ b/example_azure/example_mp/settings.yaml @@ -0,0 +1,80 @@ + +inherit_settings: True + +# - production config + +multiprocess: True +mem_tick: 60 +profile: False +strict: False + +households_sample_size: 0 +chunk_size: 32000000000 +num_processes: 20 +stagger: 30 + + + +# - tracing +trace_hh_id: +trace_od: + + +models: + - initialize_landuse + - compute_accessibility + - initialize_households + - _school_location_sample + - _school_location_logsums + - school_location_simulate + - _workplace_location_sample + - _workplace_location_logsums + - workplace_location_simulate + - auto_ownership_simulate + - cdap_simulate + - mandatory_tour_frequency + - mandatory_tour_scheduling + - joint_tour_frequency + - joint_tour_composition + - joint_tour_participation + - _joint_tour_destination_sample + - _joint_tour_destination_logsums + - joint_tour_destination_simulate + - joint_tour_scheduling + - non_mandatory_tour_frequency + - non_mandatory_tour_destination + - non_mandatory_tour_scheduling + - tour_mode_choice_simulate + - atwork_subtour_frequency + - _atwork_subtour_destination_sample + - _atwork_subtour_destination_logsums + - atwork_subtour_destination_simulate + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + - write_data_dictionary + - write_tables + +multiprocess_steps: + - name: mp_initialize + begin: initialize_landuse + - name: mp_households + begin: _school_location_sample + slice: + tables: + - households + - persons + - name: mp_summarize + begin: write_data_dictionary + +output_tables: + action: include + prefix: final_ + tables: + - checkpoints + diff --git a/example_azure/step1_create_vm.txt b/example_azure/step1_create_vm.txt new file mode 100644 index 000000000..7a9d3f8f6 --- /dev/null +++ b/example_azure/step1_create_vm.txt @@ -0,0 +1,148 @@ + + +# 128 GiB SSD Temp - want enough temp to create 64GB swap +AZ_VM_SIZE=Standard_D16s_v3 + + +########################## create vm + +az vm create \ + --resource-group $AZ_RESOURCE_GROUP \ + --name $AZ_VM_NAME \ + --image $AZ_VM_IMAGE \ + --admin-username $AZ_USERNAME \ + --size $AZ_VM_SIZE + +########################## add a standard sdd managed disk + +#https://docs.microsoft.com/en-us/azure/virtual-machines/linux/add-disk + +MANAGED_DISK_NAME=datadisk$AZ_VM_NUMBER +DISK_SIZE_GB=200 +DISK_SKU=StandardSSD_LRS +# Premium_LRS, StandardSSD_LRS, Standard_LRS, UltraSSD_LRS + +az vm disk attach \ + -g $AZ_RESOURCE_GROUP \ + --vm-name $AZ_VM_NAME \ + --disk $MANAGED_DISK_NAME \ + --new \ + --size-gb $DISK_SIZE_GB \ + --sku $DISK_SKU + + +########################## start vm + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) + +ssh $AZ_USERNAME@$VM_IP + + +########################## prepare managed disk + +#dmesg | grep SCSI + +(echo n; echo p; echo 1; echo ; echo ; echo w) | sudo fdisk /dev/sdc + + # sudo fdisk /dev/sdc + # Welcome to fdisk (util-linux 2.27.1). + # Changes will remain in memory only, until you decide to write them. + # Be careful before using the write command. + # + # Device does not contain a recognized partition table. + # Created a new DOS disklabel with disk identifier 0x06c86a75. + # + # Command (m for help): n + # Partition type + # p primary (0 primary, 0 extended, 4 free) + # e extended (container for logical partitions) + # Select (default p): p + # Partition number (1-4, default 1): 1 + # First sector (2048-209715199, default 2048): + # Last sector, +sectors or +size{K,M,G,T,P} (2048-209715199, default 209715199): + # + # Created a new partition 1 of type 'Linux' and of size 100 GiB. + # + # Command (m for help): p + # Disk /dev/sdc: 100 GiB, 107374182400 bytes, 209715200 sectors + # Units: sectors of 1 * 512 = 512 bytes + # Sector size (logical/physical): 512 bytes / 4096 bytes + # I/O size (minimum/optimal): 4096 bytes / 4096 bytes + # Disklabel type: dos + # Disk identifier: 0x06c86a75 + # + # Device Boot Start End Sectors Size Id Type + # /dev/sdc1 2048 209715199 209713152 100G 83 Linux + # + # Command (m for help): w + # The partition table has been altered. + # Calling ioctl() to re-read partition table. + # Syncing disks. + + +sudo mkfs -t ext4 /dev/sdc1 +sudo mkdir /datadrive +sudo mount /dev/sdc1 /datadrive + +sudo chmod g+w /datadrive +sudo chmod a+wt /datadrive + +# To ensure that the drive is remounted after a reboot, it must be added to the /etc/fstab file. +# The following assumes there are no other added drives that might be assigned to /dev/sdc1 +echo "/dev/sdc1 /datadrive ext4 defaults,nofail 1 2" | sudo tee -a /etc/fstab >/dev/null + +# make sure it worked +df -h + +########################## add swap file (optional) + +# https://support.microsoft.com/en-us/help/4010058/how-to-add-a-swap-file-in-linux-azure-virtual-machines + +#SWAP_SIZE_MB=64000 + +sudo sed -i 's/ResourceDisk.Format=n/ResourceDisk.Format=y/g' /etc/waagent.conf +sudo sed -i 's/ResourceDisk.EnableSwap=n/ResourceDisk.EnableSwap=y/g' /etc/waagent.conf +sudo sed -i 's/ResourceDisk.SwapSizeMB=0/ResourceDisk.SwapSizeMB=64000/g' /etc/waagent.conf + + +# sudo nano /etc/waagent.conf +# +# # ResourceDisk.Format=y +# # ResourceDisk.EnableSwap=y +# # ResourceDisk.SwapSizeMB=64000 + +sudo service walinuxagent restart + +# make sure it worked +free + +########################## mount smb shared drive (one time) + +sudo mkdir /mnt/fileshare +sudo mount -t cifs //$STORAGEACCT.file.core.windows.net/$SHARE_NAME /mnt/fileshare -o vers=3.0,username=$STORAGEACCT,password=$STORAGEKEY,dir_mode=0777,file_mode=0777,serverino + + +########################## init work dir + +mkdir /datadrive/work + +# copy from smb fileshare to datadrive +cp -r /mnt/fileshare/work/data /datadrive/work/data/ + +# show size +du -h --max-depth=1 /datadrive/work/data/ + + +########################## exit + +exit + + +########################## deallocate vm +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + + + + + diff --git a/example_azure/step2_config_asim.txt b/example_azure/step2_config_asim.txt new file mode 100644 index 000000000..7ea7dc00e --- /dev/null +++ b/example_azure/step2_config_asim.txt @@ -0,0 +1,49 @@ + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) + +ssh $AZ_USERNAME@$VM_IP + +############### install conda + +CONDA_HOME=/datadrive/work + +cd $CONDA_HOME + +wget http://repo.anaconda.com/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh +bash miniconda.sh -b -p $CONDA_HOME/miniconda +export PATH="/datadrive/work/miniconda/bin:$PATH" +hash -r + +conda config --set always_yes yes --set changeps1 yes +conda update conda +conda info -a + + +############### clone asim + +ASIM_HOME=/datadrive/work + +cd $ASIM_HOME +git clone https://github.com/ActivitySim/activitysim.git activitysim +cd activitysim +git checkout dev + +# create asim virtualenv + +conda remove --name asim --all + +conda create -q -n asim python=2.7 cytoolz numpy pandas pip pytables pyyaml toolz psutil +source activate asim +pip install openmatrix zbox future + +cd $ASIM_HOME/activitysim +pip install -e . +pip freeze + + +########################## deallocate vm +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + + + diff --git a/example_azure/step3_run_asim.txt b/example_azure/step3_run_asim.txt new file mode 100644 index 000000000..283f2f4ca --- /dev/null +++ b/example_azure/step3_run_asim.txt @@ -0,0 +1,52 @@ + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +AZ_VM_SIZE=Standard_E64s_v3 + +############### resize + +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +az vm resize --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME --size $AZ_VM_SIZE + +############### start + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) + + +# copy settings to example_mp/configs +scp example_mp/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml +scp example_mp/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml + +############### run + +ssh $AZ_USERNAME@$VM_IP + +export PATH="/datadrive/work/miniconda/bin:$PATH" +hash -r + +source activate asim + +cd /datadrive/work/activitysim/example_mp + +nano configs/settings.yaml + +export VECLIB_MAXIMUM_THREADS=1 # osx-specific +export OPENBLAS_NUM_THREADS=1 +export MKL_NUM_THREADS=1 +export NUMEXPR_NUM_THREADS=1 +export OMP_NUM_THREADS=1 + +python simulation.py -d /datadrive/work/data/full + +# 50% +#python simulation.py -s 2,0 -d /datadrive/work/data/full + +tar -zcvf log.tar.gz output/log/ + +exit + +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz log.tar.gz + +############### + +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME \ No newline at end of file diff --git a/example_azure/util_create_smb_fileshare.txt b/example_azure/util_create_smb_fileshare.txt new file mode 100644 index 000000000..62147b8a2 --- /dev/null +++ b/example_azure/util_create_smb_fileshare.txt @@ -0,0 +1,38 @@ + +########################## SMB File Share + +STORAGE_QUOTA_GB=2048 +SHARE_NAME=myshare + +STORAGEACCT=$(az storage account create \ + --resource-group $AZ_RESOURCE_GROUP \ + --name "mystorageacct$RANDOM" \ + --location $AZ_LOCATION \ + --sku Standard_LRS \ + --query "name" | tr -d '"') + + + +STORAGEKEY=$(az storage account keys list \ + --resource-group $AZ_RESOURCE_GROUP \ + --account-name $STORAGEACCT \ + --query "[0].value" | tr -d '"') + +az storage share create --name $SHARE_NAME \ + --quota $STORAGE_QUOTA_GB \ + --account-name $STORAGEACCT \ + --account-key $STORAGEKEY + + +echo $STORAGEACCT +echo $STORAGEKEY + + +# mount on local windows machine + +# assign $STORAGEACCT and $STORAGEKEY to Windows variables + +Test-NetConnection -ComputerName $STORAGEACCT.file.core.windows.net -Port 445 + +net use Z: \\$STORAGEACCT.file.core.windows.net\myshare /u:AZURE\$STORAGEACCT $STORAGEKEY + diff --git a/example_azure/util_resize_managed_storage.txt b/example_azure/util_resize_managed_storage.txt new file mode 100644 index 000000000..04b04c356 --- /dev/null +++ b/example_azure/util_resize_managed_storage.txt @@ -0,0 +1,59 @@ + +#AZ_VM_NUMBER=1 +AZ_VM_NAME=ubuntu$AZ_VM_NUMBER +AZ_RESOURCE_GROUP=jeffdoyle +AZ_USERNAME=azureuser + +########################## resize managed storage + +#https://docs.microsoft.com/en-us/azure/virtual-machines/linux/expand-disks + +DISK_NAME=datadisk$AZ_VM_NUMBER + + +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + +az disk list \ + --resource-group $AZ_RESOURCE_GROUP \ + --query '[*].{Name:name,Gb:diskSizeGb,Tier:accountType}' \ + --output table + +az disk update \ + --resource-group $AZ_RESOURCE_GROUP \ + --name $DISK_NAME \ + --size-gb 200 + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + +VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) + +ssh $AZ_USERNAME@$VM_IP + +sudo umount /dev/sdc1 + +sudo parted /dev/sdc + print + # Model: Msft Virtual Disk (scsi) + # Disk /dev/sdc: 215GB + # Sector size (logical/physical): 512B/4096B + # Partition Table: msdos + # Disk Flags: + # + # Number Start End Size Type File system Flags + # 1 1049kB 107GB 107GB primary ext4 + resizepart + # Partition number? 1 + # End? [107GB]? 215GB + # (parted) quit + # Information: You may need to update /etc/fstab. + quit + +sudo e2fsck -f /dev/sdc1 + +sudo resize2fs /dev/sdc1 + +sudo mount /dev/sdc1 /datadrive + + +# make sure it worked +df -h \ No newline at end of file diff --git a/example_azure/vm_sizes.txt b/example_azure/vm_sizes.txt new file mode 100644 index 000000000..1acecb685 --- /dev/null +++ b/example_azure/vm_sizes.txt @@ -0,0 +1,24 @@ +# # 2 processor, 8 GiB RAM, 16 GiB SSD Temp, $0.096/hour +# Standard_D2s_v3 +# +# # 4 processor, 16 GiB RAM, 32 GiB SSD Temp, $0.192/hour +# Standard_D4s_v3 + +# 16 processor, 64 GiB RAM, 128 GiB SSD Temp, $0.768/hour +Standard_D16s_v3 + +# 32 processor, 128 GiB RAM, 256 GiB SSD Temp, $1.536/hour +Standard_D32s_v3 + +# 64 processor, 256 GiB RAM, 512 GiB SSD Temp, $3.072/hour +Standard_D64s_v3 + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +Standard_E64s_v3 + +# 128 cpu 2TB ram $13.34/hour +Standard_M128s + + + +az vm list-sizes --location eastus --output table \ No newline at end of file diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 949cb252b..394f12e20 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -87,6 +87,7 @@ def log_settings(injectables): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables + injectables = list(set(injectables) | set(['data_dir', 'configs_dir', 'output_dir'])) injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None diff --git a/example_multi/README.MD b/example_multiple_zone/README.MD similarity index 100% rename from example_multi/README.MD rename to example_multiple_zone/README.MD diff --git a/example_multi/configs/best_transit_path.csv b/example_multiple_zone/configs/best_transit_path.csv similarity index 100% rename from example_multi/configs/best_transit_path.csv rename to example_multiple_zone/configs/best_transit_path.csv diff --git a/example_multi/configs/best_transit_path.yaml b/example_multiple_zone/configs/best_transit_path.yaml similarity index 100% rename from example_multi/configs/best_transit_path.yaml rename to example_multiple_zone/configs/best_transit_path.yaml diff --git a/example_multi/configs/logging.yaml b/example_multiple_zone/configs/logging.yaml similarity index 100% rename from example_multi/configs/logging.yaml rename to example_multiple_zone/configs/logging.yaml diff --git a/example_multi/configs/settings.yaml b/example_multiple_zone/configs/settings.yaml similarity index 100% rename from example_multi/configs/settings.yaml rename to example_multiple_zone/configs/settings.yaml diff --git a/example_multi/data/skims.omx b/example_multiple_zone/data/skims.omx similarity index 100% rename from example_multi/data/skims.omx rename to example_multiple_zone/data/skims.omx diff --git a/example_multi/dump_data.py b/example_multiple_zone/dump_data.py similarity index 100% rename from example_multi/dump_data.py rename to example_multiple_zone/dump_data.py diff --git a/example_multi/extensions/__init__.py b/example_multiple_zone/extensions/__init__.py similarity index 100% rename from example_multi/extensions/__init__.py rename to example_multiple_zone/extensions/__init__.py diff --git a/example_multi/extensions/los.py b/example_multiple_zone/extensions/los.py similarity index 100% rename from example_multi/extensions/los.py rename to example_multiple_zone/extensions/los.py diff --git a/example_multi/extensions/models.py b/example_multiple_zone/extensions/models.py similarity index 100% rename from example_multi/extensions/models.py rename to example_multiple_zone/extensions/models.py diff --git a/example_multi/extensions/skims.py b/example_multiple_zone/extensions/skims.py similarity index 100% rename from example_multi/extensions/skims.py rename to example_multiple_zone/extensions/skims.py diff --git a/example_multi/import_data.py b/example_multiple_zone/import_data.py similarity index 100% rename from example_multi/import_data.py rename to example_multiple_zone/import_data.py diff --git a/example_multi/output/.gitignore b/example_multiple_zone/output/.gitignore similarity index 100% rename from example_multi/output/.gitignore rename to example_multiple_zone/output/.gitignore diff --git a/example_multi/simulation.py b/example_multiple_zone/simulation.py similarity index 100% rename from example_multi/simulation.py rename to example_multiple_zone/simulation.py diff --git a/example_mp_stride/.gitignore b/example_stride/.gitignore similarity index 100% rename from example_mp_stride/.gitignore rename to example_stride/.gitignore diff --git a/example_mp_stride/coalesce.py b/example_stride/coalesce.py similarity index 100% rename from example_mp_stride/coalesce.py rename to example_stride/coalesce.py diff --git a/example_mp_stride/configs_report/logging.yaml b/example_stride/configs_report/logging.yaml similarity index 100% rename from example_mp_stride/configs_report/logging.yaml rename to example_stride/configs_report/logging.yaml diff --git a/example_mp_stride/configs_report/settings.yaml b/example_stride/configs_report/settings.yaml similarity index 100% rename from example_mp_stride/configs_report/settings.yaml rename to example_stride/configs_report/settings.yaml diff --git a/example_mp_stride/configs_sim/logging.yaml b/example_stride/configs_sim/logging.yaml similarity index 100% rename from example_mp_stride/configs_sim/logging.yaml rename to example_stride/configs_sim/logging.yaml diff --git a/example_mp_stride/configs_sim/settings.yaml b/example_stride/configs_sim/settings.yaml similarity index 100% rename from example_mp_stride/configs_sim/settings.yaml rename to example_stride/configs_sim/settings.yaml diff --git a/example_mp_stride/output/.gitignore b/example_stride/output/.gitignore similarity index 100% rename from example_mp_stride/output/.gitignore rename to example_stride/output/.gitignore diff --git a/example_mp_stride/output/log/.gitignore b/example_stride/output/log/.gitignore similarity index 100% rename from example_mp_stride/output/log/.gitignore rename to example_stride/output/log/.gitignore diff --git a/example_mp_stride/output/trace/.gitignore b/example_stride/output/trace/.gitignore similarity index 100% rename from example_mp_stride/output/trace/.gitignore rename to example_stride/output/trace/.gitignore diff --git a/example_mp_stride/script.txt b/example_stride/script.txt similarity index 100% rename from example_mp_stride/script.txt rename to example_stride/script.txt diff --git a/example_mp_stride/simulation.py b/example_stride/simulation.py similarity index 96% rename from example_mp_stride/simulation.py rename to example_stride/simulation.py index 949cb252b..394f12e20 100644 --- a/example_mp_stride/simulation.py +++ b/example_stride/simulation.py @@ -87,6 +87,7 @@ def log_settings(injectables): if run_list['multiprocess']: # do this after config.handle_standard_args, as command line args may override injectables + injectables = list(set(injectables) | set(['data_dir', 'configs_dir', 'output_dir'])) injectables = {k: inject.get_injectable(k) for k in injectables} else: injectables = None From 9f51507b5b05d7f6785f7fe0fdf4a5aaf44ed64a Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 28 Nov 2018 14:58:43 -0500 Subject: [PATCH 051/122] consolidate work and school location sub-models --- .../abm/models/atwork_subtour_destination.py | 4 +- activitysim/abm/models/school_location.py | 131 ++++++++++------ activitysim/abm/models/workplace_location.py | 146 +++++++++++------- activitysim/abm/test/configs_mp/settings.yaml | 10 +- activitysim/abm/test/output/.gitignore | 4 +- activitysim/abm/test/test_pipeline.py | 16 +- docs/abmexample.rst | 4 +- example/configs/settings.yaml | 8 +- example_azure/example_mp/settings.yaml | 6 +- example_azure/util_resize_managed_storage.txt | 8 +- example_mp/configs/settings.yaml | 12 +- example_stride/configs_sim/settings.yaml | 6 +- 12 files changed, 198 insertions(+), 157 deletions(-) diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index ed00b0b63..434ba986b 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -94,10 +94,10 @@ def atwork_subtour_destination_logsums(persons_merged, skim_dict, skim_stack, chunk_size, trace_hh_id): """ - add logsum column to existing workplace_location_sample able + add logsum column to existing atwork_subtour_destination_sample able logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair - in workplace_location_sample, and computing the logsum of all the utilities + in atwork_subtour_destination_sample, and computing the logsum of all the utilities +-----------+--------------+----------------+------------+----------------+ | person_id | dest_TAZ | rand | pick_count | logsum (added) | diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 829448451..838a62315 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -41,8 +41,7 @@ SCHOOL_TYPE_ID = OrderedDict([('university', 1), ('highschool', 2), ('gradeschool', 3)]) -@inject.step() -def school_location_sample( +def run_school_location_sample( persons_merged, skim_dict, land_use, size_terms, @@ -70,10 +69,9 @@ def school_location_sample( model_settings = config.read_model_settings('school_location.yaml') model_spec = simulate.read_model_spec(file_name=model_settings['SAMPLE_SPEC']) - choosers = persons_merged.to_frame() # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = choosers[chooser_columns] + choosers = persons_merged[chooser_columns] size_terms = tour_destination_size_terms(land_use, size_terms, 'school') @@ -136,22 +134,21 @@ def school_location_sample( logger.info("Skipping %s: add_null_results" % model_name) choices = pd.DataFrame() - inject.add_table('school_location_sample', choices) + return choices -@inject.step() -def school_location_logsums( +def run_school_location_logsums( persons_merged, skim_dict, skim_stack, - school_location_sample, + location_sample_df, chunk_size, trace_hh_id): """ - add logsum column to existing school_location_sample able + compute and add mode_choice_logsum column to location_sample_df logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair - in school_location_sample, and computing the logsum of all the utilities + in location_sample_df, and computing the logsum of all the utilities +-----------+--------------+----------------+------------+----------------+ | person_id | dest_TAZ | rand | pick_count | logsum (added) | @@ -172,15 +169,12 @@ def school_location_logsums( logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - location_sample = school_location_sample.to_frame() - - if location_sample.shape[0] == 0: + if location_sample_df.empty: tracing.no_results(model_name) - return + return location_sample_df - logger.info("Running school_location_logsums with %s rows" % location_sample.shape[0]) + logger.info("Running school_location_logsums with %s rows" % location_sample_df.shape[0]) - persons_merged = persons_merged.to_frame() # - only include columns actually used in spec persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) @@ -189,7 +183,7 @@ def school_location_logsums( tour_purpose = 'univ' if school_type == 'university' else 'school' - choosers = location_sample[location_sample['school_type'] == school_type_id] + choosers = location_sample_df[location_sample_df['school_type'] == school_type_id] if choosers.shape[0] == 0: logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) @@ -215,23 +209,25 @@ def school_location_logsums( logsums = pd.concat(logsums_list) # add_column series should have an index matching the table to which it is being added - # logsums does, since school_location_sample was on left side of merge creating choosers + # logsums does, since location_sample_df was on left side of merge creating choosers # "add_column series should have an index matching the table to which it is being added" # when the index has duplicates, however, in the special case that the series index exactly # matches the table index, then the series value order is preserved. - # logsums does align with school_location_sample as we loop through it in exactly the same + # logsums does align with location_sample_df as we loop through it in exactly the same # order as we did when we created it - inject.add_column('school_location_sample', 'mode_choice_logsum', logsums) + location_sample_df['mode_choice_logsum'] = logsums + return location_sample_df -@inject.step() -def school_location_simulate(persons_merged, persons, - school_location_sample, - skim_dict, - land_use, size_terms, - chunk_size, - trace_hh_id): + +def run_school_location_simulate( + persons_merged, + location_sample_df, + skim_dict, + land_use, size_terms, + chunk_size, + trace_hh_id): """ School location model on school_location_sample annotated with mode_choice logsum to select a school_taz from sample alternatives @@ -241,15 +237,13 @@ def school_location_simulate(persons_merged, persons, model_spec = simulate.read_model_spec(file_name=model_settings['SPEC']) - NO_SCHOOL_TAZ = -1 + if location_sample_df.empty: - location_sample = school_location_sample.to_frame() - persons = persons.to_frame() + logger.info("%s no school-goers" % model_name) + choices = pd.Series() - # if there are any school-goers - if location_sample.shape[0] > 0: + else: - choosers = persons_merged.to_frame() destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'school') alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] @@ -268,7 +262,7 @@ def school_location_simulate(persons_merged, persons, # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = choosers[chooser_columns] + choosers = persons_merged[chooser_columns] choices_list = [] for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): @@ -282,7 +276,7 @@ def school_location_simulate(persons_merged, persons, continue alts_segment = \ - location_sample[location_sample['school_type'] == school_type_id] + location_sample_df[location_sample_df['school_type'] == school_type_id] # alternatives are pre-sampled and annotated with logsums and pick_count # but we have to merge size_terms column into alt sample list @@ -304,29 +298,68 @@ def school_location_simulate(persons_merged, persons, choices = pd.concat(choices_list) - # We only chose school locations for the subset of persons who go to school - # so we backfill the empty choices with -1 to code as no school location - persons['school_taz'] = choices.reindex(persons.index).fillna(NO_SCHOOL_TAZ).astype(int) + return choices - tracing.print_summary('school_taz', choices, describe=True) - else: - - # no school-goers (but we still want to annotate persons) - persons['school_taz'] = NO_SCHOOL_TAZ +@inject.step() +def school_location( + persons_merged, persons, + skim_dict, skim_stack, + land_use, size_terms, + chunk_size, + trace_hh_id): - logger.info("%s no school-goers" % model_name) + persons_merged_df = persons_merged.to_frame() + + # - school_location_sample + location_sample_df = \ + run_school_location_sample( + persons_merged_df, + skim_dict, + land_use, size_terms, + chunk_size, + trace_hh_id) + + # - school_location_logsums + location_sample_df = \ + run_school_location_logsums( + persons_merged_df, + skim_dict, skim_stack, + location_sample_df, + chunk_size, + trace_hh_id) + + # - school_location_simulate + choices = \ + run_school_location_simulate( + persons_merged_df, + location_sample_df, + skim_dict, + land_use, size_terms, + chunk_size, + trace_hh_id) + + tracing.print_summary('school_taz', choices, describe=True) + + persons_df = persons.to_frame() + + # We only chose school locations for the subset of persons who go to school + # so we backfill the empty choices with -1 to code as no school location + NO_SCHOOL_TAZ = -1 + persons_df['school_taz'] = choices.reindex(persons_df.index).fillna(NO_SCHOOL_TAZ).astype(int) + tracing.print_summary('school_taz', choices, describe=True) + # - annotate persons + model_name = 'school_location' + model_settings = config.read_model_settings('school_location.yaml') expressions.assign_columns( - df=persons, + df=persons_df, model_settings=model_settings.get('annotate_persons'), trace_label=tracing.extend_trace_label(model_name, 'annotate_persons')) - pipeline.replace_table("persons", persons) - - pipeline.drop_table('school_location_sample') + pipeline.replace_table("persons", persons_df) if trace_hh_id: - tracing.trace_df(persons, + tracing.trace_df(persons_df, label="school_location", warn_if_empty=True) diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 2d6db236e..4e6686c07 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -36,11 +36,11 @@ logger = logging.getLogger(__name__) -@inject.step() -def workplace_location_sample(persons_merged, - skim_dict, - land_use, size_terms, - chunk_size, trace_hh_id): +def run_workplace_location_sample( + persons_merged, + skim_dict, + land_use, size_terms, + chunk_size, trace_hh_id): """ build a table of workers * all zones in order to select a sample of alternative work locations. @@ -57,13 +57,12 @@ def workplace_location_sample(persons_merged, model_spec = simulate.read_model_spec(file_name='workplace_location_sample.csv') # FIXME - only choose workplace_location of workers? is this the right criteria? - choosers = persons_merged.to_frame() - choosers = choosers[choosers.is_worker] + choosers = persons_merged[persons_merged.is_worker] - if choosers.shape[0] == 0: + if choosers.empty: logger.info("Skipping %s: no workers" % trace_label) - inject.add_table('workplace_location_sample', pd.DataFrame()) - return + choices = pd.DataFrame() + return choices # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] @@ -99,14 +98,14 @@ def workplace_location_sample(persons_merged, chunk_size=chunk_size, trace_label=trace_label) - inject.add_table('workplace_location_sample', choices) + return choices -@inject.step() -def workplace_location_logsums(persons_merged, - skim_dict, skim_stack, - workplace_location_sample, - chunk_size, trace_hh_id): +def run_workplace_location_logsums( + persons_merged_df, + skim_dict, skim_stack, + location_sample_df, + chunk_size, trace_hh_id): """ add logsum column to existing workplace_location_sample able @@ -130,22 +129,21 @@ def workplace_location_logsums(persons_merged, trace_label = 'workplace_location_logsums' - location_sample = workplace_location_sample.to_frame() - if location_sample.shape[0] == 0: + if location_sample_df.empty: tracing.no_results(trace_label) - return + return location_sample_df model_settings = config.read_model_settings('workplace_location.yaml') logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - persons_merged = persons_merged.to_frame() # FIXME - MEMORY HACK - only include columns actually used in spec - persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) + persons_merged_df = \ + logsum.filter_chooser_columns(persons_merged_df, logsum_settings, model_settings) - logger.info("Running workplace_location_logsums with %s rows" % len(location_sample)) + logger.info("Running workplace_location_logsums with %s rows" % len(location_sample_df)) - choosers = pd.merge(location_sample, - persons_merged, + choosers = pd.merge(location_sample_df, + persons_merged_df, left_index=True, right_index=True, how="left") @@ -163,15 +161,17 @@ def workplace_location_logsums(persons_merged, # when the index has duplicates, however, in the special case that the series index exactly # matches the table index, then the series value order is preserved # logsums now does, since workplace_location_sample was on left side of merge de-dup merge - inject.add_column('workplace_location_sample', 'mode_choice_logsum', logsums) + location_sample_df['mode_choice_logsum'] = logsums + return location_sample_df -@inject.step() -def workplace_location_simulate(persons_merged, persons, - workplace_location_sample, - skim_dict, - land_use, size_terms, - chunk_size, trace_hh_id): + +def run_workplace_location_simulate( + persons_merged, + location_sample_df, + skim_dict, + land_use, size_terms, + chunk_size, trace_hh_id): """ Workplace location model on workplace_location_sample annotated with mode_choice logsum to select a work_taz from sample alternatives @@ -181,15 +181,12 @@ def workplace_location_simulate(persons_merged, persons, model_settings = config.read_model_settings('workplace_location.yaml') model_spec = simulate.read_model_spec(file_name='workplace_location.csv') - NO_WORKPLACE_TAZ = -1 - - location_sample = workplace_location_sample.to_frame() - persons = persons.to_frame() - - if location_sample.shape[0] > 0: + if location_sample_df.empty: + logger.info("%s no workers" % trace_label) + choices = pd.Series() + else: - choosers = persons_merged.to_frame() - choosers = choosers[choosers.is_worker] + choosers = persons_merged[persons_merged.is_worker] # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] @@ -199,11 +196,10 @@ def workplace_location_simulate(persons_merged, persons, # alternatives are pre-sampled and annotated with logsums and pick_count # but we have to merge additional alt columns into alt sample list - location_sample = workplace_location_sample.to_frame() destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'work') alternatives = \ - pd.merge(location_sample, destination_size_terms, + pd.merge(location_sample_df, destination_size_terms, left_on=alt_dest_col_name, right_index=True, how="left") logger.info("Running workplace_location_simulate with %d persons" % len(choosers)) @@ -231,26 +227,68 @@ def workplace_location_simulate(persons_merged, persons, trace_label=trace_label, trace_choice_name='workplace_location') - persons['workplace_taz'] = \ - choices.reindex(persons.index).fillna(NO_WORKPLACE_TAZ).astype(int) + return choices - else: - # no workers (but we still want to annotate persons) - persons['workplace_taz'] = NO_WORKPLACE_TAZ +@inject.step() +def workplace_location( + persons_merged, persons, + skim_dict, skim_stack, + land_use, size_terms, + chunk_size, trace_hh_id): + + persons_merged_df = persons_merged.to_frame() + + # - workplace_location_sample + location_sample_df = \ + run_workplace_location_sample( + persons_merged_df, + skim_dict, + land_use, size_terms, + chunk_size, + trace_hh_id) + + # - workplace_location_logsums + location_sample_df = \ + run_workplace_location_logsums( + persons_merged_df, + skim_dict, skim_stack, + location_sample_df, + chunk_size, + trace_hh_id) + + # - school_location_simulate + choices = \ + run_workplace_location_simulate( + persons_merged_df, + location_sample_df, + skim_dict, + land_use, size_terms, + chunk_size, + trace_hh_id) + + tracing.print_summary('workplace_taz', choices, describe=True) + + persons_df = persons.to_frame() + + # We only chose school locations for the subset of persons who go to school + # so we backfill the empty choices with -1 to code as no school location + NO_WORKPLACE_TAZ = -1 + persons_df['workplace_taz'] = \ + choices.reindex(persons_df.index).fillna(NO_WORKPLACE_TAZ).astype(int) + + # - annotate persons + model_name = 'workplace_location' + model_settings = config.read_model_settings('workplace_location.yaml') expressions.assign_columns( - df=persons, + df=persons_df, model_settings=model_settings.get('annotate_persons'), - trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) - - pipeline.replace_table("persons", persons) - - pipeline.drop_table('workplace_location_sample') + trace_label=tracing.extend_trace_label(model_name, 'annotate_persons')) - tracing.print_summary('workplace_taz', persons.workplace_taz, describe=True) + pipeline.replace_table("persons", persons_df) if trace_hh_id: - tracing.trace_df(persons, + tracing.trace_df(persons_df, label="workplace_location", warn_if_empty=True) diff --git a/activitysim/abm/test/configs_mp/settings.yaml b/activitysim/abm/test/configs_mp/settings.yaml index 9e80f88da..802bdc1f1 100644 --- a/activitysim/abm/test/configs_mp/settings.yaml +++ b/activitysim/abm/test/configs_mp/settings.yaml @@ -8,12 +8,8 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - school_location_sample - - school_location_logsums - - school_location_simulate - - workplace_location_sample - - workplace_location_logsums - - workplace_location_simulate + - school_location + - workplace_location - auto_ownership_simulate - write_data_dictionary - write_tables @@ -33,7 +29,7 @@ multiprocess_steps: - name: mp_initialize_households begin: initialize_households - name: mp_households - begin: school_location_sample + begin: school_location num_processes: 2 stagger: 4 #chunk_size: 1000000000 diff --git a/activitysim/abm/test/output/.gitignore b/activitysim/abm/test/output/.gitignore index b987779f4..0c0762ada 100644 --- a/activitysim/abm/test/output/.gitignore +++ b/activitysim/abm/test/output/.gitignore @@ -1,6 +1,4 @@ *.csv -*.log -*.prof -*.h5 *.txt +*.h5 *.yaml diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index ef15931d5..618fb5a34 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -150,12 +150,8 @@ def test_mini_pipeline_run(): 'initialize_landuse', 'compute_accessibility', 'initialize_households', - 'school_location_sample', - 'school_location_logsums', - 'school_location_simulate', - 'workplace_location_sample', - 'workplace_location_logsums', - 'workplace_location_simulate', + 'school_location', + 'workplace_location', 'auto_ownership_simulate' ] @@ -200,7 +196,7 @@ def test_mini_pipeline_run2(): prev_checkpoint_count = len(checkpoints_df.index) # print "checkpoints_df\n", checkpoints_df[['checkpoint_name']] - assert prev_checkpoint_count == 12 + assert prev_checkpoint_count == 8 pipeline.open_pipeline('auto_ownership_simulate') @@ -302,7 +298,7 @@ def get_trace_csv(file_name): return df -EXPECT_TOUR_COUNT = 305 +EXPECT_TOUR_COUNT = 307 def regress_tour_modes(tours_df): @@ -322,7 +318,7 @@ def regress_tour_modes(tours_df): 94247744 WALK 3249922 eatout 1 non_mandatory 94247771 SHARED3FREE 3249923 eat 1 atwork 94247794 WALK_LOC 3249923 work 1 mandatory - 94247773 DRIVEALONEFREE 3249923 eatout 1 non_mandatory + 94247773 DRIVEALONEFREE 3249923 social 1 non_mandatory """ EXPECT_PERSON_IDS = [ @@ -340,7 +336,7 @@ def regress_tour_modes(tours_df): 'eatout', 'eat', 'work', - 'eatout', + 'social', ] EXPECT_MODES = [ diff --git a/docs/abmexample.rst b/docs/abmexample.rst index 1e77a65f7..c26aff96f 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -510,9 +510,7 @@ The ``models`` setting contains the specification of the data pipeline model ste - initialize_landuse - compute_accessibility - initialize_households - - school_location_sample - - school_location_logsums - - school_location_simulate + - school_location - workplace_location_sample - workplace_location_logsums - workplace_location_simulate diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index cbbc80023..5a288470f 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -25,12 +25,8 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - school_location_sample - - school_location_logsums - - school_location_simulate - - workplace_location_sample - - workplace_location_logsums - - workplace_location_simulate + - school_location + - workplace_location - auto_ownership_simulate - cdap_simulate - mandatory_tour_frequency diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml index c5dbb7219..83a35fd9c 100644 --- a/example_azure/example_mp/settings.yaml +++ b/example_azure/example_mp/settings.yaml @@ -24,9 +24,7 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - _school_location_sample - - _school_location_logsums - - school_location_simulate + - school_location - _workplace_location_sample - _workplace_location_logsums - workplace_location_simulate @@ -64,7 +62,7 @@ multiprocess_steps: - name: mp_initialize begin: initialize_landuse - name: mp_households - begin: _school_location_sample + begin: school_location slice: tables: - households diff --git a/example_azure/util_resize_managed_storage.txt b/example_azure/util_resize_managed_storage.txt index 04b04c356..30dee96ca 100644 --- a/example_azure/util_resize_managed_storage.txt +++ b/example_azure/util_resize_managed_storage.txt @@ -1,9 +1,3 @@ - -#AZ_VM_NUMBER=1 -AZ_VM_NAME=ubuntu$AZ_VM_NUMBER -AZ_RESOURCE_GROUP=jeffdoyle -AZ_USERNAME=azureuser - ########################## resize managed storage #https://docs.microsoft.com/en-us/azure/virtual-machines/linux/expand-disks @@ -56,4 +50,4 @@ sudo mount /dev/sdc1 /datadrive # make sure it worked -df -h \ No newline at end of file +df -h diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index fc3eff021..23d343b30 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -78,12 +78,8 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - _school_location_sample - - _school_location_logsums - - school_location_simulate - - _workplace_location_sample - - _workplace_location_logsums - - workplace_location_simulate + - school_location + - workplace_location - auto_ownership_simulate - cdap_simulate - mandatory_tour_frequency @@ -118,7 +114,7 @@ multiprocess_steps: - name: mp_initialize begin: initialize_landuse - name: mp_households - begin: _school_location_sample + begin: school_location #num_processes: 9 #stagger: 30 #chunk_size: 1000000000 @@ -143,7 +139,7 @@ multiprocess_steps: # - name: mp_initialize_households # begin: initialize_households # - name: mp_households -# begin: _school_location_sample +# begin: school_location # # num_processes = 0 means use all available cpus # num_processes: 2 # slice: diff --git a/example_stride/configs_sim/settings.yaml b/example_stride/configs_sim/settings.yaml index a2e0e876e..f8657c0b4 100644 --- a/example_stride/configs_sim/settings.yaml +++ b/example_stride/configs_sim/settings.yaml @@ -22,9 +22,7 @@ models: - initialize_landuse - compute_accessibility - initialize_households - - _school_location_sample - - _school_location_logsums - - school_location_simulate + - school_location - _workplace_location_sample - _workplace_location_logsums - workplace_location_simulate @@ -60,7 +58,7 @@ multiprocess_steps: - name: mp_initialize begin: initialize_landuse - name: mp_households - begin: _school_location_sample + begin: school_location slice: tables: - households From f4609ee352cbdf1452bbbd2f78c75af36aa295a9 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 6 Dec 2018 10:05:35 -0500 Subject: [PATCH 052/122] ShadowPriceCalculator with share data sycnhronization --- .../abm/models/atwork_subtour_destination.py | 2 +- activitysim/abm/models/initialize.py | 23 ++ .../abm/models/joint_tour_destination.py | 2 +- .../abm/models/non_mandatory_destination.py | 3 +- activitysim/abm/models/school_location.py | 222 ++++++++---- activitysim/abm/models/trip_destination.py | 3 +- .../abm/models/util/tour_destination.py | 1 + activitysim/abm/models/workplace_location.py | 6 +- activitysim/abm/tables/__init__.py | 1 + activitysim/abm/tables/constants.py | 5 + activitysim/abm/tables/shadow_pricing.py | 321 ++++++++++++++++++ activitysim/abm/tables/size_terms.py | 122 +++++++ activitysim/abm/tables/skims.py | 34 +- .../abm/test/configs/annotate_persons.csv | 13 +- activitysim/core/mp_tasks.py | 70 +++- example/configs/annotate_persons.csv | 4 + example/configs/school_location.yaml | 40 ++- 17 files changed, 753 insertions(+), 119 deletions(-) create mode 100644 activitysim/abm/tables/shadow_pricing.py diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index 434ba986b..2ee1da094 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -20,7 +20,7 @@ from activitysim.core.util import assign_in_place from .util import logsums as logsum -from .util.tour_destination import tour_destination_size_terms +from activitysim.abm.tables.size_terms import tour_destination_size_terms logger = logging.getLogger(__name__) DUMP = False diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 3a77b4cbb..c64788940 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -20,6 +20,8 @@ from .util import expressions +from activitysim.abm.tables.size_terms import destination_predicted_size + logger = logging.getLogger(__name__) @@ -86,6 +88,27 @@ def initialize_households(): annotate_tables(model_settings, trace_label) + # - ShadowPriceCalculator predicted_size tables + for dest_model_settings_file in ['school_location.yaml']: + + dest_choice_model_settings = config.read_model_settings(dest_model_settings_file) + selector = dest_choice_model_settings['SELECTOR'] + segment_ids = dest_choice_model_settings['SEGMENT_IDS'] + chooser_table_name = dest_choice_model_settings['CHOOSER_TABLE_NAME'] + chooser_segment_column = dest_choice_model_settings['CHOOSER_SEGMENT_COLUMN'] + destination_size_table_name = dest_choice_model_settings['DESTINATION_SIZE_TABLE'] + + logger.info("%s creating %s" % (trace_label, destination_size_table_name)) + + size_df = \ + destination_predicted_size( + chooser_table_name, + selector, + chooser_segment_column, + segment_ids) + inject.add_table(destination_size_table_name, size_df) + + t0 = tracing.print_elapsed_time() inject.get_table('person_windows').to_frame() t0 = tracing.print_elapsed_time("preload person_windows", t0, debug=True) diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index c72b0c9d6..3d7484762 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -26,7 +26,7 @@ from activitysim.core.util import assign_in_place from .util import logsums as logsum -from .util.tour_destination import tour_destination_size_terms +from activitysim.abm.tables.size_terms import tour_destination_size_terms logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/non_mandatory_destination.py b/activitysim/abm/models/non_mandatory_destination.py index f8a283831..e32e80cb5 100644 --- a/activitysim/abm/models/non_mandatory_destination.py +++ b/activitysim/abm/models/non_mandatory_destination.py @@ -18,7 +18,8 @@ from activitysim.core import simulate from activitysim.core.util import assign_in_place -from .util.tour_destination import tour_destination_size_terms +from activitysim.abm.tables.size_terms import tour_destination_size_terms + logger = logging.getLogger(__name__) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 838a62315..07b2753d9 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -23,10 +23,11 @@ from activitysim.core.interaction_sample import interaction_sample from activitysim.core.util import reindex -from activitysim.core.util import left_merge_on_index_and_col + +from activitysim.abm.tables import shadow_pricing from .util import logsums as logsum -from .util.tour_destination import tour_destination_size_terms + from .util import expressions @@ -37,16 +38,23 @@ logger = logging.getLogger(__name__) -# use int not str to identify school type in sample df -SCHOOL_TYPE_ID = OrderedDict([('university', 1), ('highschool', 2), ('gradeschool', 3)]) + +NO_SCHOOL_TAZ = -1 + + +# we want to iterate over segment_ids in the same order every time +def order_dict_by_keys(segment_ids): + return OrderedDict([(k, segment_ids[k]) for k in sorted(segment_ids.keys())]) def run_school_location_sample( persons_merged, skim_dict, - land_use, size_terms, + dest_size_terms, + model_settings, chunk_size, - trace_hh_id): + trace_hh_id, + trace_label): """ build a table of persons * all zones to select a sample of alternative school locations. @@ -65,15 +73,13 @@ def run_school_location_sample( +-----------+--------------+----------------+------------+ """ - model_name = 'school_location_sample' - model_settings = config.read_model_settings('school_location.yaml') model_spec = simulate.read_model_spec(file_name=model_settings['SAMPLE_SPEC']) # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] choosers = persons_merged[chooser_columns] - size_terms = tour_destination_size_terms(land_use, size_terms, 'school') + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] @@ -88,50 +94,53 @@ def run_school_location_sample( locals_d = { 'skims': skims } - constants = config.get_model_constants(model_settings) - if constants is not None: - locals_d.update(constants) + constants_dict = config.get_model_constants(model_settings) + if constants_dict is not None: + locals_d.update(constants_dict) + + # we want to iterate over segment_ids in the same order in sample, logsums, and simulate + segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) choices_list = [] - for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): + for segment_name, segment_id in iteritems(segment_ids): - locals_d['segment'] = school_type + locals_d['segment'] = segment_name - choosers_segment = choosers[choosers["is_" + school_type]] + choosers_segment = choosers[choosers[chooser_segment_column] == segment_id] if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers", model_name, school_type) + logger.info("%s skipping school_type %s: no choosers", trace_label, segment_name) continue # alts indexed by taz with one column containing size_term for this tour_type - alternatives_segment = size_terms[[school_type]] + alternatives_segment = dest_size_terms[[segment_name]] # no point in considering impossible alternatives (where dest size term is zero) - alternatives_segment = alternatives_segment[alternatives_segment[school_type] > 0] + alternatives_segment = alternatives_segment[alternatives_segment[segment_name] > 0] - logger.info("school_type %s: %s persons %s alternatives" % - (school_type, len(choosers_segment), len(alternatives_segment))) + logger.info("%s segment %s: %s persons %s alternatives" % + (trace_label, segment_name, len(choosers_segment), len(alternatives_segment))) choices = interaction_sample( choosers_segment, alternatives_segment, sample_size=sample_size, alt_col_name=alt_dest_col_name, - spec=model_spec[[school_type]], + spec=model_spec[[segment_name]], skims=skims, locals_d=locals_d, chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(model_name, school_type)) + trace_label=tracing.extend_trace_label(trace_label, segment_name)) - choices['school_type'] = school_type_id + choices['segment_id'] = segment_id choices_list.append(choices) if len(choices_list) > 0: choices = pd.concat(choices_list) # - NARROW - choices['school_type'] = choices['school_type'].astype(np.uint8) + choices['segment_id'] = choices['segment_id'].astype(np.uint8) else: - logger.info("Skipping %s: add_null_results" % model_name) + logger.info("Skipping %s: add_null_results" % trace_label) choices = pd.DataFrame() return choices @@ -141,8 +150,10 @@ def run_school_location_logsums( persons_merged, skim_dict, skim_stack, location_sample_df, + model_settings, chunk_size, - trace_hh_id): + trace_hh_id, + trace_label): """ compute and add mode_choice_logsum column to location_sample_df @@ -164,13 +175,11 @@ def run_school_location_logsums( | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | +-----------+--------------+----------------+------------+----------------+ """ - model_name = 'school_location_logsums' - model_settings = config.read_model_settings('school_location.yaml') logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) if location_sample_df.empty: - tracing.no_results(model_name) + tracing.no_results(trace_label) return location_sample_df logger.info("Running school_location_logsums with %s rows" % location_sample_df.shape[0]) @@ -178,15 +187,19 @@ def run_school_location_logsums( # - only include columns actually used in spec persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) + # we want to iterate over segment_ids in the same order in sample, logsums, and simulate + segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) + logsums_list = [] - for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): + for segment_name, segment_id in iteritems(segment_ids): - tour_purpose = 'univ' if school_type == 'university' else 'school' + # FIXME - pathological knowledge of relation between segment name and tour_purpose + tour_purpose = 'univ' if segment_name == 'university' else 'school' - choosers = location_sample_df[location_sample_df['school_type'] == school_type_id] + choosers = location_sample_df[location_sample_df['segment_id'] == segment_id] if choosers.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) + logger.info("%s skipping school_type %s: no choosers" % (trace_label, segment_name)) continue choosers = pd.merge( @@ -202,7 +215,7 @@ def run_school_location_logsums( logsum_settings, model_settings, skim_dict, skim_stack, chunk_size, trace_hh_id, - tracing.extend_trace_label(model_name, school_type)) + tracing.extend_trace_label(trace_label, segment_name)) logsums_list.append(logsums) @@ -225,27 +238,25 @@ def run_school_location_simulate( persons_merged, location_sample_df, skim_dict, - land_use, size_terms, + dest_size_terms, + model_settings, chunk_size, - trace_hh_id): + trace_hh_id, + trace_label): """ School location model on school_location_sample annotated with mode_choice logsum to select a school_taz from sample alternatives """ - model_name = 'school_location' - model_settings = config.read_model_settings('school_location.yaml') model_spec = simulate.read_model_spec(file_name=model_settings['SPEC']) if location_sample_df.empty: - logger.info("%s no school-goers" % model_name) + logger.info("%s no school-goers" % trace_label) choices = pd.Series() else: - destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'school') - alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers @@ -256,43 +267,46 @@ def run_school_location_simulate( locals_d = { 'skims': skims, } - constants = config.get_model_constants(model_settings) - if constants is not None: - locals_d.update(constants) + constants_dict = config.get_model_constants(model_settings) + if constants_dict is not None: + locals_d.update(constants_dict) # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] choosers = persons_merged[chooser_columns] + # we want to iterate over segment_ids in the same order in sample, logsums, and simulate + segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) + choices_list = [] - for school_type, school_type_id in iteritems(SCHOOL_TYPE_ID): + for segment_name, segment_id in iteritems(segment_ids): - locals_d['segment'] = school_type + locals_d['segment'] = segment_name - choosers_segment = choosers[choosers["is_" + school_type]] + choosers_segment = choosers[choosers['school_segment'] == segment_id] if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (model_name, school_type)) + logger.info("%s skipping school_type %s: no choosers" % (trace_label, segment_name)) continue alts_segment = \ - location_sample_df[location_sample_df['school_type'] == school_type_id] + location_sample_df[location_sample_df['segment_id'] == segment_id] # alternatives are pre-sampled and annotated with logsums and pick_count - # but we have to merge size_terms column into alt sample list - alts_segment[school_type] = \ - reindex(destination_size_terms[school_type], alts_segment[alt_dest_col_name]) + # but we have to merge dest_choice_size column into alt sample list + alts_segment[segment_name] = \ + reindex(dest_size_terms[segment_name], alts_segment[alt_dest_col_name]) choices = interaction_sample_simulate( choosers_segment, alts_segment, - spec=model_spec[[school_type]], + spec=model_spec[[segment_name]], choice_column=alt_dest_col_name, skims=skims, locals_d=locals_d, chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(model_name, school_type), - trace_choice_name=model_name) + trace_label=tracing.extend_trace_label(trace_label, segment_name), + trace_choice_name=trace_label) choices_list.append(choices) @@ -301,24 +315,24 @@ def run_school_location_simulate( return choices -@inject.step() -def school_location( - persons_merged, persons, +def run_school_location( + persons_merged_df, skim_dict, skim_stack, - land_use, size_terms, - chunk_size, - trace_hh_id): - - persons_merged_df = persons_merged.to_frame() + dest_size_terms, + model_settings, + chunk_size, trace_hh_id, trace_label + ): # - school_location_sample location_sample_df = \ run_school_location_sample( persons_merged_df, skim_dict, - land_use, size_terms, + dest_size_terms, + model_settings, chunk_size, - trace_hh_id) + trace_hh_id, + tracing.extend_trace_label(trace_label, 'sample')) # - school_location_logsums location_sample_df = \ @@ -326,8 +340,10 @@ def school_location( persons_merged_df, skim_dict, skim_stack, location_sample_df, + model_settings, chunk_size, - trace_hh_id) + trace_hh_id, + tracing.extend_trace_label(trace_label, 'logsums')) # - school_location_simulate choices = \ @@ -335,19 +351,83 @@ def school_location( persons_merged_df, location_sample_df, skim_dict, - land_use, size_terms, + dest_size_terms, + model_settings, chunk_size, - trace_hh_id) + trace_hh_id, + tracing.extend_trace_label(trace_label, 'simulate')) + + return choices + + +@inject.step() +def school_location( + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, + trace_hh_id): + + trace_label = 'school_location' + model_settings = config.read_model_settings('school_location.yaml') + + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] + + persons_merged_df = persons_merged.to_frame() + + spc = shadow_pricing.load_shadow_price_calculator(model_settings) + + # - max_iterations + if spc.saved_shadow_prices: + max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED', 1) + else: + max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) + logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) - tracing.print_summary('school_taz', choices, describe=True) + choices = None + for iteration in range(max_iterations): + + if iteration > 0: + spc.update_shadow_prices() + + # - shadow_price adjusted predicted_size + shadow_price_adjusted_predicted_size = spc.predicted_size * spc.shadow_prices + + choices = run_school_location( + persons_merged_df, + skim_dict, skim_stack, + shadow_price_adjusted_predicted_size, + model_settings, + chunk_size, trace_hh_id, + trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) + + choices_df = choices.to_frame('dest_choice') + choices_df['segment_id'] = \ + persons_merged_df[chooser_segment_column].reindex(choices_df.index) + + spc.set_choices(choices_df) + + number_of_failed_zones = spc.check_fit(iteration) + + logging.info("%s iteration: %s number_of_failed_zones: %s" % + (trace_label, iteration, number_of_failed_zones)) + + if number_of_failed_zones == 0: + break + + # - print convergence stats + print("\nshadow_pricing rms_error\n", spc.rms_error) + print("\nshadow_pricing num_fail\n", spc.num_fail) persons_df = persons.to_frame() # We only chose school locations for the subset of persons who go to school # so we backfill the empty choices with -1 to code as no school location - NO_SCHOOL_TAZ = -1 persons_df['school_taz'] = choices.reindex(persons_df.index).fillna(NO_SCHOOL_TAZ).astype(int) - tracing.print_summary('school_taz', choices, describe=True) + # tracing.print_summary('school_taz', choices, value_counts=True) + + # - shadow price table + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) # - annotate persons model_name = 'school_location' diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 2f81b220f..6fa947c47 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -26,7 +26,8 @@ from activitysim.core import assign -from .util.tour_destination import tour_destination_size_terms +from activitysim.abm.tables.size_terms import tour_destination_size_terms + from activitysim.core.skim import DataFrameMatrix from activitysim.core.interaction_sample_simulate import interaction_sample_simulate diff --git a/activitysim/abm/models/util/tour_destination.py b/activitysim/abm/models/util/tour_destination.py index 2c64ed62e..65be04d72 100644 --- a/activitysim/abm/models/util/tour_destination.py +++ b/activitysim/abm/models/util/tour_destination.py @@ -89,5 +89,6 @@ def tour_destination_size_terms(land_use, size_terms, selector): # - NARROW # float16 has 3.3 decimal digits of precision, float32 has 7.2 df = df.astype(np.float16, errors='raise') + assert np.isfinite(df.values).all() return df diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 4e6686c07..8d5b9b8a2 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -20,7 +20,7 @@ from .util import expressions from .util import logsums as logsum -from .util.tour_destination import tour_destination_size_terms +from activitysim.abm.tables.size_terms import tour_destination_size_terms """ @@ -39,7 +39,7 @@ def run_workplace_location_sample( persons_merged, skim_dict, - land_use, size_terms, + size_terms, chunk_size, trace_hh_id): """ build a table of workers * all zones in order to select a sample of alternative work locations. @@ -68,7 +68,7 @@ def run_workplace_location_sample( chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] choosers = choosers[chooser_columns] - alternatives = tour_destination_size_terms(land_use, size_terms, 'work') + alternatives = size_terms sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] diff --git a/activitysim/abm/tables/__init__.py b/activitysim/abm/tables/__init__.py index 7d95ba658..8dac5ca9e 100644 --- a/activitysim/abm/tables/__init__.py +++ b/activitysim/abm/tables/__init__.py @@ -13,6 +13,7 @@ from . import size_terms from . import trips from . import time_windows +from . import shadow_pricing from . import constants from . import table_dict diff --git a/activitysim/abm/tables/constants.py b/activitysim/abm/tables/constants.py index dd6d7694a..b6c446c1a 100644 --- a/activitysim/abm/tables/constants.py +++ b/activitysim/abm/tables/constants.py @@ -22,6 +22,11 @@ PSTUDENT_UNIVERSITY = 2 PSTUDENT_NOT = 3 +SCHOOL_SEGMENT_NONE = 0 +SCHOOL_SEGMENT_GRADE = 1 +SCHOOL_SEGMENT_HIGH = 2 +SCHOOL_SEGMENT_UNIV = 3 + PEMPLOY_FULL = 1 PEMPLOY_PART = 2 PEMPLOY_NOT = 3 diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py new file mode 100644 index 000000000..47c71b1b8 --- /dev/null +++ b/activitysim/abm/tables/shadow_pricing.py @@ -0,0 +1,321 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +from future.utils import iteritems + +import logging +import time +import multiprocessing +import ctypes +import os + +from collections import OrderedDict + +import numpy as np +import pandas as pd + +from activitysim.core import inject +from activitysim.core import util +from activitysim.core import config + + +logger = logging.getLogger(__name__) + +# - reverse semaphores to synchronize concurrent access to shared data buffer +# we use the first two rows of the final column in numpy-wrapped shared data as 'reverse semaphores' +# (synchronize concurrent access to shared data resource rather than throttling access) +TALLY_CHECKIN = (0, -1) +TALLY_CHECKOUT = (1, -1) + +class ShadowPriceCalculator(object): + + def __init__(self, model_settings, shared_data=None, shared_data_lock=None): + + self.segment_ids = model_settings['SEGMENT_IDS'] + + # - modeled_size (set by call to set_choices/synchronize_choices) + self.modeled_size = None + + # - convergence criteria for check_fit + # ignore criteria for zones smaller than size_threshold + self.size_threshold = model_settings['SIZE_THRESHOLD'] + # zone passes if modeled is within percent_tolerance of predicted_size + self.percent_tolerance = model_settings['PERCENT_TOLERANCE'] + # max number of zones allowed to fail + self.fail_threshold = model_settings['FAIL_THRESHOLD'] + + # - destination_size_table (predicted_size) + destination_size_table_name = model_settings['DESTINATION_SIZE_TABLE'] + self.predicted_size = inject.get_table(destination_size_table_name).to_frame() + assert set(self.predicted_size.columns) == set(self.segment_ids.keys()) + + # - shared_data + if shared_data is not None: + assert shared_data.shape[0] == self.predicted_size.shape[0] + assert shared_data.shape[1] == self.predicted_size.shape[1] + 1 # tally column + assert shared_data_lock is not None + self.shared_data = shared_data + self.shared_data_lock = shared_data_lock + + # - load saved shadow_prices + self.shadow_prices = self.load_saved_shadow_prices(model_settings) + self.saved_shadow_prices = (self.shadow_prices is not None) + + # - if we couldn't load saved shadow_prices, init to ones + if self.shadow_prices is None: + self.shadow_prices = \ + pd.DataFrame(data=1.0, + columns=self.predicted_size.columns, + index=self.predicted_size.index) + + self.rms_error = pd.DataFrame(index=self.predicted_size.columns) + self.num_fail = pd.DataFrame(index=self.predicted_size.columns) + + self.iter = 0 + + def load_saved_shadow_prices(self, model_settings): + + shadow_prices = None + + # - load saved shadow_prices + saved_shadow_price_file_name = model_settings.get('SAVED_SHADOW_PRICE_TABLE_NAME') + if saved_shadow_price_file_name: + file_path = config.config_file_path(saved_shadow_price_file_name) + if os.path.isfile(file_path): + shadow_prices = pd.read_csv(file_path, index_col=0) + logging.warning("loaded saved_shadow_prices from %s" % (file_path)) + + return shadow_prices + + def synchronize_choices(self, local_modeled_size): + + if self.shared_data is None: + return local_modeled_size + + num_processes = inject.get_injectable("num_processes") + + def get_tally(t): + with self.shared_data_lock: + return self.shared_data[t] + + def wait(tally, target, tally_name): + while get_tally(tally) != target: + logger.warning("waiting on %s target %s" % (tally_name, target)) + time.sleep(1) + + # - nobody checks in until checkout clears + wait(TALLY_CHECKOUT, 0, 'TALLY_CHECKOUT') + + # - add local_modeled_size data + with self.shared_data_lock: + first_in = self.shared_data[TALLY_CHECKIN] == 0 + # add local data from df to shared data buffer + # final column is used for tallys, hence the negative index + self.shared_data[..., 0:-1] += local_modeled_size.values + self.shared_data[TALLY_CHECKIN] += 1 + + # - wait until everybody else has checked in + wait(TALLY_CHECKIN, num_processes, 'TALLY_CHECKIN') + + # - copy shared data and check out + with self.shared_data_lock: + logger.warning("copy shared_data") + # numpy array with sum of local_modeled_size.values from all processes + global_modeled_size_array = self.shared_data[..., 0:-1].copy() + self.shared_data[TALLY_CHECKOUT] += 1 + + # - first in waits until all other processes have checked out, and cleans tub + if first_in: + wait(TALLY_CHECKOUT, num_processes, 'TALLY_CHECKOUT') + with self.shared_data_lock: + self.shared_data[:] = 0 + logger.warning("first_in clearing shared_data") + + # convert summed numpy array data to conform to original dataframe + return pd.DataFrame(data=global_modeled_size_array, + index=local_modeled_size.index, + columns=local_modeled_size.columns) + + + def set_choices(self, choices_df): + + assert 'dest_choice' in choices_df + + modeled_size = pd.DataFrame(index=self.predicted_size.index) + for c in self.predicted_size: + segment_choices = \ + choices_df[choices_df['segment_id'] == self.segment_ids[c]] + modeled_size[c] = segment_choices.groupby('dest_choice').size() + modeled_size = modeled_size.fillna(0).astype(int) + + self.modeled_size = self.synchronize_choices(modeled_size) + + + def check_fit(self, iter): + + assert self.modeled_size is not None + assert self.predicted_size is not None + + modeled_size = self.modeled_size + predicted_size = self.predicted_size + + abs_diff = (predicted_size - modeled_size).abs() + + rel_diff = abs_diff / modeled_size + + # ignore zones where predicted_size < threshold + rel_diff.where(self.predicted_size >= self.size_threshold, 0, inplace=True) + + # ignore zones where rel_diff < percent_tolerance + rel_diff.where(rel_diff > (self.percent_tolerance / 100.0), 0, inplace=True) + + num_fails = (rel_diff > 0).values.sum() + + self.rms_error['iter%s' % iter] = \ + (predicted_size - modeled_size).pow(2).sum() / len(modeled_size) + + self.num_fail['iter%s' % iter] = num_fails + + return num_fails + + def update_shadow_prices(self): + """ + CTRAMP: + if ( modeledDestinationLocationsByDestZone > 0 ) + shadowPrice *= ( scaledSize / modeledDestinationLocationsByDestZone ); + // else + // shadowPrice *= scaledSize; + """ + + assert self.modeled_size is not None + assert self.predicted_size is not None + assert self.shadow_prices is not None + + new_scale_factor = self.predicted_size / self.modeled_size + + # following CTRAMP (original version - later commented out) + # avoid zero-divide for 0 modeled_size, by setting scale_factor same as modeled_size of 1 + #new_scale_factor.where(self.modeled_size > 0, self.predicted_size) + + new_shadow_prices = self.shadow_prices * new_scale_factor + + # following CTRAMP (revised version - with 0 dest zone case lines commented out) + # avoid zero-divide for 0 modeled_size, by leaving shadow_prices unchanged + new_shadow_prices.where(self.modeled_size > 0, self.shadow_prices, inplace=True) + + self.shadow_prices = new_shadow_prices + + +def block_name(selector): + return selector + + +def get_shadow_pricing_info(): + + land_use = inject.get_table('land_use') + size_terms = inject.get_table('size_terms').to_frame() + + blocks = OrderedDict() + + for selector in ['school']: + + sp_rows = len(land_use) + sp_cols = len(size_terms[size_terms.selector == selector]) + + # extra tally column + blocks[block_name(selector)] = (sp_rows, sp_cols + 1) + + sp_dtype = np.int64 + + shadow_pricing_info = { + 'dtype': sp_dtype, + 'blocks': blocks, + } + + + return shadow_pricing_info + + +def buffers_for_shadow_pricing(shadow_pricing_info, shared=False): + + assert shared + + dtype = shadow_pricing_info['dtype'] + blocks = shadow_pricing_info['blocks'] + + data_buffers = {} + for block_key, block_shape in iteritems(blocks): + + # buffer_size must be int (or p2.7 long), not np.int64 + buffer_size = int(np.prod(block_shape)) + + csz = buffer_size * np.dtype(dtype).itemsize + logger.info("allocating shared buffer %s %s buffer_size %s (%s)" % + (block_key, buffer_size, block_shape, util.GB(csz))) + + if np.issubdtype(dtype, np.int64): + typecode = ctypes.c_long + else: + raise RuntimeError("buffer_for_shadow_pricing unrecognized dtype %s" % dtype) + + buffer = multiprocessing.Array(typecode, buffer_size) + + logger.info("buffer_for_shadow_pricing added block %s" % (block_key)) + + data_buffers[block_key] = buffer + + return data_buffers + + +def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): + + assert type(data_buffers) == dict + + dtype = shadow_pricing_info['dtype'] + blocks = shadow_pricing_info['blocks'] + + if selector not in blocks: + raise RuntimeError("Selector %s not in shadow_pricing_info" % selector) + + if block_name(selector) not in data_buffers: + raise RuntimeError("Block %s not in data_buffers" % block_name(selector)) + + shape = blocks[selector] + data = data_buffers[block_name(selector)] + + return np.frombuffer(data.get_obj(), dtype=dtype).reshape(shape), data.get_lock() + + +def load_shadow_price_calculator(model_settings): + + selector = model_settings['SELECTOR'] + segment_ids = model_settings['SEGMENT_IDS'] + destination_size_table = model_settings['DESTINATION_SIZE_TABLE'] + + # - data_buffers (if shared data) + data_buffers = inject.get_injectable('data_buffers', None) + if data_buffers is not None: + logger.info('Using existing data_buffers for shadow_price') + + # - shadow_pricing_info + shadow_pricing_info = inject.get_injectable('shadow_pricing_info', None) + if shadow_pricing_info is None: + shadow_pricing_info = get_shadow_pricing_info() + inject.add_injectable('shadow_pricing_info', shadow_pricing_info) + + # - extract data buffer and reshape as numpy array + data, lock = shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector) + else: + data = None # ShadowPriceCalculator will allocate its own data + lock = None + + # - ShadowPriceCalculator + spc = ShadowPriceCalculator( + model_settings, + data, lock) + + return spc diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index c2ff3b57e..0ddb17d86 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -5,7 +5,10 @@ from future.standard_library import install_aliases install_aliases() # noqa: E402 +from future.utils import iteritems + import logging +import numpy as np import pandas as pd from activitysim.core import inject @@ -19,3 +22,122 @@ def size_terms(): f = config.config_file_path('destination_choice_size_terms.csv') return pd.read_csv(f, comment='#', index_col='segment') + + +def size_term(land_use, destination_choice_coeffs): + """ + This method takes the land use data and multiplies various columns of the + land use data by coefficients from the spec table in order + to yield a size term (a linear combination of land use variables). + + Parameters + ---------- + land_use : DataFrame + A dataframe of land use attributes - the column names should match + the index of destination_choice_coeffs + destination_choice_coeffs : Series + A series of coefficients for the land use attributes - the index + describes the link to the land use table, and the values are floating + points numbers used to do the linear combination + + Returns + ------- + values : Series + The index will be the same as land use, and the values will the + linear combination of the land use table columns specified by the + coefficients series. + """ + coeffs = destination_choice_coeffs + + # first check for missing column in the land_use table + missing = coeffs[~coeffs.index.isin(land_use.columns)] + + if len(missing) > 0: + logger.warning("%s missing columns in land use" % len(missing.index)) + for v in missing.index.values: + logger.warning("missing: %s" % v) + + return land_use[coeffs.index].dot(coeffs) + + +def tour_destination_size_terms(land_use, size_terms, selector): + """ + + Parameters + ---------- + land_use - pipeline table + size_terms - pipeline table + selector - str + + Returns + ------- + + :: + + pandas.dataframe + one column per selector segment with index of land_use + e.g. for selector 'work', columns will be work_low, work_med, work_high, work_veryhigh + and for selector 'trip', columns will be eatout, escort, othdiscr, othmaint, ... + + work_low work_med work_high work_veryhigh + TAZ ... + 1 1267.00000 522.000 1108.000 1540.0000 ... + 2 1991.00000 824.500 1759.000 2420.0000 ... + ... + """ + + land_use = land_use.to_frame() + size_terms = size_terms.to_frame() + + size_terms = size_terms[size_terms.selector == selector].copy() + del size_terms['selector'] + + df = pd.DataFrame({key: size_term(land_use, row) for key, row in size_terms.iterrows()}, + index=land_use.index) + df.index.name = 'TAZ' + + if not (df.dtypes == 'float64').all(): + logger.warning('Surprised to find that not all size_terms were float64!') + + # - NARROW + # float16 has 3.3 decimal digits of precision, float32 has 7.2 + df = df.astype(np.float16, errors='raise') + assert np.isfinite(df.values).all() + + return df + + +def destination_predicted_size(choosers_table, selector, chooser_segment_column, segment_ids): + + land_use = inject.get_table('land_use') + size_terms = inject.get_table('size_terms') + choosers_df = inject.get_table(choosers_table).to_frame() + + # - raw_predicted_size + raw_size = tour_destination_size_terms(land_use, size_terms, selector) + assert set(raw_size.columns) == set(segment_ids.keys()) + + segment_chooser_counts = \ + {segment_name: (choosers_df[chooser_segment_column] == segment_id).sum() + for segment_name, segment_id in iteritems(segment_ids)} + + # - segment scale factor (modeled / predicted) keyed by segment_name + # scaling reconciles differences between synthetic population and zone demographics + # in a partial sample, it also scales predicted_size targets to sample population + segment_scale_factors = {} + for c in raw_size: + segment_predicted_size = raw_size[c].astype(np.float64).sum() + segment_scale_factors[c] = \ + segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) + + # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + predicted_size = raw_size.astype(np.float64) + for c in predicted_size: + predicted_size[c] *= segment_scale_factors[c] + + # trace_label = "destination_predicted_size %s" % (selector) + # print("%s raw_predicted_size\n" % (trace_label,), raw_size.head(20)) + # print("%s segment_scale_factors" % (trace_label,), segment_scale_factors) + # print("%s predicted_size\n" % (trace_label,), predicted_size) + + return predicted_size diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 0a1119fe2..331fd14cc 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -94,7 +94,7 @@ def get_skim_info(omx_file_path, tags_to_load=None): max_skims_per_block = num_skims def block_name(block): - return "%s_%s" % (omx_name, block) + return "skim_%s_%s" % (omx_name, block) key1_block_offsets = OrderedDict() blocks = OrderedDict() @@ -145,13 +145,13 @@ def block_name(block): return skim_info -def buffer_for_skims(skim_info, shared=False): +def buffers_for_skims(skim_info, shared=False): skim_dtype = skim_info['dtype'] omx_shape = skim_info['omx_shape'] blocks = skim_info['blocks'] - skim_buffer = {} + skim_buffers = {} for block_name, block_size in iteritems(blocks): # buffer_size must be int (or p2.7 long), not np.int64 @@ -167,20 +167,20 @@ def buffer_for_skims(skim_info, shared=False): elif np.issubdtype(skim_dtype, np.float32): typecode = 'f' else: - raise RuntimeError("buffer_for_skims unrecognized dtype %s" % skim_dtype) + raise RuntimeError("buffers_for_skims unrecognized dtype %s" % skim_dtype) buffer = multiprocessing.RawArray(typecode, buffer_size) else: buffer = np.zeros(buffer_size, dtype=skim_dtype) - skim_buffer[block_name] = buffer + skim_buffers[block_name] = buffer - return skim_buffer + return skim_buffers -def skim_data_from_buffer(skim_buffer, skim_info): +def skim_data_from_buffers(skim_buffers, skim_info): - assert type(skim_buffer) == dict + assert type(skim_buffers) == dict omx_shape = skim_info['omx_shape'] skim_dtype = skim_info['dtype'] @@ -189,7 +189,7 @@ def skim_data_from_buffer(skim_buffer, skim_info): skim_data = [] for block_name, block_size in iteritems(blocks): skims_shape = omx_shape + (block_size,) - block_buffer = skim_buffer[block_name] + block_buffer = skim_buffers[block_name] assert len(block_buffer) == int(np.prod(skims_shape)) block_data = np.frombuffer(block_buffer, dtype=skim_dtype).reshape(skims_shape) skim_data.append(block_data) @@ -197,9 +197,9 @@ def skim_data_from_buffer(skim_buffer, skim_info): return skim_data -def load_skims(omx_file_path, skim_info, skim_buffer): +def load_skims(omx_file_path, skim_info, skim_buffers): - skim_data = skim_data_from_buffer(skim_buffer, skim_info) + skim_data = skim_data_from_buffers(skim_buffers, skim_info) block_offsets = skim_info['block_offsets'] omx_keys = skim_info['omx_keys'] @@ -237,14 +237,14 @@ def skim_dict(data_dir, settings): logger.debug("omx_shape %s skim_dtype %s" % (skim_info['omx_shape'], skim_info['dtype'])) - skim_buffer = inject.get_injectable('skim_buffer', None) - if skim_buffer: - logger.info('Using existing skim_buffer for skims') + skim_buffers = inject.get_injectable('data_buffers', None) + if skim_buffers: + logger.info('Using existing skim_buffers for skims') else: - skim_buffer = buffer_for_skims(skim_info, shared=False) - load_skims(omx_file_path, skim_info, skim_buffer) + skim_buffers = buffers_for_skims(skim_info, shared=False) + load_skims(omx_file_path, skim_info, skim_buffers) - skim_data = skim_data_from_buffer(skim_buffer, skim_info) + skim_data = skim_data_from_buffers(skim_buffers, skim_info) block_names = list(skim_info['blocks'].keys()) for i in range(len(skim_data)): diff --git a/activitysim/abm/test/configs/annotate_persons.csv b/activitysim/abm/test/configs/annotate_persons.csv index 22677ece1..2414cde33 100644 --- a/activitysim/abm/test/configs/annotate_persons.csv +++ b/activitysim/abm/test/configs/annotate_persons.csv @@ -16,8 +16,13 @@ presence of university student other than self in household,has_university,"othe student_is_employed,student_is_employed,"(persons.ptype.isin([constants.PTYPE_UNIVERSITY, constants.PTYPE_DRIVING]) & persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART]))" nonstudent_to_school,nonstudent_to_school,"(persons.ptype.isin([constants.PTYPE_FULL, constants.PTYPE_PART, constants.PTYPE_NONWORK, constants.PTYPE_RETIRED]) & persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY]))" is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" -is_student,is_student,"persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY])" -is_gradeschool,is_gradeschool,"(persons.pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age <= setting('grade_school_max_age'))" -is_highschool,is_highschool,"(persons.pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" -is_university,is_university,"persons.pstudent == constants.PSTUDENT_UNIVERSITY" +is_student,is_student,"pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY])" +is_gradeschool,is_gradeschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age <= setting('grade_school_max_age'))" +is_highschool,is_highschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" +is_university,is_university,"pstudent == constants.PSTUDENT_UNIVERSITY" +#,, +school_segment gradeschool,school_segment,"np.where(is_gradeschool, constants.SCHOOL_SEGMENT_GRADE, constants.SCHOOL_SEGMENT_NONE)" +school_segment highschool,school_segment,"np.where(is_highschool, constants.SCHOOL_SEGMENT_HIGH, school_segment)" +school_segment university,school_segment,"np.where(is_university, constants.SCHOOL_SEGMENT_UNIV, school_segment).astype(np.int8)" +#,, home_taz,home_taz,"reindex(households.TAZ, persons.household_id)" diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 02395324c..23c7e8fe0 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -34,9 +34,13 @@ from activitysim import abm from activitysim.abm.tables.skims import get_skim_info -from activitysim.abm.tables.skims import buffer_for_skims +from activitysim.abm.tables.skims import buffers_for_skims from activitysim.abm.tables.skims import load_skims +from activitysim.abm.tables.shadow_pricing import buffers_for_shadow_pricing +from activitysim.abm.tables.shadow_pricing import get_shadow_pricing_info + + logger = logging.getLogger(__name__) @@ -315,7 +319,7 @@ def load_skim_data(skim_buffer): load_skims(omx_file_path, skim_info, skim_buffer) -def allocate_shared_skim_buffer(): +def allocate_shared_skim_buffers(): """ This is called by the main process and allocate memory buffer to share with subprocs @@ -331,9 +335,26 @@ def allocate_shared_skim_buffer(): # select the skims to load skim_info = get_skim_info(omx_file_path, tags_to_load) - skim_buffer = buffer_for_skims(skim_info, shared=True) + skim_buffers = buffers_for_skims(skim_info, shared=True) + + return skim_buffers - return skim_buffer + +def allocate_shared_shadow_pricing_buffers(): + """ + This is called by the main process and allocate memory buffer to share with subprocs + + Returns + ------- + multiprocessing.RawArray + """ + + logger.info("allocate_shared_shadow_pricing_buffers") + + shadow_pricing_info = get_shadow_pricing_info() + shadow_pricing_buffers = buffers_for_shadow_pricing(shadow_pricing_info, shared=True) + + return shadow_pricing_buffers def setup_injectables_and_logging(injectables): @@ -350,14 +371,16 @@ def setup_injectables_and_logging(injectables): tracing.config_logger() -def run_simulation(queue, step_info, resume_after, skim_buffer): +def run_simulation(queue, step_info, resume_after, shared_data_buffer): models = step_info['models'] chunk_size = step_info['chunk_size'] step_label = step_info['name'] + num_processes = step_info['num_processes'] - inject.add_injectable('skim_buffer', skim_buffer) + inject.add_injectable('data_buffers', shared_data_buffer) inject.add_injectable("chunk_size", chunk_size) + inject.add_injectable("num_processes", num_processes) if resume_after: logger.info('resume_after %s', resume_after) @@ -404,7 +427,7 @@ def profile_path(): def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): - skim_buffer = kwargs + shared_data_buffer = kwargs # handle_standard_args() setup_injectables_and_logging(injectables) @@ -420,7 +443,7 @@ def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): cProfile.runctx('run_simulation(queue, step_info, resume_after, skim_buffer)', globals(), locals(), filename=profile_path()) else: - run_simulation(queue, step_info, resume_after, skim_buffer) + run_simulation(queue, step_info, resume_after, shared_data_buffer) chunk.log_write_hwm() mem.log_hwm() @@ -437,14 +460,14 @@ def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): def mp_setup_skims(injectables, **kwargs): - skim_buffer = kwargs + shared_data_buffer = kwargs setup_injectables_and_logging(injectables) if setting('profile', False): cProfile.runctx('load_skim_data(skim_buffer)', globals(), locals(), filename=profile_path()) else: - load_skim_data(skim_buffer) + load_skim_data(shared_data_buffer) def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): @@ -462,8 +485,11 @@ def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): """ -def run_sub_simulations(injectables, shared_skim_buffer, step_info, process_names, - resume_after, previously_completed): +def run_sub_simulations( + injectables, + shared_data_buffers, + step_info, process_names, + resume_after, previously_completed): def log_queued_messages(): for i, process, queue in zip(list(range(num_simulations)), procs, queues): @@ -530,8 +556,8 @@ def idle(seconds): for process_name in process_names: q = multiprocessing.Queue() p = multiprocessing.Process(target=mp_run_simulation, name=process_name, - args=(q, injectables, step_info, resume_after), - kwargs=shared_skim_buffer) + args=(q, injectables, step_info, resume_after,), + kwargs=shared_data_buffers) procs.append(p) queues.append(q) @@ -619,16 +645,24 @@ def skip_phase(phase): def find_breadcrumb(crumb, default=None): return old_breadcrumbs.get(step_name, {}).get(crumb, default) + shared_data_buffers = {} + t0 = tracing.print_elapsed_time() - shared_skim_buffer = allocate_shared_skim_buffer() + shared_data_buffers.update(allocate_shared_skim_buffers()) t0 = tracing.print_elapsed_time('allocate shared skim buffer', t0) mem.trace_memory_info("allocate_shared_skim_buffer.completed") + # FIXME combine shared_skim_buffer and shared_shadow_pricing_buffer in shared_data_buffer + t0 = tracing.print_elapsed_time() + shared_data_buffers.update(allocate_shared_shadow_pricing_buffers()) + t0 = tracing.print_elapsed_time('allocate shared shadow_pricing buffer', t0) + mem.trace_memory_info("allocate_shared_shadow_pricing_buffers.completed") + # - mp_setup_skims run_sub_task( multiprocessing.Process( target=mp_setup_skims, name='mp_setup_skims', args=(injectables,), - kwargs=shared_skim_buffer) + kwargs=shared_data_buffers) ) t0 = tracing.print_elapsed_time('setup skims', t0) @@ -659,7 +693,9 @@ def find_breadcrumb(crumb, default=None): completed = find_breadcrumb('completed', default=[]) if resume_after == '_' else [] - completed = run_sub_simulations(injectables, shared_skim_buffer, step_info, + completed = run_sub_simulations(injectables, + shared_data_buffers, + step_info, sub_proc_names, resume_after, completed) if len(completed) != num_processes: diff --git a/example/configs/annotate_persons.csv b/example/configs/annotate_persons.csv index 4e5fed9e3..580ba5dfb 100644 --- a/example/configs/annotate_persons.csv +++ b/example/configs/annotate_persons.csv @@ -28,4 +28,8 @@ is_gradeschool,is_gradeschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & is_highschool,is_highschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" is_university,is_university,"pstudent == constants.PSTUDENT_UNIVERSITY" #,, +school_segment gradeschool,school_segment,"np.where(is_gradeschool, constants.SCHOOL_SEGMENT_GRADE, constants.SCHOOL_SEGMENT_NONE)" +school_segment highschool,school_segment,"np.where(is_highschool, constants.SCHOOL_SEGMENT_HIGH, school_segment)" +school_segment university,school_segment,"np.where(is_university, constants.SCHOOL_SEGMENT_UNIV, school_segment).astype(np.int8)" +#,, home_taz,home_taz,"reindex(households.TAZ, persons.household_id)" diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 80a7bc66b..97bd3955f 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -2,9 +2,7 @@ SAMPLE_SIZE: 30 SIMULATE_CHOOSER_COLUMNS: - TAZ - - is_university - - is_highschool - - is_gradeschool + - school_segment # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ @@ -21,3 +19,39 @@ LOGSUM_PREPROCESSOR: nontour_preprocessor annotate_persons: SPEC: annotate_persons_school DF: persons + +MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 + +# - shadow pricing + +CHOOSER_TABLE_NAME: persons + +DESTINATION_SIZE_TABLE: school_destination_size + +# size_terms selector +SELECTOR: school + +# chooser column with segment_id for this segment type +CHOOSER_SEGMENT_COLUMN: school_segment + +# FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this +SEGMENT_IDS: + university: 3 + highschool: 2 + gradeschool: 1 + +SHADOW_PRICE_TABLE: school_shadow_prices +MODELED_SIZE_TABLE: school_modeled_size + +SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv + + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max number of zones allowed to fail +FAIL_THRESHOLD: 10 From 7d1076483f1cde059a7c89e89b6e369eb50fa9c5 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 7 Dec 2018 08:54:31 -0500 Subject: [PATCH 053/122] shadow pricing in workplace_location --- activitysim/abm/misc.py | 2 +- activitysim/abm/models/initialize.py | 3 +- activitysim/abm/models/school_location.py | 8 +- activitysim/abm/models/workplace_location.py | 90 +++++++++++++++---- activitysim/abm/tables/constants.py | 5 ++ activitysim/abm/tables/input_store.py | 17 ++-- activitysim/abm/tables/shadow_pricing.py | 18 ++-- activitysim/abm/tables/skims.py | 3 +- .../abm/test/configs/annotate_persons.csv | 8 ++ .../abm/test/configs/school_location.yaml | 41 ++++++++- .../abm/test/configs/workplace_location.yaml | 37 ++++++++ .../{configs => data}/override_hh_ids.csv | 0 activitysim/abm/test/test_pipeline.py | 2 +- activitysim/core/config.py | 66 +++++++------- activitysim/core/mp_tasks.py | 3 +- example/configs/annotate_persons.csv | 1 + example/configs/school_location.yaml | 7 +- example/configs/workplace_location.yaml | 37 ++++++++ example_multiple_zone/extensions/skims.py | 6 +- 19 files changed, 269 insertions(+), 85 deletions(-) rename activitysim/abm/test/{configs => data}/override_hh_ids.csv (100%) diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index ff321f82b..3213a0d6a 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -50,7 +50,7 @@ def override_hh_ids(settings): if hh_ids_filename is None: return None - file_path = config.config_file_path(hh_ids_filename, mandatory=False) + file_path = config.data_file_path(hh_ids_filename, mandatory=False) if not file_path: logger.error("hh_ids file name '%s' specified in settings not found: %s" % hh_ids_filename) return None diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index c64788940..5f18ea1a5 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -89,7 +89,7 @@ def initialize_households(): annotate_tables(model_settings, trace_label) # - ShadowPriceCalculator predicted_size tables - for dest_model_settings_file in ['school_location.yaml']: + for dest_model_settings_file in ['school_location.yaml', 'workplace_location.yaml']: dest_choice_model_settings = config.read_model_settings(dest_model_settings_file) selector = dest_choice_model_settings['SELECTOR'] @@ -108,7 +108,6 @@ def initialize_households(): segment_ids) inject.add_table(destination_size_table_name, size_df) - t0 = tracing.print_elapsed_time() inject.get_table('person_windows').to_frame() t0 = tracing.print_elapsed_time("preload person_windows", t0, debug=True) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 07b2753d9..c560595ff 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -415,7 +415,7 @@ def school_location( break # - print convergence stats - print("\nshadow_pricing rms_error\n", spc.rms_error) + # print("\nshadow_pricing rms_error\n", spc.rms_error) print("\nshadow_pricing num_fail\n", spc.num_fail) persons_df = persons.to_frame() @@ -426,8 +426,10 @@ def school_location( # tracing.print_summary('school_taz', choices, value_counts=True) # - shadow price table - inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) - inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) + if 'SHADOW_PRICE_TABLE' in model_settings: + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + if 'MODELED_SIZE_TABLE' in model_settings: + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) # - annotate persons model_name = 'school_location' diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 8d5b9b8a2..ac3af9254 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -20,8 +20,8 @@ from .util import expressions from .util import logsums as logsum -from activitysim.abm.tables.size_terms import tour_destination_size_terms +from activitysim.abm.tables import shadow_pricing """ The workplace location model predicts the zones in which various people will @@ -39,7 +39,7 @@ def run_workplace_location_sample( persons_merged, skim_dict, - size_terms, + dest_size_terms, chunk_size, trace_hh_id): """ build a table of workers * all zones in order to select a sample of alternative work locations. @@ -68,7 +68,7 @@ def run_workplace_location_sample( chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] choosers = choosers[chooser_columns] - alternatives = size_terms + alternatives = dest_size_terms sample_size = model_settings["SAMPLE_SIZE"] alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] @@ -170,7 +170,7 @@ def run_workplace_location_simulate( persons_merged, location_sample_df, skim_dict, - land_use, size_terms, + dest_size_terms, chunk_size, trace_hh_id): """ Workplace location model on workplace_location_sample annotated with mode_choice logsum @@ -196,10 +196,9 @@ def run_workplace_location_simulate( # alternatives are pre-sampled and annotated with logsums and pick_count # but we have to merge additional alt columns into alt sample list - destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'work') alternatives = \ - pd.merge(location_sample_df, destination_size_terms, + pd.merge(location_sample_df, dest_size_terms, left_on=alt_dest_col_name, right_index=True, how="left") logger.info("Running workplace_location_simulate with %d persons" % len(choosers)) @@ -230,21 +229,20 @@ def run_workplace_location_simulate( return choices -@inject.step() -def workplace_location( - persons_merged, persons, +def run_workplace_location( + persons_merged_df, skim_dict, skim_stack, - land_use, size_terms, - chunk_size, trace_hh_id): - - persons_merged_df = persons_merged.to_frame() + dest_size_terms, + model_settings, + chunk_size, trace_hh_id, trace_label + ): # - workplace_location_sample location_sample_df = \ run_workplace_location_sample( persons_merged_df, skim_dict, - land_use, size_terms, + dest_size_terms, chunk_size, trace_hh_id) @@ -263,10 +261,66 @@ def workplace_location( persons_merged_df, location_sample_df, skim_dict, - land_use, size_terms, + dest_size_terms, chunk_size, trace_hh_id) + return choices + + +@inject.step() +def workplace_location( + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id): + + trace_label = 'workplace_location' + model_settings = config.read_model_settings('workplace_location.yaml') + + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] + + persons_merged_df = persons_merged.to_frame() + + spc = shadow_pricing.load_shadow_price_calculator(model_settings) + + # - max_iterations + if spc.saved_shadow_prices: + max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED', 1) + else: + max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) + logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) + + choices = None + for iteration in range(max_iterations): + + if iteration > 0: + spc.update_shadow_prices() + + # - shadow_price adjusted predicted_size + shadow_price_adjusted_predicted_size = spc.predicted_size * spc.shadow_prices + + choices = run_workplace_location( + persons_merged_df, + skim_dict, skim_stack, + shadow_price_adjusted_predicted_size, + model_settings, + chunk_size, trace_hh_id, + trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) + + choices_df = choices.to_frame('dest_choice') + choices_df['segment_id'] = \ + persons_merged_df[chooser_segment_column].reindex(choices_df.index) + + spc.set_choices(choices_df) + + number_of_failed_zones = spc.check_fit(iteration) + + logging.info("%s iteration: %s number_of_failed_zones: %s" % + (trace_label, iteration, number_of_failed_zones)) + + if number_of_failed_zones == 0: + break + tracing.print_summary('workplace_taz', choices, describe=True) persons_df = persons.to_frame() @@ -277,6 +331,12 @@ def workplace_location( persons_df['workplace_taz'] = \ choices.reindex(persons_df.index).fillna(NO_WORKPLACE_TAZ).astype(int) + # - shadow price table + if 'SHADOW_PRICE_TABLE' in model_settings: + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + if 'MODELED_SIZE_TABLE' in model_settings: + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) + # - annotate persons model_name = 'workplace_location' model_settings = config.read_model_settings('workplace_location.yaml') diff --git a/activitysim/abm/tables/constants.py b/activitysim/abm/tables/constants.py index b6c446c1a..385aec6c6 100644 --- a/activitysim/abm/tables/constants.py +++ b/activitysim/abm/tables/constants.py @@ -27,6 +27,11 @@ SCHOOL_SEGMENT_HIGH = 2 SCHOOL_SEGMENT_UNIV = 3 +INCOME_SEGMENT_LOW = 1 +INCOME_SEGMENT_MED = 2 +INCOME_SEGMENT_HIGH = 3 +INCOME_SEGMENT_VERYHIGH = 4 + PEMPLOY_FULL = 1 PEMPLOY_PART = 2 PEMPLOY_NOT = 3 diff --git a/activitysim/abm/tables/input_store.py b/activitysim/abm/tables/input_store.py index da2b6b46a..b72c5c60e 100644 --- a/activitysim/abm/tables/input_store.py +++ b/activitysim/abm/tables/input_store.py @@ -10,7 +10,7 @@ import pandas as pd -from activitysim.core import inject +from activitysim.core import config from activitysim.core.config import setting # FIXME @@ -22,18 +22,13 @@ def read_input_table(table_name): - input_store_path = inject.get_injectable("input_store_path", None) + filename = setting('input_store', None) - if not input_store_path: + if not filename: + logger.error("input store file name not specified in settings") + raise RuntimeError("store file name not specified in settings") - filename = setting('input_store', None) - - if not filename: - logger.error("input store file name not specified in settings") - raise RuntimeError("store file name not specified in settings") - - data_dir = inject.get_injectable("data_dir") - input_store_path = os.path.join(data_dir, filename) + input_store_path = config.data_file_path(filename) if not os.path.exists(input_store_path): logger.error("store file not found: %s" % input_store_path) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 47c71b1b8..76a151c51 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -31,6 +31,7 @@ TALLY_CHECKIN = (0, -1) TALLY_CHECKOUT = (1, -1) + class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): @@ -84,10 +85,13 @@ def load_saved_shadow_prices(self, model_settings): # - load saved shadow_prices saved_shadow_price_file_name = model_settings.get('SAVED_SHADOW_PRICE_TABLE_NAME') if saved_shadow_price_file_name: - file_path = config.config_file_path(saved_shadow_price_file_name) - if os.path.isfile(file_path): + # FIXME - where should we look for this file? + file_path = config.data_file_path(saved_shadow_price_file_name, mandatory=False) + if file_path: shadow_prices = pd.read_csv(file_path, index_col=0) - logging.warning("loaded saved_shadow_prices from %s" % (file_path)) + logging.warning("loading saved_shadow_prices from %s" % (file_path)) + else: + logging.warning("Could not find saved_shadow_prices file %s" % (file_path)) return shadow_prices @@ -140,7 +144,6 @@ def wait(tally, target, tally_name): index=local_modeled_size.index, columns=local_modeled_size.columns) - def set_choices(self, choices_df): assert 'dest_choice' in choices_df @@ -154,7 +157,6 @@ def set_choices(self, choices_df): self.modeled_size = self.synchronize_choices(modeled_size) - def check_fit(self, iter): assert self.modeled_size is not None @@ -197,9 +199,10 @@ def update_shadow_prices(self): new_scale_factor = self.predicted_size / self.modeled_size + # FIXME - need to decide if following CTRAMP code quoted above, and if so, which version # following CTRAMP (original version - later commented out) # avoid zero-divide for 0 modeled_size, by setting scale_factor same as modeled_size of 1 - #new_scale_factor.where(self.modeled_size > 0, self.predicted_size) + # new_scale_factor.where(self.modeled_size > 0, self.predicted_size) new_shadow_prices = self.shadow_prices * new_scale_factor @@ -236,7 +239,6 @@ def get_shadow_pricing_info(): 'blocks': blocks, } - return shadow_pricing_info @@ -293,8 +295,6 @@ def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): def load_shadow_price_calculator(model_settings): selector = model_settings['SELECTOR'] - segment_ids = model_settings['SEGMENT_IDS'] - destination_size_table = model_settings['DESTINATION_SIZE_TABLE'] # - data_buffers (if shared data) data_buffers = inject.get_injectable('data_buffers', None) diff --git a/activitysim/abm/tables/skims.py b/activitysim/abm/tables/skims.py index 331fd14cc..6cb20edc6 100644 --- a/activitysim/abm/tables/skims.py +++ b/activitysim/abm/tables/skims.py @@ -22,6 +22,7 @@ from activitysim.core import skim from activitysim.core import inject from activitysim.core import util +from activitysim.core import config logger = logging.getLogger(__name__) @@ -227,7 +228,7 @@ def load_skims(omx_file_path, skim_info, skim_buffers): @inject.injectable(cache=True) def skim_dict(data_dir, settings): - omx_file_path = os.path.join(data_dir, settings["skims_file"]) + omx_file_path = config.data_file_path(settings["skims_file"]) tags_to_load = settings['skim_time_periods']['labels'] logger.info("loading skim_dict from %s" % (omx_file_path, )) diff --git a/activitysim/abm/test/configs/annotate_persons.csv b/activitysim/abm/test/configs/annotate_persons.csv index 2414cde33..da919ca12 100644 --- a/activitysim/abm/test/configs/annotate_persons.csv +++ b/activitysim/abm/test/configs/annotate_persons.csv @@ -16,6 +16,14 @@ presence of university student other than self in household,has_university,"othe student_is_employed,student_is_employed,"(persons.ptype.isin([constants.PTYPE_UNIVERSITY, constants.PTYPE_DRIVING]) & persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART]))" nonstudent_to_school,nonstudent_to_school,"(persons.ptype.isin([constants.PTYPE_FULL, constants.PTYPE_PART, constants.PTYPE_NONWORK, constants.PTYPE_RETIRED]) & persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY]))" is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" +#,, +#,, FIXME - if person is a university student but has school age student category value then reset student category value +,pstudent,"persons.pstudent.where(persons.ptype!=constants.PTYPE_UNIVERSITY, constants.PSTUDENT_UNIVERSITY)" +#,, FIXME if person is a student of any kind but has full-time employment status then reset student category value to non-student +,pstudent,"pstudent.where(persons.ptype!=constants.PTYPE_FULL, constants.PSTUDENT_NOT)" +#,, FIXME if student category is non-student and employment is student then reset student category value to student +,pstudent,"pstudent.where((persons.ptype!=constants.PTYPE_DRIVING) & (persons.ptype!=constants.PTYPE_SCHOOL), constants.PSTUDENT_GRADE_OR_HIGH)" +#,, is_student,is_student,"pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY])" is_gradeschool,is_gradeschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age <= setting('grade_school_max_age'))" is_highschool,is_highschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index 80a7bc66b..fde88b510 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -2,9 +2,7 @@ SAMPLE_SIZE: 30 SIMULATE_CHOOSER_COLUMNS: - TAZ - - is_university - - is_highschool - - is_gradeschool + - school_segment # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ @@ -21,3 +19,40 @@ LOGSUM_PREPROCESSOR: nontour_preprocessor annotate_persons: SPEC: annotate_persons_school DF: persons + + +# - shadow pricing + + +MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 + +CHOOSER_TABLE_NAME: persons + +DESTINATION_SIZE_TABLE: school_destination_size + +# size_terms selector +SELECTOR: school + +# chooser column with segment_id for this segment type +CHOOSER_SEGMENT_COLUMN: school_segment + +# FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this +SEGMENT_IDS: + university: 3 + highschool: 2 + gradeschool: 1 + +SHADOW_PRICE_TABLE: school_shadow_prices +MODELED_SIZE_TABLE: school_modeled_size + +#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max number of zones allowed to fail +FAIL_THRESHOLD: 10 diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index 2a61a3b0d..6c4e71327 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -19,3 +19,40 @@ annotate_persons: DF: persons TABLES: - land_use + +# - shadow pricing + +MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 + +CHOOSER_TABLE_NAME: households + +DESTINATION_SIZE_TABLE: workplace_destination_size + +# size_terms selector +SELECTOR: work + +# chooser column with segment_id for this segment type +CHOOSER_SEGMENT_COLUMN: income_segment + +# FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this +SEGMENT_IDS: + work_low: 1 + work_med: 2 + work_high: 3 + work_veryhigh: 4 + + +SHADOW_PRICE_TABLE: workplace_shadow_prices +MODELED_SIZE_TABLE: workplace_modeled_size + +#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max number of zones allowed to fail +FAIL_THRESHOLD: 10 diff --git a/activitysim/abm/test/configs/override_hh_ids.csv b/activitysim/abm/test/data/override_hh_ids.csv similarity index 100% rename from activitysim/abm/test/configs/override_hh_ids.csv rename to activitysim/abm/test/data/override_hh_ids.csv diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 618fb5a34..7cb9ed8c3 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -238,7 +238,7 @@ def test_mini_pipeline_run3(): households = inject.get_table('households').to_frame() - override_hh_ids = pd.read_csv(config.config_file_path('override_hh_ids.csv')) + override_hh_ids = pd.read_csv(config.data_file_path('override_hh_ids.csv')) print("\noverride_hh_ids\n", override_hh_ids) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 728061d59..2b90d9e5d 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -115,7 +115,7 @@ def handle_standard_args(parser=None): parser.add_argument("-c", "--config", help="path to config dir", action='append') parser.add_argument("-o", "--output", help="path to output dir") - parser.add_argument("-d", "--data", help="path to data dir") + parser.add_argument("-d", "--data", help="path to data dir", action='append') parser.add_argument("-r", "--resume", nargs='?', const='_', type=str, help="resume after") parser.add_argument("-m", "--multiprocess", type=str2bool, nargs='?', const=True, help="run multiprocess (boolean flag, no arg defaults to true)") @@ -137,14 +137,15 @@ def override_injectable(name, value): if not os.path.exists(dir): raise IOError("Could not find configs dir '%s'" % dir) override_injectable("configs_dir", args.config) + if args.data: + for dir in args.data: + if not os.path.exists(dir): + raise IOError("Could not find data dir '%s'" % dir) + override_injectable("data_dir", args.config) if args.output: if not os.path.exists(args.output): raise IOError("Could not find output dir '%s'." % args.output) override_injectable("output_dir", args.output) - if args.data: - if not os.path.exists(args.data): - raise IOError("Could not find data dir '%s'" % args.data) - override_injectable("data_dir", args.data) if args.stride: override_injectable("households_sample_stride", args.stride) @@ -249,10 +250,37 @@ def build_output_file_path(file_name, use_prefix=None): return file_path -def data_file_path(file_name): +def cascading_input_file_path(file_name, dir_list_injectable_name, mandatory=True): + + dir_list = inject.get_injectable(dir_list_injectable_name) + + if isinstance(dir_list, str): + dir_list = [dir_list] + + assert isinstance(dir_list, list) + + file_path = None + for dir in dir_list: + p = os.path.join(dir, file_name) + if os.path.isfile(p): + file_path = p + break + + if mandatory and not file_path: + raise RuntimeError("file_path %s: file '%s' not in %s" % + (dir_list_injectable_name, file_path, dir_list)) + + return file_path + + +def data_file_path(file_name, mandatory=True): + + return cascading_input_file_path(file_name, 'data_dir', mandatory) + + +def config_file_path(file_name, mandatory=True): - data_dir = inject.get_injectable('data_dir') - return os.path.join(data_dir, file_name) + return cascading_input_file_path(file_name, 'configs_dir', mandatory) def output_file_path(file_name): @@ -304,28 +332,6 @@ def pipeline_file_path(file_name): return build_output_file_path(file_name, use_prefix=prefix) -def config_file_path(file_name, mandatory=True): - - configs_dir = inject.get_injectable('configs_dir') - - if isinstance(configs_dir, str): - configs_dir = [configs_dir] - - assert isinstance(configs_dir, list) - - file_path = None - for dir in configs_dir: - p = os.path.join(dir, file_name) - if os.path.exists(p): - file_path = p - break - - if mandatory and not file_path: - raise RuntimeError("config_file_path: file '%s' not in %s" % (file_path, configs_dir)) - - return file_path - - def read_settings_file(file_name, mandatory=True): def backfill_settings(settings, backfill): diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 23c7e8fe0..0f5793ea7 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -37,9 +37,8 @@ from activitysim.abm.tables.skims import buffers_for_skims from activitysim.abm.tables.skims import load_skims -from activitysim.abm.tables.shadow_pricing import buffers_for_shadow_pricing from activitysim.abm.tables.shadow_pricing import get_shadow_pricing_info - +from activitysim.abm.tables.shadow_pricing import buffers_for_shadow_pricing logger = logging.getLogger(__name__) diff --git a/example/configs/annotate_persons.csv b/example/configs/annotate_persons.csv index 580ba5dfb..da919ca12 100644 --- a/example/configs/annotate_persons.csv +++ b/example/configs/annotate_persons.csv @@ -16,6 +16,7 @@ presence of university student other than self in household,has_university,"othe student_is_employed,student_is_employed,"(persons.ptype.isin([constants.PTYPE_UNIVERSITY, constants.PTYPE_DRIVING]) & persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART]))" nonstudent_to_school,nonstudent_to_school,"(persons.ptype.isin([constants.PTYPE_FULL, constants.PTYPE_PART, constants.PTYPE_NONWORK, constants.PTYPE_RETIRED]) & persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY]))" is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" +#,, #,, FIXME - if person is a university student but has school age student category value then reset student category value ,pstudent,"persons.pstudent.where(persons.ptype!=constants.PTYPE_UNIVERSITY, constants.PSTUDENT_UNIVERSITY)" #,, FIXME if person is a student of any kind but has full-time employment status then reset student category value to non-student diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 97bd3955f..cb2843dc1 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -20,11 +20,11 @@ annotate_persons: SPEC: annotate_persons_school DF: persons +# - shadow pricing + MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 -# - shadow pricing - CHOOSER_TABLE_NAME: persons DESTINATION_SIZE_TABLE: school_destination_size @@ -44,8 +44,7 @@ SEGMENT_IDS: SHADOW_PRICE_TABLE: school_shadow_prices MODELED_SIZE_TABLE: school_modeled_size -SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv - +#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv # ignore criteria for zones smaller than size_threshold SIZE_THRESHOLD: 100 diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index 2a61a3b0d..6c4e71327 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -19,3 +19,40 @@ annotate_persons: DF: persons TABLES: - land_use + +# - shadow pricing + +MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 + +CHOOSER_TABLE_NAME: households + +DESTINATION_SIZE_TABLE: workplace_destination_size + +# size_terms selector +SELECTOR: work + +# chooser column with segment_id for this segment type +CHOOSER_SEGMENT_COLUMN: income_segment + +# FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this +SEGMENT_IDS: + work_low: 1 + work_med: 2 + work_high: 3 + work_veryhigh: 4 + + +SHADOW_PRICE_TABLE: workplace_shadow_prices +MODELED_SIZE_TABLE: workplace_modeled_size + +#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max number of zones allowed to fail +FAIL_THRESHOLD: 10 diff --git a/example_multiple_zone/extensions/skims.py b/example_multiple_zone/extensions/skims.py index d61d07570..d040b165a 100644 --- a/example_multiple_zone/extensions/skims.py +++ b/example_multiple_zone/extensions/skims.py @@ -8,7 +8,7 @@ import openmatrix as omx from activitysim.core import skim as askim -from activitysim.core import tracing +from activitysim.core import config from activitysim.core import inject @@ -59,7 +59,7 @@ def taz_skim_dict(data_dir, settings): logger.info("loading taz_skim_dict") - skims_file = os.path.join(data_dir, settings["taz_skims_file"]) + skims_file = config.data_file_path(settings["taz_skims_file"]) cache_skim_key_values = settings['skim_time_periods']['labels'] skim_dict = askim.SkimDict() @@ -79,7 +79,7 @@ def tap_skim_dict(data_dir, settings): skim_dict = askim.SkimDict() for skims_file in settings["tap_skims_files"]: - skims_file_path = os.path.join(data_dir, skims_file) + skims_file_path = config.data_file_path(skims_file) with omx.open_file(skims_file_path) as omx_file: add_to_skim_dict(skim_dict, omx_file, cache_skim_key_values) From a0c186e7c09bcfb11a1bc52636db80006c812208 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 10 Dec 2018 12:05:41 -0500 Subject: [PATCH 054/122] shadow_pricing implemented but not converging for partial datasets --- activitysim/abm/models/initialize.py | 28 +--- activitysim/abm/models/trip_destination.py | 2 +- .../abm/models/util/tour_destination.py | 94 ------------- activitysim/abm/models/workplace_location.py | 22 +-- activitysim/abm/tables/shadow_pricing.py | 130 ++++++++++++++++-- activitysim/abm/tables/size_terms.py | 50 ++----- .../abm/test/configs/annotate_persons.csv | 4 +- .../abm/test/configs/school_location.yaml | 6 +- .../abm/test/configs/workplace_location.yaml | 10 +- activitysim/abm/test/test_pipeline.py | 2 +- example/configs/annotate_persons.csv | 4 +- example/configs/school_location.yaml | 2 +- example/configs/workplace_location.yaml | 10 +- 13 files changed, 174 insertions(+), 190 deletions(-) delete mode 100644 activitysim/abm/models/util/tour_destination.py diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 5f18ea1a5..4b71c4577 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -20,7 +20,7 @@ from .util import expressions -from activitysim.abm.tables.size_terms import destination_predicted_size +from activitysim.abm.tables import shadow_pricing logger = logging.getLogger(__name__) @@ -80,34 +80,18 @@ def initialize_landuse(): @inject.step() -def initialize_households(): +def initialize_households(shadow_pricing_models): trace_label = 'initialize_households' model_settings = config.read_model_settings('initialize_households.yaml', mandatory=True) - annotate_tables(model_settings, trace_label) - # - ShadowPriceCalculator predicted_size tables - for dest_model_settings_file in ['school_location.yaml', 'workplace_location.yaml']: - - dest_choice_model_settings = config.read_model_settings(dest_model_settings_file) - selector = dest_choice_model_settings['SELECTOR'] - segment_ids = dest_choice_model_settings['SEGMENT_IDS'] - chooser_table_name = dest_choice_model_settings['CHOOSER_TABLE_NAME'] - chooser_segment_column = dest_choice_model_settings['CHOOSER_SEGMENT_COLUMN'] - destination_size_table_name = dest_choice_model_settings['DESTINATION_SIZE_TABLE'] - - logger.info("%s creating %s" % (trace_label, destination_size_table_name)) - - size_df = \ - destination_predicted_size( - chooser_table_name, - selector, - chooser_segment_column, - segment_ids) - inject.add_table(destination_size_table_name, size_df) + # - initialize shadow_pricing predicted_size after annotating household and person tables + # since these are scaled to model size, they have to be created while single-process + shadow_pricing.add_predicted_size_table(shadow_pricing_models) + # - preload person_windows t0 = tracing.print_elapsed_time() inject.get_table('person_windows').to_frame() t0 = tracing.print_elapsed_time("preload person_windows", t0, debug=True) diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 6fa947c47..4c293aa59 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -427,7 +427,7 @@ def run_trip_destination( logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) land_use = inject.get_table('land_use') - size_terms = inject.get_table('size_terms') + size_terms = inject.get_injectable('size_terms') # - initialize trip origin and destination to those of half-tour # (we will sequentially adjust intermediate trips origin and destination as we choose them) diff --git a/activitysim/abm/models/util/tour_destination.py b/activitysim/abm/models/util/tour_destination.py deleted file mode 100644 index 65be04d72..000000000 --- a/activitysim/abm/models/util/tour_destination.py +++ /dev/null @@ -1,94 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -import os -import logging - -import numpy as np -import pandas as pd - - -logger = logging.getLogger(__name__) - - -def size_term(land_use, destination_choice_coeffs): - """ - This method takes the land use data and multiplies various columns of the - land use data by coefficients from the spec table in order - to yield a size term (a linear combination of land use variables). - - Parameters - ---------- - land_use : DataFrame - A dataframe of land use attributes - the column names should match - the index of destination_choice_coeffs - destination_choice_coeffs : Series - A series of coefficients for the land use attributes - the index - describes the link to the land use table, and the values are floating - points numbers used to do the linear combination - - Returns - ------- - values : Series - The index will be the same as land use, and the values will the - linear combination of the land use table columns specified by the - coefficients series. - """ - coeffs = destination_choice_coeffs - - # first check for missing column in the land_use table - missing = coeffs[~coeffs.index.isin(land_use.columns)] - - if len(missing) > 0: - logger.warning("%s missing columns in land use" % len(missing.index)) - for v in missing.index.values: - logger.warning("missing: %s" % v) - - return land_use[coeffs.index].dot(coeffs) - - -def tour_destination_size_terms(land_use, size_terms, selector): - """ - - Parameters - ---------- - land_use - pipeline table - size_terms - pipeline table - selector - str - - Returns - ------- - - :: - - pandas.dataframe - one column per selector segment with index of land_use - e.g. for selector 'work', columns will be work_low, work_med, work_high, work_veryhigh - and for selector 'trip', columns will be eatout, escort, othdiscr, othmaint, ... - - work_low work_med work_high work_veryhigh - TAZ ... - 1 1267.00000 522.000 1108.000 1540.0000 ... - 2 1991.00000 824.500 1759.000 2420.0000 ... - ... - """ - - land_use = land_use.to_frame() - size_terms = size_terms.to_frame() - - size_terms = size_terms[size_terms.selector == selector].copy() - del size_terms['selector'] - - df = pd.DataFrame({key: size_term(land_use, row) for key, row in size_terms.iterrows()}, - index=land_use.index) - df.index.name = 'TAZ' - - if not (df.dtypes == 'float64').all(): - logger.warning('Surprised to find that not all size_terms were float64!') - - # - NARROW - # float16 has 3.3 decimal digits of precision, float32 has 7.2 - df = df.astype(np.float16, errors='raise') - assert np.isfinite(df.values).all() - - return df diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index ac3af9254..69b3d2f5a 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -56,8 +56,7 @@ def run_workplace_location_sample( model_settings = config.read_model_settings('workplace_location.yaml') model_spec = simulate.read_model_spec(file_name='workplace_location_sample.csv') - # FIXME - only choose workplace_location of workers? is this the right criteria? - choosers = persons_merged[persons_merged.is_worker] + choosers = persons_merged if choosers.empty: logger.info("Skipping %s: no workers" % trace_label) @@ -186,7 +185,7 @@ def run_workplace_location_simulate( choices = pd.Series() else: - choosers = persons_merged[persons_merged.is_worker] + choosers = persons_merged # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] @@ -281,6 +280,9 @@ def workplace_location( persons_merged_df = persons_merged.to_frame() + # presumably is_worker or something similar + persons_merged_df = persons_merged_df[persons_merged[model_settings['CHOOSER_FILTER_COLUMN']]] + spc = shadow_pricing.load_shadow_price_calculator(model_settings) # - max_iterations @@ -313,14 +315,18 @@ def workplace_location( spc.set_choices(choices_df) - number_of_failed_zones = spc.check_fit(iteration) - - logging.info("%s iteration: %s number_of_failed_zones: %s" % - (trace_label, iteration, number_of_failed_zones)) + fit = spc.check_fit(iteration) - if number_of_failed_zones == 0: + if fit: break + logging.info("check_fit converged: %s iteration: %s" % (fit, iter,)) + + # - convergence stats + print("\nshadow_pricing max_abs_diff\n", spc.max_abs_diff) + print("\nshadow_pricing max_rel_diff\n", spc.max_rel_diff) + print("\nshadow_pricing num_fail\n", spc.num_fail) + tracing.print_summary('workplace_taz', choices, describe=True) persons_df = persons.to_frame() diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 76a151c51..3cdb819d7 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -22,6 +22,8 @@ from activitysim.core import util from activitysim.core import config +from activitysim.abm.tables.size_terms import tour_destination_size_terms + logger = logging.getLogger(__name__) @@ -36,6 +38,7 @@ class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): + self.selector = model_settings['SELECTOR'] # informational self.segment_ids = model_settings['SEGMENT_IDS'] # - modeled_size (set by call to set_choices/synchronize_choices) @@ -46,7 +49,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.size_threshold = model_settings['SIZE_THRESHOLD'] # zone passes if modeled is within percent_tolerance of predicted_size self.percent_tolerance = model_settings['PERCENT_TOLERANCE'] - # max number of zones allowed to fail + # max percentage of zones allowed to fail self.fail_threshold = model_settings['FAIL_THRESHOLD'] # - destination_size_table (predicted_size) @@ -73,8 +76,9 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): columns=self.predicted_size.columns, index=self.predicted_size.index) - self.rms_error = pd.DataFrame(index=self.predicted_size.columns) self.num_fail = pd.DataFrame(index=self.predicted_size.columns) + self.max_abs_diff = pd.DataFrame(index=self.predicted_size.columns) + self.max_rel_diff = pd.DataFrame(index=self.predicted_size.columns) self.iter = 0 @@ -158,6 +162,21 @@ def set_choices(self, choices_df): self.modeled_size = self.synchronize_choices(modeled_size) def check_fit(self, iter): + """ + Check onvergence criteria fit of modeled_size to target predicted_size + (For multiprocessing, this is global modeled_size summed across processes, + so each process will independently calculate the same result.) + + Parameters + ---------- + iter: int + iteration number (informational, for num_failand max_diff history columns) + + Returns + ------- + good_enough: boolean + + """ assert self.modeled_size is not None assert self.predicted_size is not None @@ -170,19 +189,32 @@ def check_fit(self, iter): rel_diff = abs_diff / modeled_size # ignore zones where predicted_size < threshold - rel_diff.where(self.predicted_size >= self.size_threshold, 0, inplace=True) + rel_diff.where(predicted_size >= self.size_threshold, 0, inplace=True) # ignore zones where rel_diff < percent_tolerance rel_diff.where(rel_diff > (self.percent_tolerance / 100.0), 0, inplace=True) - num_fails = (rel_diff > 0).values.sum() + self.num_fail['iter%s' % iter] = (rel_diff > 0).sum() + self.max_abs_diff['iter%s' % iter] = abs_diff.max() + self.max_rel_diff['iter%s' % iter] = rel_diff.max() + + total_fails = (rel_diff > 0).values.sum() + + max_fail = (self.fail_threshold / 100.0) * predicted_size.shape[0] - self.rms_error['iter%s' % iter] = \ - (predicted_size - modeled_size).pow(2).sum() / len(modeled_size) + good_enough = (total_fails <= max_fail) - self.num_fail['iter%s' % iter] = num_fails + # for c in predicted_size: + # print("check_fit %s segment %s" % (self.selector, c)) + # print(" modeled %s" % (modeled_size[c].sum())) + # print(" predicted %s" % (predicted_size[c].sum())) + # print(" max abs diff %s" % (abs_diff[c].max())) + # print(" max rel diff %s" % (rel_diff[c].max())) - return num_fails + logging.info("check_fit iteration: %s good_enough: %s max_fail: %s total_fails: %s" % + (iter, good_enough, max_fail, total_fails)) + + return good_enough def update_shadow_prices(self): """ @@ -191,12 +223,24 @@ def update_shadow_prices(self): shadowPrice *= ( scaledSize / modeledDestinationLocationsByDestZone ); // else // shadowPrice *= scaledSize; + + Daysim: + targ = prediction > total + ? Math.Min(prediction, + Math.Min(total * (1 + percentTolerance / 100D), total + absoluteTolerance)) + : Math.Max(prediction, + Math.Max(total * (1 - percentTolerance / 100D), total - absoluteTolerance)); + + shadowPrice = + previousShadowPrice + Math.Log(Math.Max(targ, .01) * 1D / Math.Max(prediction, .01)); """ assert self.modeled_size is not None assert self.predicted_size is not None assert self.shadow_prices is not None + targ = self.modeled_size + new_scale_factor = self.predicted_size / self.modeled_size # FIXME - need to decide if following CTRAMP code quoted above, and if so, which version @@ -210,6 +254,9 @@ def update_shadow_prices(self): # avoid zero-divide for 0 modeled_size, by leaving shadow_prices unchanged new_shadow_prices.where(self.modeled_size > 0, self.shadow_prices, inplace=True) + print("\nself.predicted_size\n", self.predicted_size.head()) + print("\nself.modeled_size\n", self.modeled_size.head()) + self.shadow_prices = new_shadow_prices @@ -220,11 +267,14 @@ def block_name(selector): def get_shadow_pricing_info(): land_use = inject.get_table('land_use') - size_terms = inject.get_table('size_terms').to_frame() + size_terms = inject.get_injectable('size_terms') blocks = OrderedDict() - for selector in ['school']: + # shadow_pricing_models is dict of {: } + shadow_pricing_models = inject.get_injectable('shadow_pricing_models') + + for selector in shadow_pricing_models: sp_rows = len(land_use) sp_cols = len(size_terms[size_terms.selector == selector]) @@ -281,6 +331,7 @@ def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): blocks = shadow_pricing_info['blocks'] if selector not in blocks: + print("blocks", blocks) raise RuntimeError("Selector %s not in shadow_pricing_info" % selector) if block_name(selector) not in data_buffers: @@ -319,3 +370,62 @@ def load_shadow_price_calculator(model_settings): data, lock) return spc + + +def add_predicted_size_table(shadow_pricing_models): + + # shadow_pricing_models is dict of {: } + # since these are scaled to model size, they have to be created while single-process + + for selector, model_name in iteritems(shadow_pricing_models): + + model_settings = config.read_model_settings(model_name) + + assert selector == model_settings['SELECTOR'] + + segment_ids = model_settings['SEGMENT_IDS'] + chooser_table_name = model_settings['CHOOSER_TABLE_NAME'] + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] + size_table_name = model_settings['DESTINATION_SIZE_TABLE'] + + choosers_df = inject.get_table(chooser_table_name).to_frame() + if 'CHOOSER_FILTER_COLUMN' in model_settings: + choosers_df = choosers_df[choosers_df[model_settings['CHOOSER_FILTER_COLUMN']] != 0] + + # - raw_predicted_size + land_use = inject.get_table('land_use') + size_terms = inject.get_injectable('size_terms') + raw_size = tour_destination_size_terms(land_use, size_terms, selector) + assert set(raw_size.columns) == set(segment_ids.keys()) + + # - global number of choosers in each segment + segment_chooser_counts = \ + {segment_name: (choosers_df[chooser_segment_column] == segment_id).sum() + for segment_name, segment_id in iteritems(segment_ids)} + + # - segment scale factor (modeled / predicted) keyed by segment_name + # scaling reconciles differences between synthetic population and zone demographics + # in a partial sample, it also scales predicted_size targets to sample population + segment_scale_factors = {} + for c in raw_size: + segment_predicted_size = raw_size[c].astype(np.float64).sum() + segment_scale_factors[c] = \ + segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) + + # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + predicted_size = raw_size.astype(np.float64) + + for c in predicted_size: + print("destination_predicted_size %s segment %s predicted %s" % + (chooser_table_name, c, predicted_size[c].sum())) + print("destination_predicted_size %s segment %s modeled %s" % + (chooser_table_name, c, segment_chooser_counts[c].sum())) + print("destination_predicted_size %s segment %s scale_factor %s" % + (chooser_table_name, c, segment_scale_factors[c])) + + predicted_size[c] *= segment_scale_factors[c] + + logger.info("destination_predicted_size selector: %s table_name: %s" % + (selector, size_table_name, )) + + inject.add_table(size_table_name, predicted_size) diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index 0ddb17d86..f6ad3e33c 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -18,7 +18,13 @@ logger = logging.getLogger(__name__) -@inject.table() +@inject.injectable(cache=True) +def shadow_pricing_models(): + + return {'school': 'school_location', 'work': 'workplace_location'} + + +@inject.injectable(cache=True) def size_terms(): f = config.config_file_path('destination_choice_size_terms.csv') return pd.read_csv(f, comment='#', index_col='segment') @@ -87,14 +93,16 @@ def tour_destination_size_terms(land_use, size_terms, selector): """ land_use = land_use.to_frame() - size_terms = size_terms.to_frame() size_terms = size_terms[size_terms.selector == selector].copy() del size_terms['selector'] df = pd.DataFrame({key: size_term(land_use, row) for key, row in size_terms.iterrows()}, index=land_use.index) - df.index.name = 'TAZ' + + # df.index.name = 'TAZ' + assert land_use.index.name == 'TAZ' + df.index.name = land_use.index.name if not (df.dtypes == 'float64').all(): logger.warning('Surprised to find that not all size_terms were float64!') @@ -105,39 +113,3 @@ def tour_destination_size_terms(land_use, size_terms, selector): assert np.isfinite(df.values).all() return df - - -def destination_predicted_size(choosers_table, selector, chooser_segment_column, segment_ids): - - land_use = inject.get_table('land_use') - size_terms = inject.get_table('size_terms') - choosers_df = inject.get_table(choosers_table).to_frame() - - # - raw_predicted_size - raw_size = tour_destination_size_terms(land_use, size_terms, selector) - assert set(raw_size.columns) == set(segment_ids.keys()) - - segment_chooser_counts = \ - {segment_name: (choosers_df[chooser_segment_column] == segment_id).sum() - for segment_name, segment_id in iteritems(segment_ids)} - - # - segment scale factor (modeled / predicted) keyed by segment_name - # scaling reconciles differences between synthetic population and zone demographics - # in a partial sample, it also scales predicted_size targets to sample population - segment_scale_factors = {} - for c in raw_size: - segment_predicted_size = raw_size[c].astype(np.float64).sum() - segment_scale_factors[c] = \ - segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) - - # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) - predicted_size = raw_size.astype(np.float64) - for c in predicted_size: - predicted_size[c] *= segment_scale_factors[c] - - # trace_label = "destination_predicted_size %s" % (selector) - # print("%s raw_predicted_size\n" % (trace_label,), raw_size.head(20)) - # print("%s segment_scale_factors" % (trace_label,), segment_scale_factors) - # print("%s predicted_size\n" % (trace_label,), predicted_size) - - return predicted_size diff --git a/activitysim/abm/test/configs/annotate_persons.csv b/activitysim/abm/test/configs/annotate_persons.csv index da919ca12..9757b3d76 100644 --- a/activitysim/abm/test/configs/annotate_persons.csv +++ b/activitysim/abm/test/configs/annotate_persons.csv @@ -15,7 +15,6 @@ presence of part_time worker other than self in household,has_part_time,"other_t presence of university student other than self in household,has_university,"other_than(persons.household_id, persons.ptype == constants.PTYPE_UNIVERSITY)" student_is_employed,student_is_employed,"(persons.ptype.isin([constants.PTYPE_UNIVERSITY, constants.PTYPE_DRIVING]) & persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART]))" nonstudent_to_school,nonstudent_to_school,"(persons.ptype.isin([constants.PTYPE_FULL, constants.PTYPE_PART, constants.PTYPE_NONWORK, constants.PTYPE_RETIRED]) & persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY]))" -is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" #,, #,, FIXME - if person is a university student but has school age student category value then reset student category value ,pstudent,"persons.pstudent.where(persons.ptype!=constants.PTYPE_UNIVERSITY, constants.PSTUDENT_UNIVERSITY)" @@ -28,9 +27,10 @@ is_student,is_student,"pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constant is_gradeschool,is_gradeschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age <= setting('grade_school_max_age'))" is_highschool,is_highschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" is_university,is_university,"pstudent == constants.PSTUDENT_UNIVERSITY" -#,, school_segment gradeschool,school_segment,"np.where(is_gradeschool, constants.SCHOOL_SEGMENT_GRADE, constants.SCHOOL_SEGMENT_NONE)" school_segment highschool,school_segment,"np.where(is_highschool, constants.SCHOOL_SEGMENT_HIGH, school_segment)" school_segment university,school_segment,"np.where(is_university, constants.SCHOOL_SEGMENT_UNIV, school_segment).astype(np.int8)" #,, +is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" +#,, home_taz,home_taz,"reindex(households.TAZ, persons.household_id)" diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index fde88b510..0dee3f340 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -20,10 +20,8 @@ annotate_persons: SPEC: annotate_persons_school DF: persons - # - shadow pricing - MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 @@ -54,5 +52,5 @@ SIZE_THRESHOLD: 100 # zone passes if modeled is within percent_tolerance of predicted_size PERCENT_TOLERANCE: 5 -# max number of zones allowed to fail -FAIL_THRESHOLD: 10 +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 5 diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index 6c4e71327..08fb7a2cc 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -25,16 +25,20 @@ annotate_persons: MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 -CHOOSER_TABLE_NAME: households +# income_segment is in households, but we want to count persons +CHOOSER_TABLE_NAME: persons_merged DESTINATION_SIZE_TABLE: workplace_destination_size # size_terms selector SELECTOR: work -# chooser column with segment_id for this segment type +# we can't use use household income_segment as this will also be set for non-workers CHOOSER_SEGMENT_COLUMN: income_segment +# boolean column to filter choosers (True means keep) +CHOOSER_FILTER_COLUMN: is_worker + # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: work_low: 1 @@ -54,5 +58,5 @@ SIZE_THRESHOLD: 100 # zone passes if modeled is within percent_tolerance of predicted_size PERCENT_TOLERANCE: 5 -# max number of zones allowed to fail +# max percentage of zones allowed to fail FAIL_THRESHOLD: 10 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 7cb9ed8c3..d1efc5724 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -298,7 +298,7 @@ def get_trace_csv(file_name): return df -EXPECT_TOUR_COUNT = 307 +EXPECT_TOUR_COUNT = 308 def regress_tour_modes(tours_df): diff --git a/example/configs/annotate_persons.csv b/example/configs/annotate_persons.csv index da919ca12..9757b3d76 100644 --- a/example/configs/annotate_persons.csv +++ b/example/configs/annotate_persons.csv @@ -15,7 +15,6 @@ presence of part_time worker other than self in household,has_part_time,"other_t presence of university student other than self in household,has_university,"other_than(persons.household_id, persons.ptype == constants.PTYPE_UNIVERSITY)" student_is_employed,student_is_employed,"(persons.ptype.isin([constants.PTYPE_UNIVERSITY, constants.PTYPE_DRIVING]) & persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART]))" nonstudent_to_school,nonstudent_to_school,"(persons.ptype.isin([constants.PTYPE_FULL, constants.PTYPE_PART, constants.PTYPE_NONWORK, constants.PTYPE_RETIRED]) & persons.pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constants.PSTUDENT_UNIVERSITY]))" -is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" #,, #,, FIXME - if person is a university student but has school age student category value then reset student category value ,pstudent,"persons.pstudent.where(persons.ptype!=constants.PTYPE_UNIVERSITY, constants.PSTUDENT_UNIVERSITY)" @@ -28,9 +27,10 @@ is_student,is_student,"pstudent.isin([constants.PSTUDENT_GRADE_OR_HIGH, constant is_gradeschool,is_gradeschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age <= setting('grade_school_max_age'))" is_highschool,is_highschool,"(pstudent == constants.PSTUDENT_GRADE_OR_HIGH) & (persons.age > setting('grade_school_max_age'))" is_university,is_university,"pstudent == constants.PSTUDENT_UNIVERSITY" -#,, school_segment gradeschool,school_segment,"np.where(is_gradeschool, constants.SCHOOL_SEGMENT_GRADE, constants.SCHOOL_SEGMENT_NONE)" school_segment highschool,school_segment,"np.where(is_highschool, constants.SCHOOL_SEGMENT_HIGH, school_segment)" school_segment university,school_segment,"np.where(is_university, constants.SCHOOL_SEGMENT_UNIV, school_segment).astype(np.int8)" #,, +is_worker,is_worker,"persons.pemploy.isin([constants.PEMPLOY_FULL, constants.PEMPLOY_PART])" +#,, home_taz,home_taz,"reindex(households.TAZ, persons.household_id)" diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index cb2843dc1..9d1d74a16 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -52,5 +52,5 @@ SIZE_THRESHOLD: 100 # zone passes if modeled is within percent_tolerance of predicted_size PERCENT_TOLERANCE: 5 -# max number of zones allowed to fail +# max percentage of zones allowed to fail FAIL_THRESHOLD: 10 diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index 6c4e71327..08fb7a2cc 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -25,16 +25,20 @@ annotate_persons: MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 -CHOOSER_TABLE_NAME: households +# income_segment is in households, but we want to count persons +CHOOSER_TABLE_NAME: persons_merged DESTINATION_SIZE_TABLE: workplace_destination_size # size_terms selector SELECTOR: work -# chooser column with segment_id for this segment type +# we can't use use household income_segment as this will also be set for non-workers CHOOSER_SEGMENT_COLUMN: income_segment +# boolean column to filter choosers (True means keep) +CHOOSER_FILTER_COLUMN: is_worker + # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: work_low: 1 @@ -54,5 +58,5 @@ SIZE_THRESHOLD: 100 # zone passes if modeled is within percent_tolerance of predicted_size PERCENT_TOLERANCE: 5 -# max number of zones allowed to fail +# max percentage of zones allowed to fail FAIL_THRESHOLD: 10 From c101aed145de9df3dc281e386e95cb6a921d2696 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 12 Dec 2018 09:04:38 -0500 Subject: [PATCH 055/122] azure windows recipes --- example_azure/.gitignore | 1 + .../benchmarks/benchmarks_full_run.txt | 70 ++++++++--- example_azure/example_mp/settings.yaml | 16 ++- example_azure/ubuntu/azure_env.txt | 19 +++ .../{ => ubuntu}/step1_create_vm.txt | 3 - .../{ => ubuntu}/step2_config_asim.txt | 2 +- example_azure/{ => ubuntu}/step3_run_asim.txt | 39 +++++- .../util_create_smb_fileshare.txt | 0 .../util_resize_managed_storage.txt | 0 example_azure/{ => ubuntu}/vm_sizes.txt | 0 example_azure/{ => windows}/azure_env.txt | 4 +- example_azure/windows/step1_create_vm.txt | 117 ++++++++++++++++++ example_azure/windows/step2_config_asim.txt | 66 ++++++++++ example_azure/windows/step3_run.txt | 39 ++++++ example_mp/configs/settings.yaml | 68 +++------- 15 files changed, 364 insertions(+), 80 deletions(-) create mode 100644 example_azure/.gitignore create mode 100644 example_azure/ubuntu/azure_env.txt rename example_azure/{ => ubuntu}/step1_create_vm.txt (99%) rename example_azure/{ => ubuntu}/step2_config_asim.txt (92%) rename example_azure/{ => ubuntu}/step3_run_asim.txt (61%) rename example_azure/{ => ubuntu}/util_create_smb_fileshare.txt (100%) rename example_azure/{ => ubuntu}/util_resize_managed_storage.txt (100%) rename example_azure/{ => ubuntu}/vm_sizes.txt (100%) rename example_azure/{ => windows}/azure_env.txt (62%) create mode 100644 example_azure/windows/step1_create_vm.txt create mode 100644 example_azure/windows/step2_config_asim.txt create mode 100644 example_azure/windows/step3_run.txt diff --git a/example_azure/.gitignore b/example_azure/.gitignore new file mode 100644 index 000000000..c80916134 --- /dev/null +++ b/example_azure/.gitignore @@ -0,0 +1 @@ +output_*/ diff --git a/example_azure/benchmarks/benchmarks_full_run.txt b/example_azure/benchmarks/benchmarks_full_run.txt index 824a9256f..02eb84724 100644 --- a/example_azure/benchmarks/benchmarks_full_run.txt +++ b/example_azure/benchmarks/benchmarks_full_run.txt @@ -26,26 +26,39 @@ INFO - activitysim.core.mem - high water mark used: 312.758975983 timestamp: 08/ INFO - activitysim.core.mem - high water mark rss: 404.58372879 timestamp: 08/11/2018 20:17:16 label: INFO - activitysim.core.tracing - Time to execute everything : 7880.258 seconds (131.3 minutes) ------------- azure linux 432GB 64 processors +#### -# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour -Standard_E64_v3 +multiprocess: True +mem_tick: 30 +profile: False +strict: False +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +stagger: 5 + +INFO - activitysim.core.mem - high water mark used: 319.38 timestamp: 11/12/2018 18:52:26 label: +INFO - activitysim.core.mem - high water mark rss: 506.01 timestamp: 11/12/2018 18:52:26 label: +INFO - activitysim.core.tracing - Time to execute everything : 5344.844 seconds (89.1 minutes) -AZ_VM_SIZE=Standard_E64_v3 -az vm resize --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME --size $AZ_VM_SIZE +------------ azure Windows 2TB 128 processors +# 128 cpu 2TB ram $13.34/hour +AZ_VM_SIZE=Standard_M128s + +# Standard_M128s households_sample_size: 0 -chunk_size: 0 -num_processes: 30 -stagger: 15 +chunk_size: 0 +num_processes: 120 +stagger: 0 -INFO - activitysim.core.mem - high water mark used: 355.52 timestamp: 19/11/2018 21:06:08 label: -INFO - activitysim.core.mem - high water mark rss: 473.19 timestamp: 19/11/2018 21:06:08 label: -INFO - activitysim.core.tracing - Time to execute everything : 11756.813 seconds (195.9 minutes) -#################################################################################### +------------ azure linux 432GB 64 processors + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +Standard_E64_v3 export OMP_NUM_THREADS=1 @@ -65,7 +78,7 @@ INFO - activitysim.core.mem - high water mark rss: 480.60 timestamp: 20/11/2018 INFO - activitysim.core.tracing - Time to execute everything : 3609.947 seconds (60.2 minutes) -#################################################################################### +#### mem_tick: 0 @@ -84,7 +97,13 @@ INFO - activitysim.core.mem - high water mark rss: 478.95 timestamp: 20/11/2018 INFO - activitysim.core.tracing - Time to execute everything : 3636.418 seconds (60.6 minutes) -#################################################################################### +mem_tick: 0 +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +stagger: 5 + +#### mem_tick: 0 households_sample_size: 0 @@ -105,3 +124,26 @@ num_processes: 60 Time to execute everything : 3596.278 seconds (59.9 minutes) Max RAM 233.90GB + +------------ azure Windows 2TB 128 processors + +# 128 cpu 2TB ram $13.34/hour +AZ_VM_SIZE=Standard_M128s + +12/12/2018 03:32:11 - INFO - activitysim - process mp_households_62 failed with exitcode 1 +12/12/2018 03:32:13 - INFO - activitysim - process mp_households_70 failed with exitcode 1 +12/12/2018 03:32:40 - INFO - activitysim - process mp_households_53 failed with exitcode 1 +12/12/2018 03:32:54 - INFO - activitysim - process mp_households_57 failed with exitcode 1 +12/12/2018 03:33:09 - INFO - activitysim - process mp_households_45 failed with exitcode 1 +12/12/2018 03:33:13 - INFO - activitysim - process mp_households_51 failed with exitcode 1 +12/12/2018 03:33:32 - INFO - activitysim - process mp_households_31 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_31 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_45 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_51 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_53 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_57 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_62 failed with exitcode 1 +12/12/2018 04:13:57 - ERROR - activitysim - Process mp_households_70 failed with exitcode 1 + + +12/12/2018 04:13:57 - INFO - activitysim.core.tracing - Time to execute run_sub_simulations step mp_households : 2783.16 seconds (46.4 minutes) diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml index c5dbb7219..e1bfde453 100644 --- a/example_azure/example_mp/settings.yaml +++ b/example_azure/example_mp/settings.yaml @@ -4,15 +4,21 @@ inherit_settings: True # - production config multiprocess: True -mem_tick: 60 +mem_tick: 0 profile: False strict: False -households_sample_size: 0 -chunk_size: 32000000000 -num_processes: 20 -stagger: 30 +# Standard_E64s_v3 +#households_sample_size: 0 +#chunk_size: 90000000000 +#num_processes: 60 +#stagger: 5 +# Standard_M128s +households_sample_size: 0 +chunk_size: 0 +num_processes: 120 +stagger: 0 # - tracing diff --git a/example_azure/ubuntu/azure_env.txt b/example_azure/ubuntu/azure_env.txt new file mode 100644 index 000000000..68537e162 --- /dev/null +++ b/example_azure/ubuntu/azure_env.txt @@ -0,0 +1,19 @@ +AZ_VM_NUMBER=1 + +AZ_VM_NAME=ubuntu$AZ_VM_NUMBER +AZ_RESOURCE_GROUP=jeffdoyle +AZ_USERNAME=azureuser +AZ_LOCATION=eastus +AZ_VM_IMAGE=UbuntuLTS + +SHARE_NAME=myshare + + +########## + +STORAGEACCT=mystorageacct32320 + +STORAGEKEY=$(az storage account keys list \ + --resource-group $AZ_RESOURCE_GROUP \ + --account-name $STORAGEACCT \ + --query "[0].value" | tr -d '"') diff --git a/example_azure/step1_create_vm.txt b/example_azure/ubuntu/step1_create_vm.txt similarity index 99% rename from example_azure/step1_create_vm.txt rename to example_azure/ubuntu/step1_create_vm.txt index 7a9d3f8f6..f282247b3 100644 --- a/example_azure/step1_create_vm.txt +++ b/example_azure/ubuntu/step1_create_vm.txt @@ -143,6 +143,3 @@ exit az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME - - - diff --git a/example_azure/step2_config_asim.txt b/example_azure/ubuntu/step2_config_asim.txt similarity index 92% rename from example_azure/step2_config_asim.txt rename to example_azure/ubuntu/step2_config_asim.txt index 7ea7dc00e..b1be8928a 100644 --- a/example_azure/step2_config_asim.txt +++ b/example_azure/ubuntu/step2_config_asim.txt @@ -33,7 +33,7 @@ git checkout dev conda remove --name asim --all -conda create -q -n asim python=2.7 cytoolz numpy pandas pip pytables pyyaml toolz psutil +conda create -n asim python=2.7 cytoolz numpy pandas pip pytables pyyaml toolz psutil source activate asim pip install openmatrix zbox future diff --git a/example_azure/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt similarity index 61% rename from example_azure/step3_run_asim.txt rename to example_azure/ubuntu/step3_run_asim.txt index 283f2f4ca..285a9c853 100644 --- a/example_azure/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -1,12 +1,19 @@ +#AZ_VM_SIZE=Standard_D16s_v3 + + # 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour -AZ_VM_SIZE=Standard_E64s_v3 +#AZ_VM_SIZE=Standard_E64s_v3 + +# 128 cpu 2TB ram $13.34/hour +#AZ_VM_SIZE=Standard_M128s ############### resize az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME az vm resize --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME --size $AZ_VM_SIZE + ############### start az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME @@ -38,8 +45,6 @@ export OMP_NUM_THREADS=1 python simulation.py -d /datadrive/work/data/full -# 50% -#python simulation.py -s 2,0 -d /datadrive/work/data/full tar -zcvf log.tar.gz output/log/ @@ -49,4 +54,30 @@ scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz log.ta ############### -az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME \ No newline at end of file +az vm stop --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + + +############### mount smb + +# on dev + +STORAGEKEY=$(az storage account keys list \ + --resource-group $AZ_RESOURCE_GROUP \ + --account-name $STORAGEACCT \ + --query "[0].value" | tr -d '"') + +echo "STORAGEKEY=$STORAGEKEY" + +# on ubuntu + +STORAGEKEY= +STORAGEACCT=mystorageacct32320 +SHARE_NAME=myshare +sudo mkdir /mnt/fileshare +sudo mount -t cifs //$STORAGEACCT.file.core.windows.net/$SHARE_NAME /mnt/fileshare -o vers=3.0,username=$STORAGEACCT,password=$STORAGEKEY,dir_mode=0777,file_mode=0777,serverino + +FILE_NAME=windows1_128_run_1.zip +DEST_DIR=output_windows +scp $AZ_USERNAME@$VM_IP:/mnt/fileshare/work/$FILE_NAME $DEST_DIR/$FILE_NAME + diff --git a/example_azure/util_create_smb_fileshare.txt b/example_azure/ubuntu/util_create_smb_fileshare.txt similarity index 100% rename from example_azure/util_create_smb_fileshare.txt rename to example_azure/ubuntu/util_create_smb_fileshare.txt diff --git a/example_azure/util_resize_managed_storage.txt b/example_azure/ubuntu/util_resize_managed_storage.txt similarity index 100% rename from example_azure/util_resize_managed_storage.txt rename to example_azure/ubuntu/util_resize_managed_storage.txt diff --git a/example_azure/vm_sizes.txt b/example_azure/ubuntu/vm_sizes.txt similarity index 100% rename from example_azure/vm_sizes.txt rename to example_azure/ubuntu/vm_sizes.txt diff --git a/example_azure/azure_env.txt b/example_azure/windows/azure_env.txt similarity index 62% rename from example_azure/azure_env.txt rename to example_azure/windows/azure_env.txt index 10febb051..415f7ef0e 100644 --- a/example_azure/azure_env.txt +++ b/example_azure/windows/azure_env.txt @@ -1,9 +1,9 @@ AZ_VM_NUMBER=1 -AZ_VM_NAME=ubuntu$AZ_VM_NUMBER +AZ_VM_NAME=windows$AZ_VM_NUMBER AZ_RESOURCE_GROUP=jeffdoyle AZ_USERNAME=azureuser AZ_LOCATION=eastus -AZ_VM_IMAGE=UbuntuLTS +AZ_VM_IMAGE=Win2016Datacenter SHARE_NAME=myshare diff --git a/example_azure/windows/step1_create_vm.txt b/example_azure/windows/step1_create_vm.txt new file mode 100644 index 000000000..d051164c5 --- /dev/null +++ b/example_azure/windows/step1_create_vm.txt @@ -0,0 +1,117 @@ +# 128 GiB SSD Temp - want enough temp to create 64GB swap +AZ_VM_SIZE=Standard_D16s_v3 + + +########################## create vm + +az vm create \ + --resource-group $AZ_RESOURCE_GROUP \ + --name $AZ_VM_NAME \ + --image $AZ_VM_IMAGE \ + --admin-username $AZ_USERNAME \ + --size $AZ_VM_SIZE + + +########################## add a standard sdd managed disk + +#https://docs.microsoft.com/en-us/azure/virtual-machines/windows/attach-managed-disk-portal + +MANAGED_DISK_NAME=datadisk_w$AZ_VM_NUMBER +DISK_SIZE_GB=200 +DISK_SKU=StandardSSD_LRS +# Premium_LRS, StandardSSD_LRS, Standard_LRS, UltraSSD_LRS + +az vm disk attach \ + -g $AZ_RESOURCE_GROUP \ + --vm-name $AZ_VM_NAME \ + --disk $MANAGED_DISK_NAME \ + --new \ + --size-gb $DISK_SIZE_GB \ + --sku $DISK_SKU + + + +########################## connect to vm + +choose connect link in azure portal to open remote desktop + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + +#VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) + + + +########################## initialize managed disk + +Initialize a new data disk as E: drive +* Connect to the VM. +* Select the Windows Start menu inside the running VM and enter diskmgmt.msc in the search box. The Disk Management console opens. +* Disk Management recognizes that you have a new, uninitialized disk and the Initialize Disk window appears. +* Verify the new disk is selected and then select OK to initialize it. +* The new disk appears as unallocated. Right-click anywhere on the disk and select New simple volume. The New Simple Volume Wizard window opens. +* Proceed through the wizard, keeping all of the defaults, and when you're done select Finish. +* Close Disk Management. +* A pop-up window appears notifying you that you need to format the new disk before you can use it. Select Format disk. +* In the Format new disk window, check the settings, and then select Start. +* A warning appears notifying you that formatting the disks erases all of the data. Select OK. +* When the formatting is complete, select OK. + + +########################## mount smb disk + +smb disk: +https://docs.microsoft.com/en-us/azure/storage/files/storage-how-to-use-files-windows + +# Install Azure PowerShell with PowerShellGet +# https://docs.microsoft.com/en-us/powershell/azure/install-azurerm-ps?view=azurermps-6.13.0 +Install-Module -Name AzureRM -AllowClobber + + +$resourceGroupName = "" +$storageAccountName = "" + +$resourceGroupName = "jeffdoyle" +$storageAccountName = "mystorageacct32320" + +# Ensure port 445 is open +Test-NetConnection -ComputerName $storageAccountName.file.core.windows.net -Port 445 + +# log in to azure +Login-AzureRmAccount + +# These commands require you to be logged into your Azure account, run Login-AzureRmAccount if you haven't +# already logged in. +$storageAccount = Get-AzureRmStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName +$storageAccountKeys = Get-AzureRmStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName + +# The cmdkey utility is a command-line (rather than PowerShell) tool. We use Invoke-Expression to allow us to +# consume the appropriate values from the storage account variables. The value given to the add parameter of the +# cmdkey utility is the host address for the storage account, .file.core.windows.net for Azure +# Public Regions. $storageAccount.Context.FileEndpoint is used because non-Public Azure regions, such as sovereign +# clouds or Azure Stack deployments, will have different hosts for Azure file shares (and other storage resources). +Invoke-Expression -Command "cmdkey /add:$([System.Uri]::new($storageAccount.Context.FileEndPoint).Host)/user:AZURE\$($storageAccount.StorageAccountName) /pass:$($storageAccountKeys[0].Value)" + +# trust but verify +cmdkey /list + + +# Mount the Azure file share with PowerShell (persist) + +$fileShareName = "myshare" +$driveLetter = "F" + +$fileShare = Get-AzureStorageShare -Context $storageAccount.Context | Where-Object { + $_.Name -eq $fileShareName -and $_.IsSnapshot -eq $false +} + +if ($fileShare -eq $null) { + throw [System.Exception]::new("Azure file share not found") +} + +# The value given to the root parameter of the New-PSDrive cmdlet is the host address for the storage account, +# .file.core.windows.net for Azure Public Regions. $fileShare.StorageUri.PrimaryUri.Host is +# used because non-Public Azure regions, such as sovereign clouds or Azure Stack deployments, will have different +# hosts for Azure file shares (and other storage resources). +$password = ConvertTo-SecureString -String $storageAccountKeys[0].Value -AsPlainText -Force +$credential = New-Object System.Management.Automation.PSCredential -ArgumentList "AZURE\$($storageAccount.StorageAccountName)", $password +New-PSDrive -Name $driveLetter -PSProvider FileSystem -Root "\\$($fileShare.StorageUri.PrimaryUri.Host)\$($fileShare.Name)" -Credential $credential -Persist diff --git a/example_azure/windows/step2_config_asim.txt b/example_azure/windows/step2_config_asim.txt new file mode 100644 index 000000000..6ecd5e985 --- /dev/null +++ b/example_azure/windows/step2_config_asim.txt @@ -0,0 +1,66 @@ + +###################### install chocolatey and git from powershell administrater + +# install chocolatey +Get-ExecutionPolicy +Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + +# install git and unix tools using chocolatey +choco install git -params '"/GitAndUnixToolsOnPath"' + +# install 7zip +choco install 7zip.install + +refreshenv + +exit + + +###################### install miniconda locally in E:\miniconda2 + +E:\ + +wget https://repo.continuum.io/miniconda/Miniconda2-latest-Windows-x86_64.exe -OutFile install_miniconda.exe +.\install_miniconda.exe /InstallationType=JustMe /RegisterPython=0 /S /D=E:\miniconda2 + +# activate conda base +E:\miniconda2\Scripts\activate.bat E:\miniconda2 + + +################################################ switch to anaconda prompt + + +########################## init work dir + + +# copy from smb fileshare to datadrive +xcopy /E Z:\work\data E:\data\ + + +###################### clone activitysim repo + +git clone https://github.com/ActivitySim/activitysim.git activitysim + +cd activitysim + +git checkout dev + +###################### create asim conda env + +$env:Path += ";E:\miniconda2\Scripts" + + +# activate conda base +activate.bat E:\miniconda2 + +#conda remove --name asim --all + +conda create -n asim python=2.7 cytoolz numpy pandas pip pytables pyyaml toolz psutil +activate.bat asim +pip install openmatrix zbox future + +git status +pip install -e . + +cd example_mp +python simulation.py -d C:\Users\azureuser\work\sf_county_data diff --git a/example_azure/windows/step3_run.txt b/example_azure/windows/step3_run.txt new file mode 100644 index 000000000..b3f34a5fb --- /dev/null +++ b/example_azure/windows/step3_run.txt @@ -0,0 +1,39 @@ + + + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +#AZ_VM_SIZE=Standard_E64s_v3 + +# 128 cpu 2TB ram $13.34/hour +AZ_VM_SIZE=Standard_M128s + +############### resize + +az vm deallocate --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME +az vm resize --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME --size $AZ_VM_SIZE + +az vm start --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + + +az vm stop --resource-group $AZ_RESOURCE_GROUP --name $AZ_VM_NAME + +################################################ anaconda prompt + +cd example_mp + +git status + +activate asim + +set OPENBLAS_NUM_THREADS=1 +set MKL_NUM_THREADS=1 +set NUMEXPR_NUM_THREADS=1 +set OMP_NUM_THREADS=1 + + +python simulation.py -d E:\data\full -m + +python simulation.py -d E:\data\sf_county -m + + +# diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index fc3eff021..d406917fa 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -3,67 +3,33 @@ inherit_settings: True # - production config #multiprocess: True -#mem_tick: 30 #profile: False #strict: False -#num_processes: 10 -#stagger: 30 +#mem_tick: 0 -# azure 432GB 64 processors -#multiprocess: True -#mem_tick: 30 -#profile: False -#strict: False -#num_processes: 30 -#stagger: 15 - -# - dev config 3% -#multiprocess: True -#mem_tick: 30 -#profile: False -#strict: False -#num_processes: 3 -#stagger: 30 - -# - dev config mini +# - dev config multiprocess: True -mem_tick: 30 profile: False strict: False -#num_processes: 3 -#stagger: 5 - +mem_tick: 30 -# - full sample +# - full sample - 2732722 households on 64 processor 432 GiB RAM #households_sample_size: 0 -#chunk_size: 15000000000 - -# - 50% sample -#households_sample_size: 1366361 -#chunk_size: 7500000000 - -# - 20% sample -#households_sample_size: 546544 -#chunk_size: 3000000000 - -# - 10% sample -#households_sample_size: 273272 -#chunk_size: 1500000000 - -# - 3.3% sample -#households_sample_size: 91091 -#chunk_size: 500000000 - -# - mini sample -#households_sample_size: 300 -#households_sample_size: 0 -#households_sample_stride: [3, 0] +#chunk_size: 90000000000 +#num_processes: 60 +#stagger: 5 +# - small sample +households_sample_size: 5000 +chunk_size: 500000000 +num_processes: 2 +stagger: 5 -households_sample_size: 0 -chunk_size: 1000000000 -num_processes: 1 -households_sample_stride: [36,0] +# - stride sample +#households_sample_size: 0 +#chunk_size: 1000000000 +#num_processes: 1 +#households_sample_stride: [120,0] # - tracing trace_hh_id: From 31b1e644d0cbe66917a7152e3cf0217dadd2411d Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 13 Dec 2018 11:37:02 -0500 Subject: [PATCH 056/122] global use_shadow_pricing setting --- activitysim/abm/models/initialize.py | 4 +- activitysim/abm/models/school_location.py | 6 +- activitysim/abm/models/workplace_location.py | 6 +- activitysim/abm/tables/shadow_pricing.py | 90 ++++++++++++------- activitysim/abm/tables/size_terms.py | 6 -- .../abm/test/configs/school_location.yaml | 4 +- activitysim/abm/test/configs/settings.yaml | 4 + .../abm/test/configs/workplace_location.yaml | 4 +- activitysim/core/tracing.py | 19 ++-- example/configs/school_location.yaml | 12 +-- example/configs/settings.yaml | 9 +- example/configs/workplace_location.yaml | 11 +-- example_mp/configs/school_location.yaml | 22 +++++ example_mp/configs/settings.yaml | 21 +++-- example_mp/configs/workplace_location.yaml | 22 +++++ 15 files changed, 161 insertions(+), 79 deletions(-) create mode 100644 example_mp/configs/school_location.yaml create mode 100644 example_mp/configs/workplace_location.yaml diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index 4b71c4577..be17306e6 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -80,7 +80,7 @@ def initialize_landuse(): @inject.step() -def initialize_households(shadow_pricing_models): +def initialize_households(): trace_label = 'initialize_households' @@ -89,7 +89,7 @@ def initialize_households(shadow_pricing_models): # - initialize shadow_pricing predicted_size after annotating household and person tables # since these are scaled to model size, they have to be created while single-process - shadow_pricing.add_predicted_size_table(shadow_pricing_models) + shadow_pricing.add_predicted_size_table() # - preload person_windows t0 = tracing.print_elapsed_time() diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index c560595ff..c1c4034ad 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -375,12 +375,8 @@ def school_location( persons_merged_df = persons_merged.to_frame() spc = shadow_pricing.load_shadow_price_calculator(model_settings) + max_iterations = spc.max_iterations - # - max_iterations - if spc.saved_shadow_prices: - max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED', 1) - else: - max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) choices = None diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 69b3d2f5a..4bb98b5ed 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -284,12 +284,8 @@ def workplace_location( persons_merged_df = persons_merged_df[persons_merged[model_settings['CHOOSER_FILTER_COLUMN']]] spc = shadow_pricing.load_shadow_price_calculator(model_settings) + max_iterations = spc.max_iterations - # - max_iterations - if spc.saved_shadow_prices: - max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED', 1) - else: - max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) choices = None diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 3cdb819d7..205180d70 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -38,6 +38,12 @@ class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): + self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) + + full_model_run = config.setting('households_sample_size') == 0 + if self.use_shadow_pricing and not full_model_run: + logging.warning("deprecated combination of use_shadow_pricing and not full_model_run") + self.selector = model_settings['SELECTOR'] # informational self.segment_ids = model_settings['SEGMENT_IDS'] @@ -53,7 +59,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.fail_threshold = model_settings['FAIL_THRESHOLD'] # - destination_size_table (predicted_size) - destination_size_table_name = model_settings['DESTINATION_SIZE_TABLE'] + destination_size_table_name = model_settings['DESTINATION_PREDICTED_SIZE_TABLE'] self.predicted_size = inject.get_table(destination_size_table_name).to_frame() assert set(self.predicted_size.columns) == set(self.segment_ids.keys()) @@ -65,11 +71,19 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.shared_data = shared_data self.shared_data_lock = shared_data_lock - # - load saved shadow_prices - self.shadow_prices = self.load_saved_shadow_prices(model_settings) - self.saved_shadow_prices = (self.shadow_prices is not None) + # - load saved shadow_prices (if available) + if self.use_shadow_pricing: + self.shadow_prices = self.load_saved_shadow_prices(model_settings) + + if self.saved_shadow_prices: + self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_SAVED', 1) + else: + self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) + else: + self.shadow_prices = None + self.max_iterations = 1 - # - if we couldn't load saved shadow_prices, init to ones + # - if we did't load saved shadow_prices, init shadow_prices to all ones if self.shadow_prices is None: self.shadow_prices = \ pd.DataFrame(data=1.0, @@ -80,8 +94,6 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.max_abs_diff = pd.DataFrame(index=self.predicted_size.columns) self.max_rel_diff = pd.DataFrame(index=self.predicted_size.columns) - self.iter = 0 - def load_saved_shadow_prices(self, model_settings): shadow_prices = None @@ -161,15 +173,15 @@ def set_choices(self, choices_df): self.modeled_size = self.synchronize_choices(modeled_size) - def check_fit(self, iter): + def check_fit(self, iteration): """ - Check onvergence criteria fit of modeled_size to target predicted_size + Check convergence criteria fit of modeled_size to target predicted_size (For multiprocessing, this is global modeled_size summed across processes, so each process will independently calculate the same result.) Parameters ---------- - iter: int + iteration: int iteration number (informational, for num_failand max_diff history columns) Returns @@ -194,9 +206,9 @@ def check_fit(self, iter): # ignore zones where rel_diff < percent_tolerance rel_diff.where(rel_diff > (self.percent_tolerance / 100.0), 0, inplace=True) - self.num_fail['iter%s' % iter] = (rel_diff > 0).sum() - self.max_abs_diff['iter%s' % iter] = abs_diff.max() - self.max_rel_diff['iter%s' % iter] = rel_diff.max() + self.num_fail['iter%s' % iteration] = (rel_diff > 0).sum() + self.max_abs_diff['iter%s' % iteration] = abs_diff.max() + self.max_rel_diff['iter%s' % iteration] = rel_diff.max() total_fails = (rel_diff > 0).values.sum() @@ -212,7 +224,7 @@ def check_fit(self, iter): # print(" max rel diff %s" % (rel_diff[c].max())) logging.info("check_fit iteration: %s good_enough: %s max_fail: %s total_fails: %s" % - (iter, good_enough, max_fail, total_fails)) + (iteration, good_enough, max_fail, total_fails)) return good_enough @@ -235,12 +247,14 @@ def update_shadow_prices(self): previousShadowPrice + Math.Log(Math.Max(targ, .01) * 1D / Math.Max(prediction, .01)); """ + assert self.use_shadow_pricing + + # can't update_shadow_prices until after first iteration + # modeled_size should have been set by set_choices at end of previous iteration assert self.modeled_size is not None assert self.predicted_size is not None assert self.shadow_prices is not None - targ = self.modeled_size - new_scale_factor = self.predicted_size / self.modeled_size # FIXME - need to decide if following CTRAMP code quoted above, and if so, which version @@ -269,11 +283,10 @@ def get_shadow_pricing_info(): land_use = inject.get_table('land_use') size_terms = inject.get_injectable('size_terms') - blocks = OrderedDict() - # shadow_pricing_models is dict of {: } - shadow_pricing_models = inject.get_injectable('shadow_pricing_models') + shadow_pricing_models = config.setting('shadow_pricing_models') + blocks = OrderedDict() for selector in shadow_pricing_models: sp_rows = len(land_use) @@ -372,7 +385,20 @@ def load_shadow_price_calculator(model_settings): return spc -def add_predicted_size_table(shadow_pricing_models): +def add_predicted_size_table(): + + use_shadow_pricing = bool(config.setting('use_shadow_pricing')) + shadow_pricing_models = config.setting('shadow_pricing_models') + full_model_run = config.setting('households_sample_size') == 0 + + scale_predicted_size = use_shadow_pricing or full_model_run + if not scale_predicted_size: + logger.info("add_predicted_size_table using raw size " + "(not scaling) since no shadow pricing and not full_model_run") + + if shadow_pricing_models is None: + logger.warning('add_predicted_size_table: shadow_pricing_models not in settings') + return # shadow_pricing_models is dict of {: } # since these are scaled to model size, they have to be created while single-process @@ -386,7 +412,7 @@ def add_predicted_size_table(shadow_pricing_models): segment_ids = model_settings['SEGMENT_IDS'] chooser_table_name = model_settings['CHOOSER_TABLE_NAME'] chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] - size_table_name = model_settings['DESTINATION_SIZE_TABLE'] + size_table_name = model_settings['DESTINATION_PREDICTED_SIZE_TABLE'] choosers_df = inject.get_table(chooser_table_name).to_frame() if 'CHOOSER_FILTER_COLUMN' in model_settings: @@ -412,20 +438,22 @@ def add_predicted_size_table(shadow_pricing_models): segment_scale_factors[c] = \ segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) - # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) predicted_size = raw_size.astype(np.float64) - for c in predicted_size: - print("destination_predicted_size %s segment %s predicted %s" % - (chooser_table_name, c, predicted_size[c].sum())) - print("destination_predicted_size %s segment %s modeled %s" % - (chooser_table_name, c, segment_chooser_counts[c].sum())) - print("destination_predicted_size %s segment %s scale_factor %s" % - (chooser_table_name, c, segment_scale_factors[c])) + if scale_predicted_size: + + # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + for c in predicted_size: + logger.info("add_predicted_size_table %s segment %s " + "predicted %s modeled %s scale_factor %s" % + (chooser_table_name, c, + predicted_size[c].sum(), + segment_chooser_counts[c].sum(), + segment_scale_factors[c])) - predicted_size[c] *= segment_scale_factors[c] + predicted_size[c] *= segment_scale_factors[c] - logger.info("destination_predicted_size selector: %s table_name: %s" % + logger.info("add_predicted_size_table selector: %s adding table: %s" % (selector, size_table_name, )) inject.add_table(size_table_name, predicted_size) diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index f6ad3e33c..56c0dfb33 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -18,12 +18,6 @@ logger = logging.getLogger(__name__) -@inject.injectable(cache=True) -def shadow_pricing_models(): - - return {'school': 'school_location', 'work': 'workplace_location'} - - @inject.injectable(cache=True) def size_terms(): f = config.config_file_path('destination_choice_size_terms.csv') diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index 0dee3f340..f8f6aa96d 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -23,11 +23,11 @@ annotate_persons: # - shadow pricing MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 CHOOSER_TABLE_NAME: persons -DESTINATION_SIZE_TABLE: school_destination_size +DESTINATION_PREDICTED_SIZE_TABLE: school_destination_size # size_terms selector SELECTOR: school diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 8fd943ae6..53adbe15f 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -37,3 +37,7 @@ skim_time_periods: - AM - MD - PM + +shadow_pricing_models: + school: school_location + work: workplace_location diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index 08fb7a2cc..3c8567629 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -23,12 +23,12 @@ annotate_persons: # - shadow pricing MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -DESTINATION_SIZE_TABLE: workplace_destination_size +DESTINATION_PREDICTED_SIZE_TABLE: workplace_destination_size # size_terms selector SELECTOR: work diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 1d7968114..2bbc43bc3 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -268,13 +268,13 @@ def register_traceable_table(table_name, df): def write_df_csv(df, file_path, index_label=None, columns=None, column_labels=None, transpose=True): - mode = 'a' if os.path.isfile(file_path) else 'w' + need_header = not os.path.isfile(file_path) if columns: df = df[columns] if not transpose: - df.to_csv(file_path, mode="a", index=df.index.name is not None, header=True) + df.to_csv(file_path, mode='a', index=df.index.name is not None, header=need_header) return df_t = df.transpose() @@ -283,7 +283,8 @@ def write_df_csv(df, file_path, index_label=None, columns=None, column_labels=No elif index_label: df_t.index.name = index_label - with open(file_path, mode=mode) as f: + if need_header: + if column_labels is None: column_labels = [None, None] if column_labels[0] is None: @@ -298,10 +299,10 @@ def write_df_csv(df, file_path, index_label=None, columns=None, column_labels=No column_labels[0] + ',' \ + ','.join([column_labels[1] + '_' + str(i+1) for i in range(len(df_t.columns))]) - if mode == 'a': - column_label_row = '# ' + column_label_row - f.write(column_label_row + '\n') - df_t.to_csv(file_path, mode='a', index=True, header=True) + with open(file_path, mode='a') as f: + f.write(column_label_row + '\n') + + df_t.to_csv(file_path, mode='a', index=True, header=False) def write_series_csv(series, file_path, index_label=None, columns=None, column_labels=None): @@ -314,7 +315,9 @@ def write_series_csv(series, file_path, index_label=None, columns=None, column_l series = series.rename(columns[1]) if index_label and series.index.name is None: series.index.name = index_label - series.to_csv(file_path, mode='a', index=True, header=True) + + need_header = not os.path.isfile(file_path) + series.to_csv(file_path, mode='a', index=True, header=need_header) def write_csv(df, file_name, index_label=None, columns=None, column_labels=None, transpose=True): diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 9d1d74a16..03d17b0d3 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -23,11 +23,11 @@ annotate_persons: # - shadow pricing MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 +# required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons - -DESTINATION_SIZE_TABLE: school_destination_size +DESTINATION_PREDICTED_SIZE_TABLE: school_destination_size # size_terms selector SELECTOR: school @@ -41,9 +41,11 @@ SEGMENT_IDS: highschool: 2 gradeschool: 1 -SHADOW_PRICE_TABLE: school_shadow_prices -MODELED_SIZE_TABLE: school_modeled_size +# model adds these tables (informational - not added if commented out) +#SHADOW_PRICE_TABLE: school_shadow_prices +#MODELED_SIZE_TABLE: school_modeled_size +# not loaded if commented out #SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv # ignore criteria for zones smaller than size_threshold diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 5a288470f..165c6f3f9 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -17,9 +17,11 @@ trace_od: chunk_size: 0 -# comment out or set false to disable variability check in simple_simulate and interaction_simulate +# set false to disable variability check in simple_simulate and interaction_simulate check_for_variability: False +# set false to disable shadow_pricing +use_shadow_pricing: False models: - initialize_landuse @@ -101,3 +103,8 @@ skim_time_periods: - AM - MD - PM + +shadow_pricing_models: + school: school_location + work: workplace_location + diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index 08fb7a2cc..ca08805a8 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -23,12 +23,12 @@ annotate_persons: # - shadow pricing MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_WITH_SAVED: 1 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -DESTINATION_SIZE_TABLE: workplace_destination_size +DESTINATION_PREDICTED_SIZE_TABLE: workplace_destination_size # size_terms selector SELECTOR: work @@ -46,10 +46,11 @@ SEGMENT_IDS: work_high: 3 work_veryhigh: 4 +# model adds these tables (informational - not added if commented out) +#SHADOW_PRICE_TABLE: workplace_shadow_prices +#MODELED_SIZE_TABLE: workplace_modeled_size -SHADOW_PRICE_TABLE: workplace_shadow_prices -MODELED_SIZE_TABLE: workplace_modeled_size - +# not loaded if commented out #SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv # ignore criteria for zones smaller than size_threshold diff --git a/example_mp/configs/school_location.yaml b/example_mp/configs/school_location.yaml new file mode 100644 index 000000000..c7cf470e2 --- /dev/null +++ b/example_mp/configs/school_location.yaml @@ -0,0 +1,22 @@ +inherit_settings: True + +# - shadow pricing + +MAX_SHADOW_PRICE_ITERATIONS: 2 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 + +# model adds these tables (informational - not added if commented out) +SHADOW_PRICE_TABLE: school_shadow_prices +MODELED_SIZE_TABLE: school_modeled_size + +# not loaded if commented out +#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 10 diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 128ab4cb0..c7bbb677d 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -6,12 +6,7 @@ inherit_settings: True #profile: False #strict: False #mem_tick: 0 - -# - dev config -multiprocess: True -profile: False -strict: False -mem_tick: 30 +#use_shadow_pricing: True # - full sample - 2732722 households on 64 processor 432 GiB RAM #households_sample_size: 0 @@ -19,6 +14,13 @@ mem_tick: 30 #num_processes: 60 #stagger: 5 +# - dev config +multiprocess: True +profile: False +strict: False +mem_tick: 30 +use_shadow_pricing: False + # - small sample households_sample_size: 5000 chunk_size: 500000000 @@ -127,7 +129,12 @@ output_tables: # - persons # - trips # - tours - +# - school_shadow_prices +# - school_destination_size +# - school_modeled_size +# - workplace_shadow_prices +# - workplace_destination_size +# - workplace_modeled_size coalesce: names: diff --git a/example_mp/configs/workplace_location.yaml b/example_mp/configs/workplace_location.yaml new file mode 100644 index 000000000..382ae9765 --- /dev/null +++ b/example_mp/configs/workplace_location.yaml @@ -0,0 +1,22 @@ +inherit_settings: True + +# - shadow pricing + +MAX_SHADOW_PRICE_ITERATIONS: 2 +MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 + +# model adds these tables (informational - not added if commented out) +SHADOW_PRICE_TABLE: workplace_shadow_prices +MODELED_SIZE_TABLE: workplace_modeled_size + +# not loaded if commented out +#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 100 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 10 From a3d6335b67894aba6fe5a5bb48f99cb4486c15cd Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 13 Dec 2018 14:44:03 -0500 Subject: [PATCH 057/122] mp_tasks fail_fast settign and better exception logging --- activitysim/abm/models/util/cdap.py | 2 ++ activitysim/abm/test/test_pipeline.py | 6 ++++-- activitysim/core/mp_tasks.py | 21 ++++++++++++++++----- example_azure/ubuntu/step3_run_asim.txt | 6 ++++-- example_mp/configs/settings.yaml | 3 +++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 1ca07f963..0e50e2cf7 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -748,6 +748,8 @@ def extra_hh_member_choices(persons, cdap_fixed_relative_proportions, locals_d, list of alternatives chosen for all extra members, indexed by _persons_index_ """ + trace_label = tracing.extend_trace_label(trace_label, 'extra_hh_member_choices') + # extra household members have cdap_ran > MAX_HHSIZE choosers = persons[persons['cdap_rank'] > MAX_HHSIZE] diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index d1efc5724..d80eb522f 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -217,11 +217,13 @@ def test_mini_pipeline_run2(): checkpoints_df = pipeline.get_checkpoints() assert len(checkpoints_df.index) == prev_checkpoint_count - # - write list of override_hh_ids to override_hh_ids.csv in configs for use in next test + # - write list of override_hh_ids to override_hh_ids.csv in data for use in next test num_hh_ids = 10 hh_ids = pipeline.get_table("households").head(num_hh_ids).index.values hh_ids = pd.DataFrame({'household_id': hh_ids}) - hh_ids.to_csv(os.path.join(configs_dir, 'override_hh_ids.csv'), index=False, header=True) + + data_dir = inject.get_injectable('data_dir') + hh_ids.to_csv(os.path.join(data_dir, 'override_hh_ids.csv'), index=False, header=True) pipeline.close_pipeline() inject.clear_cache() diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 0f5793ea7..5951de9c1 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -385,8 +385,8 @@ def run_simulation(queue, step_info, resume_after, shared_data_buffer): logger.info('resume_after %s', resume_after) # if they specified a resume_after model, check to make sure it is checkpointed - if resume_after != '_' \ - and resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: + if resume_after != '_' and resume_after \ + not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: # if not checkpointed, then fall back to last checkpoint logger.info("resume_after checkpoint '%s' not in pipeline.", resume_after) resume_after = '_' @@ -405,7 +405,13 @@ def run_simulation(queue, step_info, resume_after, shared_data_buffer): for model in models: t1 = tracing.print_elapsed_time() - pipeline.run_model(model) + + try: + pipeline.run_model(model) + except Exception as e: + logger.warning("%s exception running %s model: %s", type(e).__name__, model, str(e), + exc_info=True) + raise e queue.put({'model': model, 'time': time.time()-t1}) @@ -488,7 +494,7 @@ def run_sub_simulations( injectables, shared_data_buffers, step_info, process_names, - resume_after, previously_completed): + resume_after, previously_completed, fail_fast): def log_queued_messages(): for i, process, queue in zip(list(range(num_simulations)), procs, queues): @@ -516,6 +522,8 @@ def check_proc_status(): logger.info("process %s failed with exitcode %s", p.name, p.exitcode) failed.add(p.name) mem.trace_memory_info("%s.failed" % p.name) + if fail_fast: + raise RuntimeError("Process %s failed" % (p.name,)) def idle(seconds): log_queued_messages() @@ -635,6 +643,9 @@ def run_multiprocess(run_list, injectables): old_breadcrumbs = run_list.get('breadcrumbs', {}) + # raise error if any sub-process fails without waiting for others to complete + fail_fast = setting('fail_fast') + def skip_phase(phase): skip = old_breadcrumbs and old_breadcrumbs.get(step_name, {}).get(phase, False) if skip: @@ -695,7 +706,7 @@ def find_breadcrumb(crumb, default=None): completed = run_sub_simulations(injectables, shared_data_buffers, step_info, - sub_proc_names, resume_after, completed) + sub_proc_names, resume_after, completed, fail_fast) if len(completed) != num_processes: raise RuntimeError("%s processes failed in step %s" % diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 285a9c853..697cf00ba 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -46,11 +46,13 @@ export OMP_NUM_THREADS=1 python simulation.py -d /datadrive/work/data/full -tar -zcvf log.tar.gz output/log/ +tar -zcvf log.tar.gz output/log/ +tar -zcvf trace.tar.gz output/trace/ exit -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz log.tar.gz +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz output_ubuntu/log.tar.gz +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/trace.tar.gz output_ubuntu/trace.tar.gz ############### diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index c7bbb677d..da8835f6d 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -1,6 +1,9 @@ inherit_settings: True +# raise error if any sub-process fails without waiting for others to complete +fail_fast: True + # - production config #multiprocess: True #profile: False From b4c8f75ccf97f91d2041185fa2bff88dfdab854c Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 13 Dec 2018 15:56:37 -0500 Subject: [PATCH 058/122] bug in handle_standard_args where data_dir not getting set --- activitysim/core/config.py | 2 +- example_azure/ubuntu/step3_run_asim.txt | 11 ++++--- example_mp/configs/school_location.yaml | 2 +- example_mp/configs/settings.yaml | 38 +++++++++++----------- example_mp/configs/workplace_location.yaml | 2 +- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 2b90d9e5d..5008ad08e 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -141,7 +141,7 @@ def override_injectable(name, value): for dir in args.data: if not os.path.exists(dir): raise IOError("Could not find data dir '%s'" % dir) - override_injectable("data_dir", args.config) + override_injectable("data_dir", args.data) if args.output: if not os.path.exists(args.output): raise IOError("Could not find output dir '%s'." % args.output) diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 697cf00ba..e8ff5f912 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -21,8 +21,11 @@ VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.networ # copy settings to example_mp/configs -scp example_mp/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml -scp example_mp/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml +scp example_mp/configs/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml +scp example_mp/configs/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml + +#scp example_mp/configs/school_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/school_location.yaml +#scp example_mp/configs/workplace_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/workplace_location.yaml ############### run @@ -51,8 +54,8 @@ tar -zcvf trace.tar.gz output/trace/ exit -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz output_ubuntu/log.tar.gz -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/trace.tar.gz output_ubuntu/trace.tar.gz +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz example_azure/output_ubuntu/log.tar.gz +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/trace.tar.gz example_azure/output_ubuntu/trace.tar.gz ############### diff --git a/example_mp/configs/school_location.yaml b/example_mp/configs/school_location.yaml index c7cf470e2..fa7d0bec8 100644 --- a/example_mp/configs/school_location.yaml +++ b/example_mp/configs/school_location.yaml @@ -2,7 +2,7 @@ inherit_settings: True # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 2 +MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # model adds these tables (informational - not added if commented out) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index da8835f6d..b9ac0438b 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -5,31 +5,31 @@ inherit_settings: True fail_fast: True # - production config -#multiprocess: True -#profile: False -#strict: False -#mem_tick: 0 -#use_shadow_pricing: True - -# - full sample - 2732722 households on 64 processor 432 GiB RAM -#households_sample_size: 0 -#chunk_size: 90000000000 -#num_processes: 60 -#stagger: 5 - -# - dev config multiprocess: True profile: False strict: False -mem_tick: 30 -use_shadow_pricing: False +mem_tick: 0 +use_shadow_pricing: True -# - small sample -households_sample_size: 5000 -chunk_size: 500000000 -num_processes: 2 +# - full sample - 2732722 households on 64 processor 432 GiB RAM +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 stagger: 5 +## - dev config +#multiprocess: True +#profile: False +#strict: False +#mem_tick: 30 +#use_shadow_pricing: False +# +## - small sample +#households_sample_size: 5000 +#chunk_size: 500000000 +#num_processes: 2 +#stagger: 5 + # - stride sample #households_sample_size: 0 #chunk_size: 1000000000 diff --git a/example_mp/configs/workplace_location.yaml b/example_mp/configs/workplace_location.yaml index 382ae9765..ee7d452c0 100644 --- a/example_mp/configs/workplace_location.yaml +++ b/example_mp/configs/workplace_location.yaml @@ -2,7 +2,7 @@ inherit_settings: True # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 2 +MAX_SHADOW_PRICE_ITERATIONS: 5 MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # model adds these tables (informational - not added if commented out) From 56a5f4a6322ad5991e9d0a8c53f25da24ba22245 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 13 Dec 2018 19:48:21 -0500 Subject: [PATCH 059/122] stale variable reference --- activitysim/abm/models/school_location.py | 16 ++++++++-------- activitysim/abm/tables/shadow_pricing.py | 2 +- activitysim/abm/test/configs/settings.yaml | 1 + activitysim/abm/test/output/trace/.gitignore | 1 + activitysim/abm/test/test_pipeline.py | 14 +++++++++++++- example_mp/configs/settings.yaml | 11 +++++++++-- 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 activitysim/abm/test/output/trace/.gitignore diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index c1c4034ad..3254f520e 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -377,7 +377,7 @@ def school_location( spc = shadow_pricing.load_shadow_price_calculator(model_settings) max_iterations = spc.max_iterations - logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) + logging.info("%s max_iterations: %s" % (trace_label, max_iterations)) choices = None for iteration in range(max_iterations): @@ -402,16 +402,16 @@ def school_location( spc.set_choices(choices_df) - number_of_failed_zones = spc.check_fit(iteration) + fit = spc.check_fit(iteration) - logging.info("%s iteration: %s number_of_failed_zones: %s" % - (trace_label, iteration, number_of_failed_zones)) - - if number_of_failed_zones == 0: + if fit: break - # - print convergence stats - # print("\nshadow_pricing rms_error\n", spc.rms_error) + logging.info("check_fit converged: %s iteration: %s" % (fit, iter,)) + + # - convergence stats + print("\nshadow_pricing max_abs_diff\n", spc.max_abs_diff) + print("\nshadow_pricing max_rel_diff\n", spc.max_rel_diff) print("\nshadow_pricing num_fail\n", spc.num_fail) persons_df = persons.to_frame() diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 205180d70..ea0b3cee0 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -75,7 +75,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): if self.use_shadow_pricing: self.shadow_prices = self.load_saved_shadow_prices(model_settings) - if self.saved_shadow_prices: + if self.shadow_prices: self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_SAVED', 1) else: self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 53adbe15f..2269d2fb2 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -41,3 +41,4 @@ skim_time_periods: shadow_pricing_models: school: school_location work: workplace_location + diff --git a/activitysim/abm/test/output/trace/.gitignore b/activitysim/abm/test/output/trace/.gitignore new file mode 100644 index 000000000..afed0735d --- /dev/null +++ b/activitysim/abm/test/output/trace/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index d80eb522f..69d6db656 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -46,6 +46,10 @@ def setup_dirs(configs_dir): tracing.config_logger() + tracing.delete_output_files('csv') + tracing.delete_output_files('txt') + tracing.delete_output_files('yaml') + def teardown_function(func): inject.clear_cache() @@ -144,7 +148,10 @@ def test_mini_pipeline_run(): setup_dirs(configs_dir) - inject_settings(configs_dir, households_sample_size=HOUSEHOLDS_SAMPLE_SIZE) + inject_settings(configs_dir, + households_sample_size=HOUSEHOLDS_SAMPLE_SIZE, + # use_shadow_pricing=True + ) _MODELS = [ 'initialize_landuse', @@ -157,6 +164,11 @@ def test_mini_pipeline_run(): pipeline.run(models=_MODELS, resume_after=None) + # data_dir = inject.get_injectable('data_dir') + # school_destination_size = pipeline.get_table("school_shadow_prices") + # school_destination_size.to_csv(os.path.join(data_dir, 'school_shadow_prices.csv'), + # index=True, header=True) + regress_mini_auto() pipeline.run_model('cdap_simulate') diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index b9ac0438b..67b5b530b 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -4,7 +4,7 @@ inherit_settings: True # raise error if any sub-process fails without waiting for others to complete fail_fast: True -# - production config +# - ------------------------- production config multiprocess: True profile: False strict: False @@ -17,7 +17,14 @@ chunk_size: 90000000000 num_processes: 60 stagger: 5 -## - dev config +# - full sample - 2732722 households on Standard_M128s +#households_sample_size: 0 +#chunk_size: 0 +#num_processes: 124 +#stagger: 0 + + +## - ------------------------- dev config #multiprocess: True #profile: False #strict: False From 5c59dbc509f458962ec78f3eacfabdb36178906e Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 17 Dec 2018 12:09:16 -0500 Subject: [PATCH 060/122] trace shadow_prices --- activitysim/abm/models/school_location.py | 36 +++-- activitysim/abm/models/workplace_location.py | 34 +++-- activitysim/abm/tables/shadow_pricing.py | 130 +++++++++++------- .../abm/test/configs/school_location.yaml | 2 - .../abm/test/configs/workplace_location.yaml | 2 - activitysim/core/config.py | 7 + activitysim/core/mp_tasks.py | 12 +- example/configs/school_location.yaml | 1 - example/configs/settings.yaml | 23 +++- example/configs/workplace_location.yaml | 4 - .../benchmarks/benchmarks_full_run.txt | 14 +- example_azure/ubuntu/step3_run_asim.txt | 26 ++-- example_mp/configs/logging.yaml | 4 +- example_mp/configs/school_location.yaml | 2 +- example_mp/configs/settings.yaml | 55 ++------ example_mp/configs/workplace_location.yaml | 2 +- 16 files changed, 200 insertions(+), 154 deletions(-) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 3254f520e..5c64324fe 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -365,7 +365,9 @@ def school_location( persons_merged, persons, skim_dict, skim_stack, chunk_size, - trace_hh_id): + trace_hh_id, + locutor + ): trace_label = 'school_location' model_settings = config.read_model_settings('school_location.yaml') @@ -385,13 +387,10 @@ def school_location( if iteration > 0: spc.update_shadow_prices() - # - shadow_price adjusted predicted_size - shadow_price_adjusted_predicted_size = spc.predicted_size * spc.shadow_prices - choices = run_school_location( persons_merged_df, skim_dict, skim_stack, - shadow_price_adjusted_predicted_size, + spc.shadow_price_adjusted_predicted_size(), model_settings, chunk_size, trace_hh_id, trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) @@ -404,15 +403,28 @@ def school_location( fit = spc.check_fit(iteration) + if locutor: + spc.write_trace_files(iteration) + if fit: break - logging.info("check_fit converged: %s iteration: %s" % (fit, iter,)) + if fit: + logging.info("%s converged after iteration %s" % (trace_label, iteration,)) + else: + logging.info("%s did not converge after iteration %s" % (trace_label, iteration,)) + + # - shadow price table + if locutor: + if 'SHADOW_PRICE_TABLE' in model_settings: + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + if 'MODELED_SIZE_TABLE' in model_settings: + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) # - convergence stats - print("\nshadow_pricing max_abs_diff\n", spc.max_abs_diff) - print("\nshadow_pricing max_rel_diff\n", spc.max_rel_diff) - print("\nshadow_pricing num_fail\n", spc.num_fail) + logging.info("\nshadow_pricing max_abs_diff\n%s" % spc.max_abs_diff) + logging.info("\nshadow_pricing max_rel_diff\n%s" % spc.max_rel_diff) + logging.info("\nshadow_pricing num_fail\n%s" % spc.num_fail) persons_df = persons.to_frame() @@ -421,12 +433,6 @@ def school_location( persons_df['school_taz'] = choices.reindex(persons_df.index).fillna(NO_SCHOOL_TAZ).astype(int) # tracing.print_summary('school_taz', choices, value_counts=True) - # - shadow price table - if 'SHADOW_PRICE_TABLE' in model_settings: - inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) - if 'MODELED_SIZE_TABLE' in model_settings: - inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) - # - annotate persons model_name = 'school_location' model_settings = config.read_model_settings('school_location.yaml') diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 4bb98b5ed..5d90441c2 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -271,7 +271,7 @@ def run_workplace_location( def workplace_location( persons_merged, persons, skim_dict, skim_stack, - chunk_size, trace_hh_id): + chunk_size, trace_hh_id, locutor): trace_label = 'workplace_location' model_settings = config.read_model_settings('workplace_location.yaml') @@ -294,13 +294,10 @@ def workplace_location( if iteration > 0: spc.update_shadow_prices() - # - shadow_price adjusted predicted_size - shadow_price_adjusted_predicted_size = spc.predicted_size * spc.shadow_prices - choices = run_workplace_location( persons_merged_df, skim_dict, skim_stack, - shadow_price_adjusted_predicted_size, + spc.shadow_price_adjusted_predicted_size(), model_settings, chunk_size, trace_hh_id, trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) @@ -313,15 +310,28 @@ def workplace_location( fit = spc.check_fit(iteration) + if locutor: + spc.write_trace_files(iteration) + if fit: break - logging.info("check_fit converged: %s iteration: %s" % (fit, iter,)) + if fit: + logging.info("%s converged after iteration %s" % (trace_label, iteration,)) + else: + logging.info("%s did not converge after iteration %s" % (trace_label, iteration,)) # - convergence stats - print("\nshadow_pricing max_abs_diff\n", spc.max_abs_diff) - print("\nshadow_pricing max_rel_diff\n", spc.max_rel_diff) - print("\nshadow_pricing num_fail\n", spc.num_fail) + logging.info("\nshadow_pricing max_abs_diff\n%s" % spc.max_abs_diff) + logging.info("\nshadow_pricing max_rel_diff\n%s" % spc.max_rel_diff) + logging.info("\nshadow_pricing num_fail\n%s" % spc.num_fail) + + # - shadow price table + if locutor: + if 'SHADOW_PRICE_TABLE' in model_settings: + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + if 'MODELED_SIZE_TABLE' in model_settings: + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) tracing.print_summary('workplace_taz', choices, describe=True) @@ -333,12 +343,6 @@ def workplace_location( persons_df['workplace_taz'] = \ choices.reindex(persons_df.index).fillna(NO_WORKPLACE_TAZ).astype(int) - # - shadow price table - if 'SHADOW_PRICE_TABLE' in model_settings: - inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) - if 'MODELED_SIZE_TABLE' in model_settings: - inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) - # - annotate persons model_name = 'workplace_location' model_settings = config.read_model_settings('workplace_location.yaml') diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index ea0b3cee0..47844c8fc 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -21,6 +21,7 @@ from activitysim.core import inject from activitysim.core import util from activitysim.core import config +from activitysim.core import tracing from activitysim.abm.tables.size_terms import tour_destination_size_terms @@ -34,17 +35,34 @@ TALLY_CHECKOUT = (1, -1) +def size_table_name(selector, scaled=True): + if scaled: + table_name = "scaled_%s_destination_size" % selector + else: + table_name = "raw_%s_destination_size" % selector + return table_name + + +def get_size_table(selector, scaled=True): + return inject.get_table(size_table_name(selector, scaled)).to_frame() + + +USE_RAW_SIZE = True + + class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) + self.use_saved_shadow_prices = bool(config.setting('use_saved_shadow_prices')) + + self.selector = model_settings['SELECTOR'] full_model_run = config.setting('households_sample_size') == 0 if self.use_shadow_pricing and not full_model_run: logging.warning("deprecated combination of use_shadow_pricing and not full_model_run") - self.selector = model_settings['SELECTOR'] # informational self.segment_ids = model_settings['SEGMENT_IDS'] # - modeled_size (set by call to set_choices/synchronize_choices) @@ -59,9 +77,9 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.fail_threshold = model_settings['FAIL_THRESHOLD'] # - destination_size_table (predicted_size) - destination_size_table_name = model_settings['DESTINATION_PREDICTED_SIZE_TABLE'] - self.predicted_size = inject.get_table(destination_size_table_name).to_frame() - assert set(self.predicted_size.columns) == set(self.segment_ids.keys()) + if USE_RAW_SIZE: + self.raw_predicted_size = get_size_table(self.selector, scaled=False) + self.predicted_size = get_size_table(self.selector, scaled=True) # - shared_data if shared_data is not None: @@ -71,19 +89,21 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.shared_data = shared_data self.shared_data_lock = shared_data_lock - # - load saved shadow_prices (if available) + # - load saved shadow_prices (if available) and set max_iterations accordingly + self.shadow_prices = None if self.use_shadow_pricing: - self.shadow_prices = self.load_saved_shadow_prices(model_settings) + if self.load_saved_shadow_prices: + self.shadow_prices = self.load_saved_shadow_prices(model_settings) - if self.shadow_prices: - self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_SAVED', 1) - else: + if self.shadow_prices is None: self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) + else: + self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_SAVED', 1) else: - self.shadow_prices = None self.max_iterations = 1 - # - if we did't load saved shadow_prices, init shadow_prices to all ones + # - if we did't load saved shadow_prices, initialize shadow_prices to all ones + # this will start first iteration with no shadow price adjustment, if self.shadow_prices is None: self.shadow_prices = \ pd.DataFrame(data=1.0, @@ -105,7 +125,7 @@ def load_saved_shadow_prices(self, model_settings): file_path = config.data_file_path(saved_shadow_price_file_name, mandatory=False) if file_path: shadow_prices = pd.read_csv(file_path, index_col=0) - logging.warning("loading saved_shadow_prices from %s" % (file_path)) + logging.info("loading saved_shadow_prices from %s" % (file_path)) else: logging.warning("Could not find saved_shadow_prices file %s" % (file_path)) @@ -124,7 +144,6 @@ def get_tally(t): def wait(tally, target, tally_name): while get_tally(tally) != target: - logger.warning("waiting on %s target %s" % (tally_name, target)) time.sleep(1) # - nobody checks in until checkout clears @@ -143,7 +162,7 @@ def wait(tally, target, tally_name): # - copy shared data and check out with self.shared_data_lock: - logger.warning("copy shared_data") + logger.info("copy shared_data") # numpy array with sum of local_modeled_size.values from all processes global_modeled_size_array = self.shared_data[..., 0:-1].copy() self.shared_data[TALLY_CHECKOUT] += 1 @@ -153,7 +172,7 @@ def wait(tally, target, tally_name): wait(TALLY_CHECKOUT, num_processes, 'TALLY_CHECKOUT') with self.shared_data_lock: self.shared_data[:] = 0 - logger.warning("first_in clearing shared_data") + logger.info("first_in clearing shared_data") # convert summed numpy array data to conform to original dataframe return pd.DataFrame(data=global_modeled_size_array, @@ -186,7 +205,7 @@ def check_fit(self, iteration): Returns ------- - good_enough: boolean + converged: boolean """ @@ -214,7 +233,7 @@ def check_fit(self, iteration): max_fail = (self.fail_threshold / 100.0) * predicted_size.shape[0] - good_enough = (total_fails <= max_fail) + converged = (total_fails <= max_fail) # for c in predicted_size: # print("check_fit %s segment %s" % (self.selector, c)) @@ -223,10 +242,10 @@ def check_fit(self, iteration): # print(" max abs diff %s" % (abs_diff[c].max())) # print(" max rel diff %s" % (rel_diff[c].max())) - logging.info("check_fit iteration: %s good_enough: %s max_fail: %s total_fails: %s" % - (iteration, good_enough, max_fail, total_fails)) + logging.info("check_fit %s iteration: %s converged: %s max_fail: %s total_fails: %s" % + (self.selector, iteration, converged, max_fail, total_fails)) - return good_enough + return converged def update_shadow_prices(self): """ @@ -268,11 +287,31 @@ def update_shadow_prices(self): # avoid zero-divide for 0 modeled_size, by leaving shadow_prices unchanged new_shadow_prices.where(self.modeled_size > 0, self.shadow_prices, inplace=True) - print("\nself.predicted_size\n", self.predicted_size.head()) - print("\nself.modeled_size\n", self.modeled_size.head()) + # print("\nself.predicted_size\n", self.predicted_size.head()) + # print("\nself.modeled_size\n", self.modeled_size.head()) self.shadow_prices = new_shadow_prices + def shadow_price_adjusted_predicted_size(self): + + if USE_RAW_SIZE: + return self.raw_predicted_size * self.shadow_prices + else: + return self.predicted_size * self.shadow_prices + + def write_trace_files(self, iteration): + logger.info("write_trace_files iteration %s" % iteration) + if iteration == 0: + tracing.write_csv(self.predicted_size, + 'shadow_price_%s_predicted_size_%s' % (self.selector, iteration), + transpose=False) + tracing.write_csv(self.modeled_size, + 'shadow_price_%s_modeled_size_%s' % (self.selector, iteration), + transpose=False) + tracing.write_csv(self.shadow_prices, + 'shadow_price_%s_shadow_prices_%s' % (self.selector, iteration), + transpose=False) + def block_name(selector): return selector @@ -344,7 +383,6 @@ def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): blocks = shadow_pricing_info['blocks'] if selector not in blocks: - print("blocks", blocks) raise RuntimeError("Selector %s not in shadow_pricing_info" % selector) if block_name(selector) not in data_buffers: @@ -387,14 +425,7 @@ def load_shadow_price_calculator(model_settings): def add_predicted_size_table(): - use_shadow_pricing = bool(config.setting('use_shadow_pricing')) shadow_pricing_models = config.setting('shadow_pricing_models') - full_model_run = config.setting('households_sample_size') == 0 - - scale_predicted_size = use_shadow_pricing or full_model_run - if not scale_predicted_size: - logger.info("add_predicted_size_table using raw size " - "(not scaling) since no shadow pricing and not full_model_run") if shadow_pricing_models is None: logger.warning('add_predicted_size_table: shadow_pricing_models not in settings') @@ -412,7 +443,6 @@ def add_predicted_size_table(): segment_ids = model_settings['SEGMENT_IDS'] chooser_table_name = model_settings['CHOOSER_TABLE_NAME'] chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] - size_table_name = model_settings['DESTINATION_PREDICTED_SIZE_TABLE'] choosers_df = inject.get_table(chooser_table_name).to_frame() if 'CHOOSER_FILTER_COLUMN' in model_settings: @@ -421,9 +451,12 @@ def add_predicted_size_table(): # - raw_predicted_size land_use = inject.get_table('land_use') size_terms = inject.get_injectable('size_terms') - raw_size = tour_destination_size_terms(land_use, size_terms, selector) + raw_size = tour_destination_size_terms(land_use, size_terms, selector).astype(np.float64) assert set(raw_size.columns) == set(segment_ids.keys()) + if USE_RAW_SIZE: + inject.add_table(size_table_name(selector, scaled=False), raw_size) + # - global number of choosers in each segment segment_chooser_counts = \ {segment_name: (choosers_df[chooser_segment_column] == segment_id).sum() @@ -434,26 +467,25 @@ def add_predicted_size_table(): # in a partial sample, it also scales predicted_size targets to sample population segment_scale_factors = {} for c in raw_size: + # number of zone demographics predicted destination choices segment_predicted_size = raw_size[c].astype(np.float64).sum() - segment_scale_factors[c] = \ - segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) - predicted_size = raw_size.astype(np.float64) + # number of synthetic population choosers in segment + segment_chooser_count = (choosers_df[chooser_segment_column] == segment_ids[c]).sum() - if scale_predicted_size: - - # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) - for c in predicted_size: - logger.info("add_predicted_size_table %s segment %s " - "predicted %s modeled %s scale_factor %s" % - (chooser_table_name, c, - predicted_size[c].sum(), - segment_chooser_counts[c].sum(), - segment_scale_factors[c])) + segment_scale_factors[c] = \ + segment_chooser_count / np.maximum(segment_predicted_size, 1) - predicted_size[c] *= segment_scale_factors[c] + logger.info("add_predicted_size_table %s segment %s " + "predicted %s modeled %s scale_factor %s" % + (chooser_table_name, c, + segment_predicted_size, + segment_chooser_count, + segment_scale_factors[c])) - logger.info("add_predicted_size_table selector: %s adding table: %s" % - (selector, size_table_name, )) + # segment_scale_factors[c] = \ + # segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) - inject.add_table(size_table_name, predicted_size) + # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + scaled_size = raw_size * segment_scale_factors + inject.add_table(size_table_name(selector, scaled=True), scaled_size) diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index f8f6aa96d..150d0b20c 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -27,8 +27,6 @@ MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 CHOOSER_TABLE_NAME: persons -DESTINATION_PREDICTED_SIZE_TABLE: school_destination_size - # size_terms selector SELECTOR: school diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index 3c8567629..57310a9b9 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -28,8 +28,6 @@ MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -DESTINATION_PREDICTED_SIZE_TABLE: workplace_destination_size - # size_terms selector SELECTOR: work diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 5008ad08e..1c95b591b 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -20,6 +20,13 @@ """ +@inject.injectable(cache=True) +def locutor(): + # when multiprocessing, sometimes you only want one process to write trace files + # mp_tasks overrides this definition to designate a single sub-process as locutor + return True + + @inject.injectable(cache=True) def configs_dir(): if not os.path.exists('configs'): diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 5951de9c1..efee4d75b 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -356,12 +356,13 @@ def allocate_shared_shadow_pricing_buffers(): return shadow_pricing_buffers -def setup_injectables_and_logging(injectables): +def setup_injectables_and_logging(injectables, locutor=True): for k, v in iteritems(injectables): inject.add_injectable(k, v) inject.add_injectable("is_sub_task", True) + inject.add_injectable("locutor", locutor) filter_warnings() @@ -430,12 +431,12 @@ def profile_path(): """ -def mp_run_simulation(queue, injectables, step_info, resume_after, **kwargs): +def mp_run_simulation(locutor, queue, injectables, step_info, resume_after, **kwargs): shared_data_buffer = kwargs # handle_standard_args() - setup_injectables_and_logging(injectables) + setup_injectables_and_logging(injectables, locutor) mem.init_trace(setting('mem_tick')) @@ -560,10 +561,11 @@ def idle(seconds): failed = set([]) # so we can log process failure when it happens drop_breadcrumb(step_name, 'completed', list(completed)) - for process_name in process_names: + for i, process_name in enumerate(process_names): q = multiprocessing.Queue() + spokesman = (i == 0) p = multiprocessing.Process(target=mp_run_simulation, name=process_name, - args=(q, injectables, step_info, resume_after,), + args=(spokesman, q, injectables, step_info, resume_after,), kwargs=shared_data_buffers) procs.append(p) queues.append(q) diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 03d17b0d3..c3c344096 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -27,7 +27,6 @@ MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons -DESTINATION_PREDICTED_SIZE_TABLE: school_destination_size # size_terms selector SELECTOR: school diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 165c6f3f9..2ea5e2dd8 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -7,6 +7,22 @@ households_sample_size: 100 # simulate all households #households_sample_size: 0 +chunk_size: 0 + +# set false to disable variability check in simple_simulate and interaction_simulate +check_for_variability: False + +# - shadow pricing global switches + +# turn shadow_pricing on and off for all models (e.g. school and work) +use_shadow_pricing: True + +# global switch to enable/disable loading of saved shadow prices +# (ignored if global use_shadow_pricing switch is False) +load_saved_shadow_prices: True + +# - tracing + #trace household id; comment out or leave empty for no trace #trace_hh_id: 1482966 trace_hh_id: @@ -15,13 +31,6 @@ trace_hh_id: #trace_od: [5, 11] trace_od: -chunk_size: 0 - -# set false to disable variability check in simple_simulate and interaction_simulate -check_for_variability: False - -# set false to disable shadow_pricing -use_shadow_pricing: False models: - initialize_landuse diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index ca08805a8..121524c28 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -22,14 +22,10 @@ annotate_persons: # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -DESTINATION_PREDICTED_SIZE_TABLE: workplace_destination_size - # size_terms selector SELECTOR: work diff --git a/example_azure/benchmarks/benchmarks_full_run.txt b/example_azure/benchmarks/benchmarks_full_run.txt index 02eb84724..14aabea6c 100644 --- a/example_azure/benchmarks/benchmarks_full_run.txt +++ b/example_azure/benchmarks/benchmarks_full_run.txt @@ -122,7 +122,19 @@ households_sample_size: 0 chunk_size: 90000000000 num_processes: 60 Time to execute everything : 3596.278 seconds (59.9 minutes) -Max RAM 233.90GB +Max RAM 233.90GB + + +# with shadow pricing 5 iterations + +households_sample_size: 0 +chunk_size: 90000000000 +num_processes: 60 +stagger: 5 + +INFO - activitysim.core.mem - high water mark used: 325.70 timestamp: 14/12/2018 02:38:08 label: mp_households_15.non_mandatory_tour_destination.completed +INFO - activitysim.core.mem - high water mark rss: 557.75 timestamp: 14/12/2018 02:38:08 label: mp_households_15.non_mandatory_tour_destination.completed +INFO - activitysim.core.tracing - Time to execute everything : 6017.381 seconds (100.3 minutes) ------------ azure Windows 2TB 128 processors diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index e8ff5f912..8a077d348 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -22,10 +22,12 @@ VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.networ # copy settings to example_mp/configs scp example_mp/configs/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml -scp example_mp/configs/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml +#scp example_mp/configs/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml +scp example_mp/configs/school_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/school_location.yaml +scp example_mp/configs/workplace_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/workplace_location.yaml -#scp example_mp/configs/school_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/school_location.yaml -#scp example_mp/configs/workplace_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/workplace_location.yaml + +scp activitysim/abm/tables/shadow_pricing.py $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/activitysim/abm/tables/shadow_pricing.py ############### run @@ -36,11 +38,20 @@ hash -r source activate asim +cd /datadrive/work/activitysim +git stash +git pull +#git stash pop +#git stash drop + cd /datadrive/work/activitysim/example_mp +# copy shadow prices to data dir +cp output/final_school_shadow_prices.csv /datadrive/work/data/full/school_shadow_prices.csv +cp output/final_workplace_shadow_prices.csv /datadrive/work/data/full/workplace_shadow_prices.csv + nano configs/settings.yaml -export VECLIB_MAXIMUM_THREADS=1 # osx-specific export OPENBLAS_NUM_THREADS=1 export MKL_NUM_THREADS=1 export NUMEXPR_NUM_THREADS=1 @@ -49,13 +60,12 @@ export OMP_NUM_THREADS=1 python simulation.py -d /datadrive/work/data/full -tar -zcvf log.tar.gz output/log/ -tar -zcvf trace.tar.gz output/trace/ +tar zcvf output.tar.gz output/log/ output/trace/ exit -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/log.tar.gz example_azure/output_ubuntu/log.tar.gz -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/trace.tar.gz example_azure/output_ubuntu/trace.tar.gz +TAR_TAG=azure-64-ubuntu-shadow2 +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz ############### diff --git a/example_mp/configs/logging.yaml b/example_mp/configs/logging.yaml index e4da6d784..c4c01a3af 100644 --- a/example_mp/configs/logging.yaml +++ b/example_mp/configs/logging.yaml @@ -15,7 +15,7 @@ logging: loggers: activitysim: - level: DEBUG + level: INFO handlers: [console, logfile] propagate: false @@ -38,7 +38,7 @@ logging: stream: ext://sys.stdout formatter: simpleFormatter #level: WARNING - level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [WARNING, NOTSET] formatters: diff --git a/example_mp/configs/school_location.yaml b/example_mp/configs/school_location.yaml index fa7d0bec8..01ac27f50 100644 --- a/example_mp/configs/school_location.yaml +++ b/example_mp/configs/school_location.yaml @@ -2,7 +2,7 @@ inherit_settings: True # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS: 10 MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # model adds these tables (informational - not added if commented out) diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 67b5b530b..26568ae11 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -10,6 +10,8 @@ profile: False strict: False mem_tick: 0 use_shadow_pricing: True +load_saved_shadow_prices: True + # - full sample - 2732722 households on 64 processor 432 GiB RAM households_sample_size: 0 @@ -29,14 +31,15 @@ stagger: 5 #profile: False #strict: False #mem_tick: 30 -#use_shadow_pricing: False +#use_shadow_pricing: True +#load_saved_shadow_prices: False # ## - small sample #households_sample_size: 5000 #chunk_size: 500000000 #num_processes: 2 #stagger: 5 - +# # - stride sample #households_sample_size: 0 #chunk_size: 1000000000 @@ -103,54 +106,24 @@ multiprocess_steps: - name: mp_summarize begin: write_data_dictionary -#multiprocess_steps: -# - name: mp_initialize_landuse -# begin: initialize_landuse -# - name: mp_accessibility -# begin: compute_accessibility -# num_processes: 2 -# slice: -# tables: -# - accessibility -# except: -# - land_use -# - name: mp_initialize_households -# begin: initialize_households -# - name: mp_households -# begin: school_location -# # num_processes = 0 means use all available cpus -# num_processes: 2 -# slice: -# tables: -# - households -# - persons -# - name: mp_summarize -# begin: write_data_dictionary - output_tables: action: include prefix: final_ tables: - - checkpoints +# - checkpoints # - accessibility # - land_use # - households # - persons # - trips # - tours -# - school_shadow_prices -# - school_destination_size -# - school_modeled_size -# - workplace_shadow_prices -# - workplace_destination_size -# - workplace_modeled_size + - school_shadow_prices + - raw_school_destination_size + - scaled_school_destination_size + - school_modeled_size + - workplace_shadow_prices + - raw_work_destination_size + - scaled_work_destination_size + - workplace_modeled_size -coalesce: - names: - - pipeline_0.h5 - - pipeline_1.h5 - slice: - tables: - - households - - persons diff --git a/example_mp/configs/workplace_location.yaml b/example_mp/configs/workplace_location.yaml index ee7d452c0..caf0fc8fc 100644 --- a/example_mp/configs/workplace_location.yaml +++ b/example_mp/configs/workplace_location.yaml @@ -2,7 +2,7 @@ inherit_settings: True # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 +MAX_SHADOW_PRICE_ITERATIONS: 10 MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # model adds these tables (informational - not added if commented out) From 91f29234922dd7db64e08c136633e3ef9569b6f9 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 17 Dec 2018 17:07:00 -0500 Subject: [PATCH 061/122] bug in shadow_pricing max)fail calculation --- activitysim/abm/models/school_location.py | 3 +++ activitysim/abm/models/workplace_location.py | 3 +++ activitysim/abm/tables/shadow_pricing.py | 2 +- activitysim/core/mp_tasks.py | 1 + example_azure/ubuntu/step3_run_asim.txt | 20 ++++++++++---------- example_mp/configs/settings.yaml | 7 +++++-- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py index 5c64324fe..d2a66743e 100644 --- a/activitysim/abm/models/school_location.py +++ b/activitysim/abm/models/school_location.py @@ -18,6 +18,7 @@ from activitysim.core import pipeline from activitysim.core import simulate from activitysim.core import inject +from activitysim.core.mem import force_garbage_collect from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.interaction_sample import interaction_sample @@ -395,6 +396,8 @@ def school_location( chunk_size, trace_hh_id, trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) + force_garbage_collect() + choices_df = choices.to_frame('dest_choice') choices_df['segment_id'] = \ persons_merged_df[chooser_segment_column].reindex(choices_df.index) diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py index 5d90441c2..c1b1c8f86 100644 --- a/activitysim/abm/models/workplace_location.py +++ b/activitysim/abm/models/workplace_location.py @@ -14,6 +14,7 @@ from activitysim.core import pipeline from activitysim.core import simulate from activitysim.core import inject +from activitysim.core.mem import force_garbage_collect from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.interaction_sample import interaction_sample @@ -302,6 +303,8 @@ def workplace_location( chunk_size, trace_hh_id, trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) + force_garbage_collect() + choices_df = choices.to_frame('dest_choice') choices_df['segment_id'] = \ persons_merged_df[chooser_segment_column].reindex(choices_df.index) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 47844c8fc..08a6981dd 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -231,7 +231,7 @@ def check_fit(self, iteration): total_fails = (rel_diff > 0).values.sum() - max_fail = (self.fail_threshold / 100.0) * predicted_size.shape[0] + max_fail = (self.fail_threshold / 100.0) * np.prod(predicted_size.shape) converged = (total_fails <= max_fail) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index efee4d75b..69b12dcfb 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -647,6 +647,7 @@ def run_multiprocess(run_list, injectables): # raise error if any sub-process fails without waiting for others to complete fail_fast = setting('fail_fast') + logger.info("run_multiprocess fail_fast: %s", fail_fast) def skip_phase(phase): skip = old_breadcrumbs and old_breadcrumbs.get(step_name, {}).get(phase, False) diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 8a077d348..05ded19fc 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -27,8 +27,6 @@ scp example_mp/configs/school_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/ scp example_mp/configs/workplace_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/workplace_location.yaml -scp activitysim/abm/tables/shadow_pricing.py $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/activitysim/abm/tables/shadow_pricing.py - ############### run ssh $AZ_USERNAME@$VM_IP @@ -38,19 +36,21 @@ hash -r source activate asim -cd /datadrive/work/activitysim -git stash -git pull +# - git +#cd /datadrive/work/activitysim +#git stash +#git pull #git stash pop #git stash drop -cd /datadrive/work/activitysim/example_mp -# copy shadow prices to data dir -cp output/final_school_shadow_prices.csv /datadrive/work/data/full/school_shadow_prices.csv -cp output/final_workplace_shadow_prices.csv /datadrive/work/data/full/workplace_shadow_prices.csv +# - copy shadow prices to data dir +#cp output/final_school_shadow_prices.csv /datadrive/work/data/full/school_shadow_prices.csv +#cp output/final_workplace_shadow_prices.csv /datadrive/work/data/full/workplace_shadow_prices.csv + +cd /datadrive/work/activitysim/example_mp -nano configs/settings.yaml +#nano configs/settings.yaml export OPENBLAS_NUM_THREADS=1 export MKL_NUM_THREADS=1 diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 26568ae11..04a39eea9 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -15,9 +15,9 @@ load_saved_shadow_prices: True # - full sample - 2732722 households on 64 processor 432 GiB RAM households_sample_size: 0 -chunk_size: 90000000000 +chunk_size: 80000000000 num_processes: 60 -stagger: 5 +stagger: 0 # - full sample - 2732722 households on Standard_M128s #households_sample_size: 0 @@ -56,9 +56,11 @@ trace_od: #resume_after: trip_purpose_and_destination models: + ### mp_initialize step - initialize_landuse - compute_accessibility - initialize_households + ### mp_households step - school_location - workplace_location - auto_ownership_simulate @@ -88,6 +90,7 @@ models: - trip_purpose_and_destination - trip_scheduling - trip_mode_choice + ### mp_summarize step - write_data_dictionary - write_tables From 301958c3ccf4f29eecfcee0a34209aae7b1851ed Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 20 Dec 2018 17:12:13 -0500 Subject: [PATCH 062/122] consolidate school and workplace location model logic and add shadow_pricing settings file --- activitysim/abm/models/__init__.py | 3 +- activitysim/abm/models/initialize.py | 4 +- activitysim/abm/models/location_choice.py | 423 ++++++++++++++++ activitysim/abm/models/school_location.py | 452 ------------------ activitysim/abm/models/util/logsums.py | 4 +- activitysim/abm/models/workplace_location.py | 363 -------------- activitysim/abm/tables/shadow_pricing.py | 229 +++++---- .../configs/destination_choice_size_terms.csv | 8 +- .../abm/test/configs/school_location.csv | 16 +- .../abm/test/configs/school_location.yaml | 30 +- .../test/configs/school_location_sample.csv | 15 +- activitysim/abm/test/configs/settings.yaml | 16 +- .../abm/test/configs/shadow_pricing.yaml | 32 ++ .../abm/test/configs/workplace_location.csv | 31 +- .../abm/test/configs/workplace_location.yaml | 25 +- .../configs/workplace_location_sample.csv | 27 +- .../test/configs_shadow/shadow_pricing.yaml | 7 + activitysim/abm/test/test_pipeline.py | 13 +- activitysim/core/chunk.py | 12 +- activitysim/core/interaction_sample.py | 2 +- activitysim/core/tracing.py | 3 +- docs/core.rst | 2 +- .../configs/destination_choice_size_terms.csv | 8 +- example/configs/school_location.csv | 16 +- example/configs/school_location.yaml | 31 +- example/configs/school_location_sample.csv | 15 +- example/configs/settings.yaml | 9 +- example/configs/shadow_pricing.yaml | 34 ++ example/configs/workplace_location.csv | 31 +- example/configs/workplace_location.yaml | 24 +- example/configs/workplace_location_sample.csv | 27 +- .../benchmarks/benchmarks_full_run.txt | 12 +- example_azure/ubuntu/step3_run_asim.txt | 1 + example_mp/configs/school_location.yaml | 22 - example_mp/configs/settings.yaml | 2 - example_mp/configs/shadow_pricing.yaml | 33 ++ example_mp/configs/workplace_location.yaml | 22 - 37 files changed, 847 insertions(+), 1157 deletions(-) create mode 100644 activitysim/abm/models/location_choice.py delete mode 100644 activitysim/abm/models/school_location.py delete mode 100644 activitysim/abm/models/workplace_location.py create mode 100644 activitysim/abm/test/configs/shadow_pricing.yaml create mode 100644 activitysim/abm/test/configs_shadow/shadow_pricing.yaml create mode 100644 example/configs/shadow_pricing.yaml delete mode 100644 example_mp/configs/school_location.yaml create mode 100644 example_mp/configs/shadow_pricing.yaml delete mode 100644 example_mp/configs/workplace_location.yaml diff --git a/activitysim/abm/models/__init__.py b/activitysim/abm/models/__init__.py index e9ebf8dff..f074f0688 100644 --- a/activitysim/abm/models/__init__.py +++ b/activitysim/abm/models/__init__.py @@ -16,8 +16,7 @@ from . import non_mandatory_tour_frequency from . import non_mandatory_destination from . import non_mandatory_scheduling -from . import school_location -from . import workplace_location +from . import location_choice from . import tour_mode_choice from . import cdap diff --git a/activitysim/abm/models/initialize.py b/activitysim/abm/models/initialize.py index be17306e6..92bfa784f 100644 --- a/activitysim/abm/models/initialize.py +++ b/activitysim/abm/models/initialize.py @@ -53,7 +53,7 @@ def annotate_tables(model_settings, trace_label): expressions.assign_columns( df=df, model_settings=annotate, - trace_label=tracing.extend_trace_label(trace_label, 'annotate_%s' % tablename)) + trace_label=trace_label) # fixme - narrow? @@ -89,7 +89,7 @@ def initialize_households(): # - initialize shadow_pricing predicted_size after annotating household and person tables # since these are scaled to model size, they have to be created while single-process - shadow_pricing.add_predicted_size_table() + shadow_pricing.add_predicted_size_tables() # - preload person_windows t0 = tracing.print_elapsed_time() diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py new file mode 100644 index 000000000..601670e12 --- /dev/null +++ b/activitysim/abm/models/location_choice.py @@ -0,0 +1,423 @@ +# ActivitySim +# See full license in LICENSE.txt. + +from __future__ import (absolute_import, division, print_function, ) +from future.standard_library import install_aliases +install_aliases() # noqa: E402 + +from future.utils import iteritems + +from collections import OrderedDict + +import logging + +import pandas as pd + +from activitysim.core import tracing +from activitysim.core import config +from activitysim.core import pipeline +from activitysim.core import simulate +from activitysim.core import inject +from activitysim.core.mem import force_garbage_collect + +from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.interaction_sample import interaction_sample + +from .util import expressions +from .util import logsums as logsum + +from activitysim.abm.tables import shadow_pricing + +""" +The school/workplace location model predicts the zones in which various people will +work or attend school. +""" + +logger = logging.getLogger(__name__) + + +def spec_for_segment(model_spec, segment_name): + + spec = model_spec[[segment_name]] + + # drop spec rows with zero coefficients since they won't have any effect (0 marginal utility) + zero_rows = (spec == 0).all(axis=1) + if zero_rows.any(): + logger.debug("dropping %s all-zero rows from spec" % (zero_rows.sum(),)) + spec = spec.loc[~zero_rows] + + return spec + + +# we want to iterate over segment_ids in the same order every time +def order_dict_by_keys(segment_ids): + return OrderedDict([(k, segment_ids[k]) for k in sorted(segment_ids.keys())]) + + +def run_location_sample( + segment_name, + persons_merged, + skim_dict, + dest_size_terms, + model_settings, + chunk_size, trace_hh_id, trace_label): + """ + build a table of persons * all zones in order to select a sample of alternative locations. + + person_id, dest_TAZ, rand, pick_count + 23750, 14, 0.565502716034, 4 + 23750, 16, 0.711135838871, 6 + ... + 23751, 12, 0.408038878552, 1 + 23751, 14, 0.972732479292, 2 + """ + assert not persons_merged.empty + + model_spec = simulate.read_model_spec(file_name=model_settings['SAMPLE_SPEC']) + + # FIXME - MEMORY HACK - only include columns actually used in spec + chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] + choosers = persons_merged[chooser_columns] + + alternatives = dest_size_terms + + sample_size = model_settings["SAMPLE_SIZE"] + alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] + + logger.info("Running %s with %d persons" % (trace_label, len(choosers.index))) + + # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers + # and a TAZ in the alternatives which get merged during interaction + # the skims will be available under the name "skims" for any @ expressions + skims = skim_dict.wrap("TAZ", "TAZ_r") + + locals_d = { + 'skims': skims, + 'segment_size': segment_name + } + constants = config.get_model_constants(model_settings) + if constants is not None: + locals_d.update(constants) + + choices = interaction_sample( + choosers, + alternatives, + sample_size=sample_size, + alt_col_name=alt_dest_col_name, + spec=spec_for_segment(model_spec, segment_name), + skims=skims, + locals_d=locals_d, + chunk_size=chunk_size, + trace_label=trace_label) + + return choices + + +def run_location_logsums( + segment_name, + persons_merged_df, + skim_dict, skim_stack, + location_sample_df, + model_settings, + chunk_size, trace_hh_id, trace_label): + """ + add logsum column to existing location_sample table + + logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair + in location_sample, and computing the logsum of all the utilities + + +-----------+--------------+----------------+------------+----------------+ + | PERID | dest_TAZ | rand | pick_count | logsum (added) | + +===========+==============+================+============+================+ + | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | + +-----------+--------------+----------------+------------+----------------+ + + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | + +-----------+--------------+----------------+------------+----------------+ + + ... | | | | | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | + +-----------+--------------+----------------+------------+----------------+ + | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | + +-----------+--------------+----------------+------------+----------------+ + """ + + assert not location_sample_df.empty + + logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) + + # FIXME - MEMORY HACK - only include columns actually used in spec + persons_merged_df = \ + logsum.filter_chooser_columns(persons_merged_df, logsum_settings, model_settings) + + logger.info("Running %s with %s rows" % (trace_label, len(location_sample_df.index))) + + choosers = pd.merge(location_sample_df, + persons_merged_df, + left_index=True, + right_index=True, + how="left") + + tour_purpose = model_settings['LOGSUM_TOUR_PURPOSE'] + if isinstance(tour_purpose, dict): + tour_purpose = tour_purpose[segment_name] + + logsums = logsum.compute_logsums( + choosers, + tour_purpose, + logsum_settings, model_settings, + skim_dict, skim_stack, + chunk_size, trace_hh_id, + trace_label) + + # "add_column series should have an index matching the table to which it is being added" + # when the index has duplicates, however, in the special case that the series index exactly + # matches the table index, then the series value order is preserved + # logsums now does, since workplace_location_sample was on left side of merge de-dup merge + location_sample_df['mode_choice_logsum'] = logsums + + return location_sample_df + + +def run_location_simulate( + segment_name, + persons_merged, + location_sample_df, + skim_dict, + dest_size_terms, + model_settings, + chunk_size, trace_hh_id, trace_label): + """ + run location model on location_sample annotated with mode_choice logsum + to select a dest zone from sample alternatives + """ + assert not persons_merged.empty + + model_spec = simulate.read_model_spec(file_name=model_settings['SPEC']) + + # FIXME - MEMORY HACK - only include columns actually used in spec + chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] + choosers = persons_merged[chooser_columns] + + alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] + + # alternatives are pre-sampled and annotated with logsums and pick_count + # but we have to merge additional alt columns into alt sample list + + alternatives = \ + pd.merge(location_sample_df, dest_size_terms, + left_on=alt_dest_col_name, right_index=True, how="left") + + logger.info("Running location_simulate with %d persons" % len(choosers)) + + # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers + # and a TAZ in the alternatives which get merged during interaction + # the skims will be available under the name "skims" for any @ expressions + skims = skim_dict.wrap("TAZ", alt_dest_col_name) + + locals_d = { + 'skims': skims, + 'segment_size': segment_name + } + constants = config.get_model_constants(model_settings) + if constants is not None: + locals_d.update(constants) + + choices = interaction_sample_simulate( + choosers, + alternatives, + spec=spec_for_segment(model_spec, segment_name), + choice_column=alt_dest_col_name, + skims=skims, + locals_d=locals_d, + chunk_size=chunk_size, + trace_label=trace_label, + trace_choice_name=model_settings['DEST_CHOICE_COLUMN_NAME']) + + return choices + + +def run_location_choice( + persons_merged_df, + skim_dict, skim_stack, + dest_size_terms, + model_settings, + chunk_size, trace_hh_id, trace_label + ): + + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN_NAME'] + segment_ids = model_settings['SEGMENT_IDS'] + + # we want to iterate over segment_ids in the same order for replicability + segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) + + choices_list = [] + for segment_name, segment_id in iteritems(segment_ids): + + choosers = persons_merged_df[persons_merged_df[chooser_segment_column] == segment_id] + + if choosers.shape[0] == 0: + logger.info("%s skipping segment %s: no choosers", trace_label, segment_name) + continue + + # - location_sample + location_sample_df = \ + run_location_sample( + segment_name, + choosers, + skim_dict, + dest_size_terms, + model_settings, + chunk_size, + trace_hh_id, + tracing.extend_trace_label(trace_label, 'sample.%s' % segment_name)) + + # - location_logsums + location_sample_df = \ + run_location_logsums( + segment_name, + choosers, + skim_dict, skim_stack, + location_sample_df, + model_settings, + chunk_size, + trace_hh_id, + tracing.extend_trace_label(trace_label, 'logsums.%s' % segment_name)) + + # - location_simulate + choices = \ + run_location_simulate( + segment_name, + choosers, + location_sample_df, + skim_dict, + dest_size_terms, + model_settings, + chunk_size, + trace_hh_id, + tracing.extend_trace_label(trace_label, 'simulate.%s' % segment_name)) + + choices_list.append(choices) + + # FIXME - want to do this here? + del location_sample_df + force_garbage_collect() + + return pd.concat(choices_list) if len(choices_list) > 0 else pd.Series() + + +def iterate_location_choice( + model_settings, + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id, locutor, + trace_label): + + # column containing segment id + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN_NAME'] + + # boolean to filter out persons not needing location modeling (e.g. is_worker, is_student) + chooser_filter_column = model_settings['CHOOSER_FILTER_COLUMN_NAME'] + + persons_merged_df = persons_merged.to_frame() + + persons_merged_df = persons_merged_df[persons_merged[chooser_filter_column]] + + spc = shadow_pricing.load_shadow_price_calculator(model_settings) + max_iterations = spc.max_iterations + + logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) + + choices = None + for iteration in range(max_iterations): + + if iteration > 0: + spc.update_shadow_prices() + + choices = run_location_choice( + persons_merged_df, + skim_dict, skim_stack, + spc.shadow_price_adjusted_predicted_size(), + model_settings, + chunk_size, trace_hh_id, + trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) + + choices_df = choices.to_frame('dest_choice') + choices_df['segment_id'] = \ + persons_merged_df[chooser_segment_column].reindex(choices_df.index) + + spc.set_choices(choices_df) + + if locutor: + spc.write_trace_files(iteration) + + if spc.use_shadow_pricing and spc.check_fit(iteration): + logging.info("%s converged after iteration %s" % (trace_label, iteration,)) + break + + # - shadow price table + if locutor: + if 'SHADOW_PRICE_TABLE' in model_settings: + inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) + if 'MODELED_SIZE_TABLE' in model_settings: + inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) + + dest_choice_column_name = model_settings['DEST_CHOICE_COLUMN_NAME'] + tracing.print_summary(dest_choice_column_name, choices, value_counts=True) + + persons_df = persons.to_frame() + + # We only chose school locations for the subset of persons who go to school + # so we backfill the empty choices with -1 to code as no school location + NO_DEST_TAZ = -1 + persons_df[dest_choice_column_name] = \ + choices.reindex(persons_df.index).fillna(NO_DEST_TAZ).astype(int) + + # - annotate persons + expressions.assign_columns( + df=persons_df, + model_settings=model_settings.get('annotate_persons'), + trace_label=tracing.extend_trace_label(trace_label, 'annotate_persons')) + + pipeline.replace_table("persons", persons_df) + + if trace_hh_id: + tracing.trace_df(persons_df, + label=trace_label, + warn_if_empty=True) + + return persons_df + + +@inject.step() +def workplace_location( + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id, locutor): + + trace_label = 'workplace_location' + model_settings = config.read_model_settings('workplace_location.yaml') + + iterate_location_choice( + model_settings, + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id, locutor, trace_label + ) + + +@inject.step() +def school_location( + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id, locutor + ): + + trace_label = 'school_location' + model_settings = config.read_model_settings('school_location.yaml') + + iterate_location_choice( + model_settings, + persons_merged, persons, + skim_dict, skim_stack, + chunk_size, trace_hh_id, locutor, trace_label + ) diff --git a/activitysim/abm/models/school_location.py b/activitysim/abm/models/school_location.py deleted file mode 100644 index d2a66743e..000000000 --- a/activitysim/abm/models/school_location.py +++ /dev/null @@ -1,452 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -from __future__ import (absolute_import, division, print_function, ) -from future.standard_library import install_aliases -install_aliases() # noqa: E402 - -from future.utils import iteritems - -import logging -from collections import OrderedDict - -import numpy as np -import pandas as pd - -from activitysim.core import tracing -from activitysim.core import config -from activitysim.core import pipeline -from activitysim.core import simulate -from activitysim.core import inject -from activitysim.core.mem import force_garbage_collect - -from activitysim.core.interaction_sample_simulate import interaction_sample_simulate -from activitysim.core.interaction_sample import interaction_sample - -from activitysim.core.util import reindex - -from activitysim.abm.tables import shadow_pricing - -from .util import logsums as logsum - - -from .util import expressions - -""" -The school location model predicts the zones in which various people will -go to school. -""" - -logger = logging.getLogger(__name__) - - -NO_SCHOOL_TAZ = -1 - - -# we want to iterate over segment_ids in the same order every time -def order_dict_by_keys(segment_ids): - return OrderedDict([(k, segment_ids[k]) for k in sorted(segment_ids.keys())]) - - -def run_school_location_sample( - persons_merged, - skim_dict, - dest_size_terms, - model_settings, - chunk_size, - trace_hh_id, - trace_label): - - """ - build a table of persons * all zones to select a sample of alternative school locations. - - | PERID | dest_TAZ | rand | pick_count | - +===========+==============+================+============+ - | 23750 | 14 | 0.565502716034 | 4 | - +-----------+--------------+----------------+------------+ - + 23750 | 16 | 0.711135838871 | 6 | - +-----------+--------------+----------------+------------+ - + ... | | | | - +-----------+--------------+----------------+------------+ - | 23751 | 12 | 0.408038878552 | 1 | - +-----------+--------------+----------------+------------+ - | 23751 | 14 | 0.972732479292 | 2 | - +-----------+--------------+----------------+------------+ - """ - - model_spec = simulate.read_model_spec(file_name=model_settings['SAMPLE_SPEC']) - - # FIXME - MEMORY HACK - only include columns actually used in spec - chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = persons_merged[chooser_columns] - - chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] - - sample_size = model_settings["SAMPLE_SIZE"] - alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - - logger.info("Running school_location_simulate with %d persons", len(choosers)) - - # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers - # and a TAZ in the alternatives which get merged during interaction - # the skims will be available under the name "skims" for any @ expressions - skims = skim_dict.wrap("TAZ", "TAZ_r") - - locals_d = { - 'skims': skims - } - constants_dict = config.get_model_constants(model_settings) - if constants_dict is not None: - locals_d.update(constants_dict) - - # we want to iterate over segment_ids in the same order in sample, logsums, and simulate - segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) - - choices_list = [] - for segment_name, segment_id in iteritems(segment_ids): - - locals_d['segment'] = segment_name - - choosers_segment = choosers[choosers[chooser_segment_column] == segment_id] - - if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers", trace_label, segment_name) - continue - - # alts indexed by taz with one column containing size_term for this tour_type - alternatives_segment = dest_size_terms[[segment_name]] - - # no point in considering impossible alternatives (where dest size term is zero) - alternatives_segment = alternatives_segment[alternatives_segment[segment_name] > 0] - - logger.info("%s segment %s: %s persons %s alternatives" % - (trace_label, segment_name, len(choosers_segment), len(alternatives_segment))) - - choices = interaction_sample( - choosers_segment, - alternatives_segment, - sample_size=sample_size, - alt_col_name=alt_dest_col_name, - spec=model_spec[[segment_name]], - skims=skims, - locals_d=locals_d, - chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(trace_label, segment_name)) - - choices['segment_id'] = segment_id - choices_list.append(choices) - - if len(choices_list) > 0: - choices = pd.concat(choices_list) - # - NARROW - choices['segment_id'] = choices['segment_id'].astype(np.uint8) - else: - logger.info("Skipping %s: add_null_results" % trace_label) - choices = pd.DataFrame() - - return choices - - -def run_school_location_logsums( - persons_merged, - skim_dict, skim_stack, - location_sample_df, - model_settings, - chunk_size, - trace_hh_id, - trace_label): - - """ - compute and add mode_choice_logsum column to location_sample_df - - logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair - in location_sample_df, and computing the logsum of all the utilities - - +-----------+--------------+----------------+------------+----------------+ - | person_id | dest_TAZ | rand | pick_count | logsum (added) | - +===========+==============+================+============+================+ - | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | - +-----------+--------------+----------------+------------+----------------+ - + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | - +-----------+--------------+----------------+------------+----------------+ - + ... | | | | | - +-----------+--------------+----------------+------------+----------------+ - | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | - +-----------+--------------+----------------+------------+----------------+ - | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | - +-----------+--------------+----------------+------------+----------------+ - """ - - logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - - if location_sample_df.empty: - tracing.no_results(trace_label) - return location_sample_df - - logger.info("Running school_location_logsums with %s rows" % location_sample_df.shape[0]) - - # - only include columns actually used in spec - persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) - - # we want to iterate over segment_ids in the same order in sample, logsums, and simulate - segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) - - logsums_list = [] - for segment_name, segment_id in iteritems(segment_ids): - - # FIXME - pathological knowledge of relation between segment name and tour_purpose - tour_purpose = 'univ' if segment_name == 'university' else 'school' - - choosers = location_sample_df[location_sample_df['segment_id'] == segment_id] - - if choosers.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (trace_label, segment_name)) - continue - - choosers = pd.merge( - choosers, - persons_merged, - left_index=True, - right_index=True, - how="left") - - logsums = logsum.compute_logsums( - choosers, - tour_purpose, - logsum_settings, model_settings, - skim_dict, skim_stack, - chunk_size, trace_hh_id, - tracing.extend_trace_label(trace_label, segment_name)) - - logsums_list.append(logsums) - - logsums = pd.concat(logsums_list) - - # add_column series should have an index matching the table to which it is being added - # logsums does, since location_sample_df was on left side of merge creating choosers - - # "add_column series should have an index matching the table to which it is being added" - # when the index has duplicates, however, in the special case that the series index exactly - # matches the table index, then the series value order is preserved. - # logsums does align with location_sample_df as we loop through it in exactly the same - # order as we did when we created it - location_sample_df['mode_choice_logsum'] = logsums - - return location_sample_df - - -def run_school_location_simulate( - persons_merged, - location_sample_df, - skim_dict, - dest_size_terms, - model_settings, - chunk_size, - trace_hh_id, - trace_label): - """ - School location model on school_location_sample annotated with mode_choice logsum - to select a school_taz from sample alternatives - """ - - model_spec = simulate.read_model_spec(file_name=model_settings['SPEC']) - - if location_sample_df.empty: - - logger.info("%s no school-goers" % trace_label) - choices = pd.Series() - - else: - - alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - - # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers - # and a TAZ in the alternatives which get merged during interaction - # the skims will be available under the name "skims" for any @ expressions - skims = skim_dict.wrap("TAZ", alt_dest_col_name) - - locals_d = { - 'skims': skims, - } - constants_dict = config.get_model_constants(model_settings) - if constants_dict is not None: - locals_d.update(constants_dict) - - # FIXME - MEMORY HACK - only include columns actually used in spec - chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = persons_merged[chooser_columns] - - # we want to iterate over segment_ids in the same order in sample, logsums, and simulate - segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) - - choices_list = [] - for segment_name, segment_id in iteritems(segment_ids): - - locals_d['segment'] = segment_name - - choosers_segment = choosers[choosers['school_segment'] == segment_id] - - if choosers_segment.shape[0] == 0: - logger.info("%s skipping school_type %s: no choosers" % (trace_label, segment_name)) - continue - - alts_segment = \ - location_sample_df[location_sample_df['segment_id'] == segment_id] - - # alternatives are pre-sampled and annotated with logsums and pick_count - # but we have to merge dest_choice_size column into alt sample list - alts_segment[segment_name] = \ - reindex(dest_size_terms[segment_name], alts_segment[alt_dest_col_name]) - - choices = interaction_sample_simulate( - choosers_segment, - alts_segment, - spec=model_spec[[segment_name]], - choice_column=alt_dest_col_name, - skims=skims, - locals_d=locals_d, - chunk_size=chunk_size, - trace_label=tracing.extend_trace_label(trace_label, segment_name), - trace_choice_name=trace_label) - - choices_list.append(choices) - - choices = pd.concat(choices_list) - - return choices - - -def run_school_location( - persons_merged_df, - skim_dict, skim_stack, - dest_size_terms, - model_settings, - chunk_size, trace_hh_id, trace_label - ): - - # - school_location_sample - location_sample_df = \ - run_school_location_sample( - persons_merged_df, - skim_dict, - dest_size_terms, - model_settings, - chunk_size, - trace_hh_id, - tracing.extend_trace_label(trace_label, 'sample')) - - # - school_location_logsums - location_sample_df = \ - run_school_location_logsums( - persons_merged_df, - skim_dict, skim_stack, - location_sample_df, - model_settings, - chunk_size, - trace_hh_id, - tracing.extend_trace_label(trace_label, 'logsums')) - - # - school_location_simulate - choices = \ - run_school_location_simulate( - persons_merged_df, - location_sample_df, - skim_dict, - dest_size_terms, - model_settings, - chunk_size, - trace_hh_id, - tracing.extend_trace_label(trace_label, 'simulate')) - - return choices - - -@inject.step() -def school_location( - persons_merged, persons, - skim_dict, skim_stack, - chunk_size, - trace_hh_id, - locutor - ): - - trace_label = 'school_location' - model_settings = config.read_model_settings('school_location.yaml') - - chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] - - persons_merged_df = persons_merged.to_frame() - - spc = shadow_pricing.load_shadow_price_calculator(model_settings) - max_iterations = spc.max_iterations - - logging.info("%s max_iterations: %s" % (trace_label, max_iterations)) - - choices = None - for iteration in range(max_iterations): - - if iteration > 0: - spc.update_shadow_prices() - - choices = run_school_location( - persons_merged_df, - skim_dict, skim_stack, - spc.shadow_price_adjusted_predicted_size(), - model_settings, - chunk_size, trace_hh_id, - trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) - - force_garbage_collect() - - choices_df = choices.to_frame('dest_choice') - choices_df['segment_id'] = \ - persons_merged_df[chooser_segment_column].reindex(choices_df.index) - - spc.set_choices(choices_df) - - fit = spc.check_fit(iteration) - - if locutor: - spc.write_trace_files(iteration) - - if fit: - break - - if fit: - logging.info("%s converged after iteration %s" % (trace_label, iteration,)) - else: - logging.info("%s did not converge after iteration %s" % (trace_label, iteration,)) - - # - shadow price table - if locutor: - if 'SHADOW_PRICE_TABLE' in model_settings: - inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) - if 'MODELED_SIZE_TABLE' in model_settings: - inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) - - # - convergence stats - logging.info("\nshadow_pricing max_abs_diff\n%s" % spc.max_abs_diff) - logging.info("\nshadow_pricing max_rel_diff\n%s" % spc.max_rel_diff) - logging.info("\nshadow_pricing num_fail\n%s" % spc.num_fail) - - persons_df = persons.to_frame() - - # We only chose school locations for the subset of persons who go to school - # so we backfill the empty choices with -1 to code as no school location - persons_df['school_taz'] = choices.reindex(persons_df.index).fillna(NO_SCHOOL_TAZ).astype(int) - # tracing.print_summary('school_taz', choices, value_counts=True) - - # - annotate persons - model_name = 'school_location' - model_settings = config.read_model_settings('school_location.yaml') - expressions.assign_columns( - df=persons_df, - model_settings=model_settings.get('annotate_persons'), - trace_label=tracing.extend_trace_label(model_name, 'annotate_persons')) - - pipeline.replace_table("persons", persons_df) - - if trace_hh_id: - tracing.trace_df(persons_df, - label="school_location", - warn_if_empty=True) diff --git a/activitysim/abm/models/util/logsums.py b/activitysim/abm/models/util/logsums.py index eb1d59025..3c52c0216 100644 --- a/activitysim/abm/models/util/logsums.py +++ b/activitysim/abm/models/util/logsums.py @@ -39,7 +39,7 @@ def filter_chooser_columns(choosers, logsum_settings, model_settings): missing_columns = [c for c in chooser_columns if c not in choosers] if missing_columns: - logger.info("filter_chooser_columns missing_columns %s" % missing_columns) + logger.debug("logsum.filter_chooser_columns missing_columns %s" % missing_columns) # ignore any columns not appearing in choosers df chooser_columns = [c for c in chooser_columns if c in choosers] @@ -92,7 +92,7 @@ def compute_logsums(choosers, nest_spec = config.get_logit_model_settings(logsum_settings) constants = config.get_model_constants(logsum_settings) - logger.info("Running compute_logsums with %d choosers" % choosers.shape[0]) + logger.debug("Running compute_logsums with %d choosers" % choosers.shape[0]) # setup skim keys odt_skim_stack_wrapper = skim_stack.wrap(left_key=orig_col_name, right_key=dest_col_name, diff --git a/activitysim/abm/models/workplace_location.py b/activitysim/abm/models/workplace_location.py deleted file mode 100644 index c1b1c8f86..000000000 --- a/activitysim/abm/models/workplace_location.py +++ /dev/null @@ -1,363 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -from __future__ import (absolute_import, division, print_function, ) -from future.standard_library import install_aliases -install_aliases() # noqa: E402 - -import logging - -import pandas as pd - -from activitysim.core import tracing -from activitysim.core import config -from activitysim.core import pipeline -from activitysim.core import simulate -from activitysim.core import inject -from activitysim.core.mem import force_garbage_collect - -from activitysim.core.interaction_sample_simulate import interaction_sample_simulate -from activitysim.core.interaction_sample import interaction_sample - -from .util import expressions -from .util import logsums as logsum - -from activitysim.abm.tables import shadow_pricing - -""" -The workplace location model predicts the zones in which various people will -work. - -for now we generate a workplace location for everyone - -presumably it will not get used in downstream models for everyone - -it should depend on CDAP and mandatory tour generation as to whether -it gets used -""" - -logger = logging.getLogger(__name__) - - -def run_workplace_location_sample( - persons_merged, - skim_dict, - dest_size_terms, - chunk_size, trace_hh_id): - """ - build a table of workers * all zones in order to select a sample of alternative work locations. - - person_id, dest_TAZ, rand, pick_count - 23750, 14, 0.565502716034, 4 - 23750, 16, 0.711135838871, 6 - ... - 23751, 12, 0.408038878552, 1 - 23751, 14, 0.972732479292, 2 - """ - - trace_label = 'workplace_location_sample' - model_settings = config.read_model_settings('workplace_location.yaml') - model_spec = simulate.read_model_spec(file_name='workplace_location_sample.csv') - - choosers = persons_merged - - if choosers.empty: - logger.info("Skipping %s: no workers" % trace_label) - choices = pd.DataFrame() - return choices - - # FIXME - MEMORY HACK - only include columns actually used in spec - chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = choosers[chooser_columns] - - alternatives = dest_size_terms - - sample_size = model_settings["SAMPLE_SIZE"] - alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - - logger.info("Running workplace_location_sample with %d persons" % len(choosers)) - - # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers - # and a TAZ in the alternatives which get merged during interaction - # the skims will be available under the name "skims" for any @ expressions - skims = skim_dict.wrap("TAZ", "TAZ_r") - - locals_d = { - 'skims': skims - } - constants = config.get_model_constants(model_settings) - if constants is not None: - locals_d.update(constants) - - choices = interaction_sample( - choosers, - alternatives, - sample_size=sample_size, - alt_col_name=alt_dest_col_name, - spec=model_spec, - skims=skims, - locals_d=locals_d, - chunk_size=chunk_size, - trace_label=trace_label) - - return choices - - -def run_workplace_location_logsums( - persons_merged_df, - skim_dict, skim_stack, - location_sample_df, - chunk_size, trace_hh_id): - """ - add logsum column to existing workplace_location_sample able - - logsum is calculated by running the mode_choice model for each sample (person, dest_taz) pair - in workplace_location_sample, and computing the logsum of all the utilities - - +-----------+--------------+----------------+------------+----------------+ - | PERID | dest_TAZ | rand | pick_count | logsum (added) | - +===========+==============+================+============+================+ - | 23750 | 14 | 0.565502716034 | 4 | 1.85659498857 | - +-----------+--------------+----------------+------------+----------------+ - + 23750 | 16 | 0.711135838871 | 6 | 1.92315598631 | - +-----------+--------------+----------------+------------+----------------+ - + ... | | | | | - +-----------+--------------+----------------+------------+----------------+ - | 23751 | 12 | 0.408038878552 | 1 | 2.40612135416 | - +-----------+--------------+----------------+------------+----------------+ - | 23751 | 14 | 0.972732479292 | 2 | 1.44009018355 | - +-----------+--------------+----------------+------------+----------------+ - """ - - trace_label = 'workplace_location_logsums' - - if location_sample_df.empty: - tracing.no_results(trace_label) - return location_sample_df - - model_settings = config.read_model_settings('workplace_location.yaml') - logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - - # FIXME - MEMORY HACK - only include columns actually used in spec - persons_merged_df = \ - logsum.filter_chooser_columns(persons_merged_df, logsum_settings, model_settings) - - logger.info("Running workplace_location_logsums with %s rows" % len(location_sample_df)) - - choosers = pd.merge(location_sample_df, - persons_merged_df, - left_index=True, - right_index=True, - how="left") - - tour_purpose = 'work' - logsums = logsum.compute_logsums( - choosers, - tour_purpose, - logsum_settings, model_settings, - skim_dict, skim_stack, - chunk_size, trace_hh_id, - trace_label) - - # "add_column series should have an index matching the table to which it is being added" - # when the index has duplicates, however, in the special case that the series index exactly - # matches the table index, then the series value order is preserved - # logsums now does, since workplace_location_sample was on left side of merge de-dup merge - location_sample_df['mode_choice_logsum'] = logsums - - return location_sample_df - - -def run_workplace_location_simulate( - persons_merged, - location_sample_df, - skim_dict, - dest_size_terms, - chunk_size, trace_hh_id): - """ - Workplace location model on workplace_location_sample annotated with mode_choice logsum - to select a work_taz from sample alternatives - """ - - trace_label = 'workplace_location_simulate' - model_settings = config.read_model_settings('workplace_location.yaml') - model_spec = simulate.read_model_spec(file_name='workplace_location.csv') - - if location_sample_df.empty: - logger.info("%s no workers" % trace_label) - choices = pd.Series() - else: - - choosers = persons_merged - - # FIXME - MEMORY HACK - only include columns actually used in spec - chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] - choosers = choosers[chooser_columns] - - alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] - - # alternatives are pre-sampled and annotated with logsums and pick_count - # but we have to merge additional alt columns into alt sample list - - alternatives = \ - pd.merge(location_sample_df, dest_size_terms, - left_on=alt_dest_col_name, right_index=True, how="left") - - logger.info("Running workplace_location_simulate with %d persons" % len(choosers)) - - # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers - # and a TAZ in the alternatives which get merged during interaction - # the skims will be available under the name "skims" for any @ expressions - skims = skim_dict.wrap("TAZ", alt_dest_col_name) - - locals_d = { - 'skims': skims, - } - constants = config.get_model_constants(model_settings) - if constants is not None: - locals_d.update(constants) - - choices = interaction_sample_simulate( - choosers, - alternatives, - spec=model_spec, - choice_column=alt_dest_col_name, - skims=skims, - locals_d=locals_d, - chunk_size=chunk_size, - trace_label=trace_label, - trace_choice_name='workplace_location') - - return choices - - -def run_workplace_location( - persons_merged_df, - skim_dict, skim_stack, - dest_size_terms, - model_settings, - chunk_size, trace_hh_id, trace_label - ): - - # - workplace_location_sample - location_sample_df = \ - run_workplace_location_sample( - persons_merged_df, - skim_dict, - dest_size_terms, - chunk_size, - trace_hh_id) - - # - workplace_location_logsums - location_sample_df = \ - run_workplace_location_logsums( - persons_merged_df, - skim_dict, skim_stack, - location_sample_df, - chunk_size, - trace_hh_id) - - # - school_location_simulate - choices = \ - run_workplace_location_simulate( - persons_merged_df, - location_sample_df, - skim_dict, - dest_size_terms, - chunk_size, - trace_hh_id) - - return choices - - -@inject.step() -def workplace_location( - persons_merged, persons, - skim_dict, skim_stack, - chunk_size, trace_hh_id, locutor): - - trace_label = 'workplace_location' - model_settings = config.read_model_settings('workplace_location.yaml') - - chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] - - persons_merged_df = persons_merged.to_frame() - - # presumably is_worker or something similar - persons_merged_df = persons_merged_df[persons_merged[model_settings['CHOOSER_FILTER_COLUMN']]] - - spc = shadow_pricing.load_shadow_price_calculator(model_settings) - max_iterations = spc.max_iterations - - logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) - - choices = None - for iteration in range(max_iterations): - - if iteration > 0: - spc.update_shadow_prices() - - choices = run_workplace_location( - persons_merged_df, - skim_dict, skim_stack, - spc.shadow_price_adjusted_predicted_size(), - model_settings, - chunk_size, trace_hh_id, - trace_label=tracing.extend_trace_label(trace_label, 'i%s' % iteration)) - - force_garbage_collect() - - choices_df = choices.to_frame('dest_choice') - choices_df['segment_id'] = \ - persons_merged_df[chooser_segment_column].reindex(choices_df.index) - - spc.set_choices(choices_df) - - fit = spc.check_fit(iteration) - - if locutor: - spc.write_trace_files(iteration) - - if fit: - break - - if fit: - logging.info("%s converged after iteration %s" % (trace_label, iteration,)) - else: - logging.info("%s did not converge after iteration %s" % (trace_label, iteration,)) - - # - convergence stats - logging.info("\nshadow_pricing max_abs_diff\n%s" % spc.max_abs_diff) - logging.info("\nshadow_pricing max_rel_diff\n%s" % spc.max_rel_diff) - logging.info("\nshadow_pricing num_fail\n%s" % spc.num_fail) - - # - shadow price table - if locutor: - if 'SHADOW_PRICE_TABLE' in model_settings: - inject.add_table(model_settings['SHADOW_PRICE_TABLE'], spc.shadow_prices) - if 'MODELED_SIZE_TABLE' in model_settings: - inject.add_table(model_settings['MODELED_SIZE_TABLE'], spc.modeled_size) - - tracing.print_summary('workplace_taz', choices, describe=True) - - persons_df = persons.to_frame() - - # We only chose school locations for the subset of persons who go to school - # so we backfill the empty choices with -1 to code as no school location - NO_WORKPLACE_TAZ = -1 - persons_df['workplace_taz'] = \ - choices.reindex(persons_df.index).fillna(NO_WORKPLACE_TAZ).astype(int) - - # - annotate persons - model_name = 'workplace_location' - model_settings = config.read_model_settings('workplace_location.yaml') - - expressions.assign_columns( - df=persons_df, - model_settings=model_settings.get('annotate_persons'), - trace_label=tracing.extend_trace_label(model_name, 'annotate_persons')) - - pipeline.replace_table("persons", persons_df) - - if trace_hh_id: - tracing.trace_df(persons_df, - label="workplace_location", - warn_if_empty=True) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 08a6981dd..e134a8900 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -37,7 +37,7 @@ def size_table_name(selector, scaled=True): if scaled: - table_name = "scaled_%s_destination_size" % selector + table_name = "%s_destination_size" % selector else: table_name = "raw_%s_destination_size" % selector return table_name @@ -47,15 +47,11 @@ def get_size_table(selector, scaled=True): return inject.get_table(size_table_name(selector, scaled)).to_frame() -USE_RAW_SIZE = True - - class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) - self.use_saved_shadow_prices = bool(config.setting('use_saved_shadow_prices')) self.selector = model_settings['SELECTOR'] @@ -68,17 +64,10 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): # - modeled_size (set by call to set_choices/synchronize_choices) self.modeled_size = None - # - convergence criteria for check_fit - # ignore criteria for zones smaller than size_threshold - self.size_threshold = model_settings['SIZE_THRESHOLD'] - # zone passes if modeled is within percent_tolerance of predicted_size - self.percent_tolerance = model_settings['PERCENT_TOLERANCE'] - # max percentage of zones allowed to fail - self.fail_threshold = model_settings['FAIL_THRESHOLD'] + if self.use_shadow_pricing: + self.shadow_settings = config.read_model_settings('shadow_pricing.yaml') # - destination_size_table (predicted_size) - if USE_RAW_SIZE: - self.raw_predicted_size = get_size_table(self.selector, scaled=False) self.predicted_size = get_size_table(self.selector, scaled=True) # - shared_data @@ -92,13 +81,14 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): # - load saved shadow_prices (if available) and set max_iterations accordingly self.shadow_prices = None if self.use_shadow_pricing: - if self.load_saved_shadow_prices: - self.shadow_prices = self.load_saved_shadow_prices(model_settings) + if self.shadow_settings['LOAD_SAVED_SHADOW_PRICES']: + # read_saved_shadow_prices logs error and returns None if file not found + self.shadow_prices = self.read_saved_shadow_prices(model_settings) if self.shadow_prices is None: - self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS', 5) + self.max_iterations = self.shadow_settings.get('MAX_ITERATIONS', 5) else: - self.max_iterations = model_settings.get('MAX_SHADOW_PRICE_ITERATIONS_SAVED', 1) + self.max_iterations = self.shadow_settings.get('MAX_ITERATIONS_SAVED', 1) else: self.max_iterations = 1 @@ -114,7 +104,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.max_abs_diff = pd.DataFrame(index=self.predicted_size.columns) self.max_rel_diff = pd.DataFrame(index=self.predicted_size.columns) - def load_saved_shadow_prices(self, model_settings): + def read_saved_shadow_prices(self, model_settings): shadow_prices = None @@ -125,7 +115,7 @@ def load_saved_shadow_prices(self, model_settings): file_path = config.data_file_path(saved_shadow_price_file_name, mandatory=False) if file_path: shadow_prices = pd.read_csv(file_path, index_col=0) - logging.info("loading saved_shadow_prices from %s" % (file_path)) + logging.info("reading saved_shadow_prices from %s" % (file_path)) else: logging.warning("Could not find saved_shadow_prices file %s" % (file_path)) @@ -209,9 +199,21 @@ def check_fit(self, iteration): """ + if not self.use_shadow_pricing: + return False + assert self.modeled_size is not None assert self.predicted_size is not None + # - convergence criteria for check_fit + # - convergence criteria for check_fit + # ignore convergence criteria for zones smaller than size_threshold + size_threshold = self.shadow_settings['SIZE_THRESHOLD'] + # zone passes if modeled is within percent_tolerance of predicted_size + percent_tolerance = self.shadow_settings['PERCENT_TOLERANCE'] + # max percentage of zones allowed to fail + fail_threshold = self.shadow_settings['FAIL_THRESHOLD'] + modeled_size = self.modeled_size predicted_size = self.predicted_size @@ -220,10 +222,10 @@ def check_fit(self, iteration): rel_diff = abs_diff / modeled_size # ignore zones where predicted_size < threshold - rel_diff.where(predicted_size >= self.size_threshold, 0, inplace=True) + rel_diff.where(predicted_size >= size_threshold, 0, inplace=True) # ignore zones where rel_diff < percent_tolerance - rel_diff.where(rel_diff > (self.percent_tolerance / 100.0), 0, inplace=True) + rel_diff.where(rel_diff > (percent_tolerance / 100.0), 0, inplace=True) self.num_fail['iter%s' % iteration] = (rel_diff > 0).sum() self.max_abs_diff['iter%s' % iteration] = abs_diff.max() @@ -231,7 +233,8 @@ def check_fit(self, iteration): total_fails = (rel_diff > 0).values.sum() - max_fail = (self.fail_threshold / 100.0) * np.prod(predicted_size.shape) + # FIXME - should not count zones where predicted_size < threshold? (could calc in init) + max_fail = (fail_threshold / 100.0) * np.prod(predicted_size.shape) converged = (total_fails <= max_fail) @@ -245,59 +248,98 @@ def check_fit(self, iteration): logging.info("check_fit %s iteration: %s converged: %s max_fail: %s total_fails: %s" % (self.selector, iteration, converged, max_fail, total_fails)) + # - convergence stats + if converged or iteration == self.max_iterations: + logging.info("\nshadow_pricing max_abs_diff\n%s" % self.max_abs_diff) + logging.info("\nshadow_pricing max_rel_diff\n%s" % self.max_rel_diff) + logging.info("\nshadow_pricing num_fail\n%s" % self.num_fail) + return converged def update_shadow_prices(self): - """ - CTRAMP: - if ( modeledDestinationLocationsByDestZone > 0 ) - shadowPrice *= ( scaledSize / modeledDestinationLocationsByDestZone ); - // else - // shadowPrice *= scaledSize; - - Daysim: - targ = prediction > total - ? Math.Min(prediction, - Math.Min(total * (1 + percentTolerance / 100D), total + absoluteTolerance)) - : Math.Max(prediction, - Math.Max(total * (1 - percentTolerance / 100D), total - absoluteTolerance)); - - shadowPrice = - previousShadowPrice + Math.Log(Math.Max(targ, .01) * 1D / Math.Max(prediction, .01)); - """ assert self.use_shadow_pricing + shadow_price_method = self.shadow_settings['SHADOW_PRICE_METHOD'] + # can't update_shadow_prices until after first iteration # modeled_size should have been set by set_choices at end of previous iteration assert self.modeled_size is not None assert self.predicted_size is not None assert self.shadow_prices is not None - new_scale_factor = self.predicted_size / self.modeled_size + if shadow_price_method == 'ctramp': + # - CTRAMP + """ + if ( modeledDestinationLocationsByDestZone > 0 ) + shadowPrice *= ( scaledSize / modeledDestinationLocationsByDestZone ); + // else + // shadowPrice *= scaledSize; + """ + damping_factor = self.shadow_settings['DAMPING_FACTOR'] + assert 0 < damping_factor <= 1 + + new_scale_factor = self.predicted_size / self.modeled_size + damped_scale_factor = 1 + (new_scale_factor - 1) * damping_factor + new_shadow_prices = self.shadow_prices * damped_scale_factor + + # following CTRAMP (revised version - with 0 dest zone case lines commented out) + # avoid zero-divide for 0 modeled_size, by leaving shadow_prices unchanged + new_shadow_prices.where(self.modeled_size > 0, self.shadow_prices, inplace=True) + + elif shadow_price_method == 'daysim': + # - Daysim + """ + if predicted > modeled: # if modeled is too low, increase shadow price + target = min( + predicted, + modeled + modeled * percent_tolerance, + modeled + absolute_tolerance) + + if modeled > predicted # modeled is too high, decrease shadow price + target = max of: + predicted + modeled - modeled * percentTolerance + modeled - absoluteTolerance + + shadow_price = shadow_price + log(np.maximum(target, 0.01) / np.maximum(modeled, 0.01)) + """ + # FIXME should these be the same as PERCENT_TOLERANCE and FAIL_THRESHOLD above? + absolute_tolerance = self.shadow_settings['DAYSIM_ABSOLUTE_TOLERANCE'] + percent_tolerance = self.shadow_settings['DAYSIM_PERCENT_TOLERANCE'] / 100.0 + assert 0 <= percent_tolerance <= 1 + + target = np.where( + self.predicted_size > self.modeled_size, + np.minimum(self.predicted_size, + np.minimum(self.modeled_size * (1 + percent_tolerance), + self.modeled_size + absolute_tolerance)), + np.maximum(self.predicted_size, + np.maximum(self.modeled_size * (1 - percent_tolerance), + self.modeled_size - absolute_tolerance))) + + adjustment = np.log(np.maximum(target, 0.01) / np.maximum(self.modeled_size, 0.01)) + + # def like_df(data, df): + # return pd.DataFrame(data=data, columns=df.columns, index=df.index) + # print("\ntarget\n", like_df(target, self.shadow_prices).head()) + # print("\nadjustment\n", like_df(adjustment, self.shadow_prices).head()) + + new_shadow_prices = self.shadow_prices + adjustment - # FIXME - need to decide if following CTRAMP code quoted above, and if so, which version - # following CTRAMP (original version - later commented out) - # avoid zero-divide for 0 modeled_size, by setting scale_factor same as modeled_size of 1 - # new_scale_factor.where(self.modeled_size > 0, self.predicted_size) - - new_shadow_prices = self.shadow_prices * new_scale_factor - - # following CTRAMP (revised version - with 0 dest zone case lines commented out) - # avoid zero-divide for 0 modeled_size, by leaving shadow_prices unchanged - new_shadow_prices.where(self.modeled_size > 0, self.shadow_prices, inplace=True) + else: + raise RuntimeError("unknown SHADOW_PRICE_METHOD %s" % self.shadow_price_method) # print("\nself.predicted_size\n", self.predicted_size.head()) # print("\nself.modeled_size\n", self.modeled_size.head()) + # print("\nprevious shadow_prices\n", self.shadow_prices.head()) + # print("\nnew_shadow_prices\n", new_shadow_prices.head()) self.shadow_prices = new_shadow_prices def shadow_price_adjusted_predicted_size(self): - if USE_RAW_SIZE: - return self.raw_predicted_size * self.shadow_prices - else: - return self.predicted_size * self.shadow_prices + return self.predicted_size * self.shadow_prices def write_trace_files(self, iteration): logger.info("write_trace_files iteration %s" % iteration) @@ -322,8 +364,10 @@ def get_shadow_pricing_info(): land_use = inject.get_table('land_use') size_terms = inject.get_injectable('size_terms') + shadow_settings = config.read_model_settings('shadow_pricing.yaml') + # shadow_pricing_models is dict of {: } - shadow_pricing_models = config.setting('shadow_pricing_models') + shadow_pricing_models = shadow_settings['shadow_pricing_models'] blocks = OrderedDict() for selector in shadow_pricing_models: @@ -423,12 +467,15 @@ def load_shadow_price_calculator(model_settings): return spc -def add_predicted_size_table(): +def add_predicted_size_tables(): + + use_shadow_pricing = bool(config.setting('use_shadow_pricing')) - shadow_pricing_models = config.setting('shadow_pricing_models') + shadow_settings = config.read_model_settings('shadow_pricing.yaml') + shadow_pricing_models = shadow_settings['shadow_pricing_models'] if shadow_pricing_models is None: - logger.warning('add_predicted_size_table: shadow_pricing_models not in settings') + logger.warning('shadow_pricing_models list not found in shadow_pricing settings') return # shadow_pricing_models is dict of {: } @@ -442,11 +489,12 @@ def add_predicted_size_table(): segment_ids = model_settings['SEGMENT_IDS'] chooser_table_name = model_settings['CHOOSER_TABLE_NAME'] - chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN'] + chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN_NAME'] choosers_df = inject.get_table(chooser_table_name).to_frame() - if 'CHOOSER_FILTER_COLUMN' in model_settings: - choosers_df = choosers_df[choosers_df[model_settings['CHOOSER_FILTER_COLUMN']] != 0] + if 'CHOOSER_FILTER_COLUMN_NAME' in model_settings: + choosers_df = \ + choosers_df[choosers_df[model_settings['CHOOSER_FILTER_COLUMN_NAME']] != 0] # - raw_predicted_size land_use = inject.get_table('land_use') @@ -454,38 +502,41 @@ def add_predicted_size_table(): raw_size = tour_destination_size_terms(land_use, size_terms, selector).astype(np.float64) assert set(raw_size.columns) == set(segment_ids.keys()) - if USE_RAW_SIZE: - inject.add_table(size_table_name(selector, scaled=False), raw_size) + # this is just informational - for debugging shadow pricing + inject.add_table(size_table_name(selector, scaled=False), raw_size) - # - global number of choosers in each segment - segment_chooser_counts = \ - {segment_name: (choosers_df[chooser_segment_column] == segment_id).sum() - for segment_name, segment_id in iteritems(segment_ids)} + if use_shadow_pricing: - # - segment scale factor (modeled / predicted) keyed by segment_name - # scaling reconciles differences between synthetic population and zone demographics - # in a partial sample, it also scales predicted_size targets to sample population - segment_scale_factors = {} - for c in raw_size: - # number of zone demographics predicted destination choices - segment_predicted_size = raw_size[c].astype(np.float64).sum() + # - segment scale factor (modeled / predicted) keyed by segment_name + # scaling reconciles differences between synthetic population and zone demographics + # in a partial sample, it also scales predicted_size targets to sample population + segment_scale_factors = {} + for c in raw_size: + # number of zone demographics predicted destination choices + segment_predicted_size = raw_size[c].astype(np.float64).sum() - # number of synthetic population choosers in segment - segment_chooser_count = (choosers_df[chooser_segment_column] == segment_ids[c]).sum() + # number of synthetic population choosers in segment + segment_chooser_count = \ + (choosers_df[chooser_segment_column] == segment_ids[c]).sum() - segment_scale_factors[c] = \ - segment_chooser_count / np.maximum(segment_predicted_size, 1) + segment_scale_factors[c] = \ + segment_chooser_count / np.maximum(segment_predicted_size, 1) - logger.info("add_predicted_size_table %s segment %s " - "predicted %s modeled %s scale_factor %s" % - (chooser_table_name, c, - segment_predicted_size, - segment_chooser_count, - segment_scale_factors[c])) + logger.info("add_predicted_size_tables %s segment %s " + "predicted %s modeled %s scale_factor %s" % + (chooser_table_name, c, + segment_predicted_size, + segment_chooser_count, + segment_scale_factors[c])) - # segment_scale_factors[c] = \ - # segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) + # segment_scale_factors[c] = \ + # segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) + + # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + scaled_size = (raw_size * segment_scale_factors).round() + + else: + # don't scale if not shadow_pricing (breaks partial sample replicability) + scaled_size = raw_size.copy() - # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) - scaled_size = raw_size * segment_scale_factors inject.add_table(size_table_name(selector, scaled=True), scaled_size) diff --git a/activitysim/abm/test/configs/destination_choice_size_terms.csv b/activitysim/abm/test/configs/destination_choice_size_terms.csv index d38f1acd3..72ad8c472 100644 --- a/activitysim/abm/test/configs/destination_choice_size_terms.csv +++ b/activitysim/abm/test/configs/destination_choice_size_terms.csv @@ -1,8 +1,8 @@ selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE -work,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 -work,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 -work,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 -work,work_veryhigh,0,0.093,0.27,0.241,0.146,0.004,0.246,0,0,0,0 +workplace,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 +workplace,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 +workplace,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 +workplace,work_veryhigh,0,0.093,0.27,0.241,0.146,0.004,0.246,0,0,0,0 school,university,0,0,0,0,0,0,0,0,0,0.592,0.408 school,gradeschool,0,0,0,0,0,0,0,1,0,0,0 school,highschool,0,0,0,0,0,0,0,0,1,0,0 diff --git a/activitysim/abm/test/configs/school_location.csv b/activitysim/abm/test/configs/school_location.csv index c771116ca..893f79a06 100644 --- a/activitysim/abm/test/configs/school_location.csv +++ b/activitysim/abm/test/configs/school_location.csv @@ -1,11 +1,11 @@ Description,Expression,university,highschool,gradeschool -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-3.2451,-0.9523,-1.6419 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-2.7011,-0.5700,-0.5700 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.5707,-0.5700,-0.5700 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.5002,-0.1930,-0.2031 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0730,-0.1882,-0.0460 -Size variable,@df[segment].apply(np.log1p),1.0000,1.0000,1.0000 -No attractions,@df[segment]==0,-999.0000,-999.0000,-999.0000 +,_DIST@skims['DIST'],1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-3.2451,-0.9523,-1.6419 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-2.7011,-0.5700,-0.5700 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.5707,-0.5700,-0.5700 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.5002,-0.1930,-0.2031 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0730,-0.1882,-0.0460 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000 Mode choice logsum,mode_choice_logsum,0.5358,0.5358,0.5358 "Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1 - diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index 150d0b20c..2b78ac8a5 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -10,28 +10,37 @@ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +DEST_CHOICE_COLUMN_NAME: school_taz + SAMPLE_SPEC: school_location_sample.csv SPEC: school_location.csv LOGSUM_SETTINGS: tour_mode_choice.yaml LOGSUM_PREPROCESSOR: nontour_preprocessor +LOGSUM_TOUR_PURPOSE: + university: univ + highschool: school + gradeschool: school + annotate_persons: SPEC: annotate_persons_school DF: persons # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 - +# required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons # size_terms selector SELECTOR: school # chooser column with segment_id for this segment type -CHOOSER_SEGMENT_COLUMN: school_segment +CHOOSER_SEGMENT_COLUMN_NAME: school_segment + +# boolean column to filter choosers (True means keep) +CHOOSER_FILTER_COLUMN_NAME: is_student + # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: @@ -39,16 +48,9 @@ SEGMENT_IDS: highschool: 2 gradeschool: 1 +# model adds these tables (informational - not added if commented out) SHADOW_PRICE_TABLE: school_shadow_prices MODELED_SIZE_TABLE: school_modeled_size -#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 - -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 - -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 5 +# not loaded if commented out +SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv diff --git a/activitysim/abm/test/configs/school_location_sample.csv b/activitysim/abm/test/configs/school_location_sample.csv index c66526b1a..8bbf5171e 100644 --- a/activitysim/abm/test/configs/school_location_sample.csv +++ b/activitysim/abm/test/configs/school_location_sample.csv @@ -1,8 +1,9 @@ Description,Expression,university,highschool,gradeschool -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-3.2451,-0.9523,-1.6419 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-2.7011,-0.5700,-0.5700 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.5707,-0.5700,-0.5700 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.5002,-0.1930,-0.2031 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0730,-0.1882,-0.0460 -Size variable,@df[segment].apply(np.log1p),1.0000,1.0000,1.0000 -No attractions,@df[segment]==0,-999.0000,-999.0000,-999.0000 +,_DIST@skims['DIST'],1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-3.2451,-0.9523,-1.6419 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-2.7011,-0.5700,-0.5700 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.5707,-0.5700,-0.5700 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.5002,-0.1930,-0.2031 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0730,-0.1882,-0.0460 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000 diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 2269d2fb2..06c334b58 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -1,10 +1,10 @@ input_store: mtc_asim.h5 skims_file: skims.omx -# area_types less than this are considered urban -urban_threshold: 4 -cbd_threshold: 2 -rural_threshold: 6 +# - shadow pricing global switches + +# turn shadow_pricing on and off for all models (e.g. school and work) +use_shadow_pricing: True households_sample_size: 100 @@ -14,6 +14,11 @@ trace_hh_id: 961042 # trace origin, destination in accessibility calculation #trace_od: [5, 11] +# area_types less than this are considered urban +urban_threshold: 4 +cbd_threshold: 2 +rural_threshold: 6 + grade_school_max_age: 14 county_map: @@ -38,7 +43,4 @@ skim_time_periods: - MD - PM -shadow_pricing_models: - school: school_location - work: workplace_location diff --git a/activitysim/abm/test/configs/shadow_pricing.yaml b/activitysim/abm/test/configs/shadow_pricing.yaml new file mode 100644 index 000000000..337a01019 --- /dev/null +++ b/activitysim/abm/test/configs/shadow_pricing.yaml @@ -0,0 +1,32 @@ + +shadow_pricing_models: + school: school_location + workplace: workplace_location + +# global switch to enable/disable loading of saved shadow prices +# (ignored if global use_shadow_pricing switch is False) +LOAD_SAVED_SHADOW_PRICES: True + +MAX_ITERATIONS: 5 +MAX_ITERATIONS_SAVED: 1 + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 10 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 10 + +# CTRAMP or daysim +SHADOW_PRICE_METHOD: ctramp +#SHADOW_PRICE_METHOD: daysim + +# ctramp-style shadow_pricing_method parameters +DAMPING_FACTOR: 0.3 + +# daysim-style shadow_pricing_method parameters +# FIXME should these be the same as PERCENT_TOLERANCE and FAIL_THRESHOLD above? +DAYSIM_ABSOLUTE_TOLERANCE: 50 +DAYSIM_PERCENT_TOLERANCE: 10 diff --git a/activitysim/abm/test/configs/workplace_location.csv b/activitysim/abm/test/configs/workplace_location.csv index 1aec2724d..f2c9bab4b 100644 --- a/activitysim/abm/test/configs/workplace_location.csv +++ b/activitysim/abm/test/configs/workplace_location.csv @@ -1,18 +1,13 @@ -Description,Expression,Alt -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-0.8428 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-0.3104 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.3783 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.1285 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0917 -"Distance 0 to 5 mi, high and very high income",@(df.income_segment>=3)*skims['DIST'].clip(upper=5),0.15 -"Distance 5+ mi, high and very high income",@(df.income_segment>=3)*(skims['DIST']-5).clip(0),0.02 -"Size variable full-time worker, low income","@(df.income_segment==1)*df['work_low'].apply(np.log1p)",1 -"Size variable full-time worker, medium income","@(df.income_segment==2)*df['work_med'].apply(np.log1p)",1 -"Size variable full-time worker, high income","@(df.income_segment==3)*df['work_high'].apply(np.log1p)",1 -"Size variable full-time worker, very high income","@(df.income_segment==4)*df['work_veryhigh'].apply(np.log1p)",1 -"No attractions full-time worker, low income","@(df.income_segment==1)&(df['work_low']==0)",-999 -"No attractions full-time worker, medium income","@(df.income_segment==2)&(df['work_med']==0)",-999 -"No attractions full-time worker, high income","@(df.income_segment==3)&(df['work_high']==0)",-999 -"No attractions full-time worker, very high income","@(df.income_segment==4)&(df['work_veryhigh']==0)",-999 -"Mode choice logsum",mode_choice_logsum,0.3 -"Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1 +Description,Expression,work_low,work_med,work_high,work_veryhigh +,_DIST@skims['DIST'],1,1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-0.8428,-0.8428,-0.8428,-0.8428 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-0.3104,-0.3104,-0.3104,-0.3104 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.3783,-0.3783,-0.3783,-0.3783 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.1285,-0.1285,-0.1285,-0.1285 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0917,-0.0917,-0.0917,-0.0917 +"Distance 0 to 5 mi, high and very high income",@_DIST.clip(upper=5),0.0,0.0,0.15,0.15 +"Distance 5+ mi, high and very high income",@(_DIST-5).clip(0),0.0,0.0,0.02,0.02 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000,-999.0000 +"Mode choice logsum",mode_choice_logsum,0.3,0.3,0.3,0.3 +"Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1,1,1,1 diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index 57310a9b9..e099d23ff 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -4,8 +4,12 @@ SIMULATE_CHOOSER_COLUMNS: - income_segment - TAZ +SAMPLE_SPEC: workplace_location_sample.csv +SPEC: workplace_location.csv + LOGSUM_SETTINGS: tour_mode_choice.yaml LOGSUM_PREPROCESSOR: nontour_preprocessor +LOGSUM_TOUR_PURPOSE: work # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ @@ -13,6 +17,7 @@ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +DEST_CHOICE_COLUMN_NAME: workplace_taz annotate_persons: SPEC: annotate_persons_workplace @@ -22,20 +27,18 @@ annotate_persons: # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged # size_terms selector -SELECTOR: work +SELECTOR: workplace # we can't use use household income_segment as this will also be set for non-workers -CHOOSER_SEGMENT_COLUMN: income_segment +CHOOSER_SEGMENT_COLUMN_NAME: income_segment # boolean column to filter choosers (True means keep) -CHOOSER_FILTER_COLUMN: is_worker +CHOOSER_FILTER_COLUMN_NAME: is_worker # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: @@ -44,17 +47,11 @@ SEGMENT_IDS: work_high: 3 work_veryhigh: 4 - +# model adds these tables (informational - not added if commented out) SHADOW_PRICE_TABLE: workplace_shadow_prices MODELED_SIZE_TABLE: workplace_modeled_size -#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 +# not loaded if commented out +SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 10 diff --git a/activitysim/abm/test/configs/workplace_location_sample.csv b/activitysim/abm/test/configs/workplace_location_sample.csv index 5c4abd4f5..1559309b1 100644 --- a/activitysim/abm/test/configs/workplace_location_sample.csv +++ b/activitysim/abm/test/configs/workplace_location_sample.csv @@ -1,16 +1,11 @@ -Description,Expression,Alt -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-0.8428 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-0.3104 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.3783 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.1285 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0917 -"Distance 0 to 5 mi, high and very high income",@(df.income_segment>=3)*skims['DIST'].clip(upper=5),0.15 -"Distance 5+ mi, high and very high income",@(df.income_segment>=3)*(skims['DIST']-5).clip(0),0.02 -"Size variable full-time worker, low income","@(df.income_segment==1)*df['work_low'].apply(np.log1p)",1 -"Size variable full-time worker, medium income","@(df.income_segment==2)*df['work_med'].apply(np.log1p)",1 -"Size variable full-time worker, high income","@(df.income_segment==3)*df['work_high'].apply(np.log1p)",1 -"Size variable full-time worker, very high income","@(df.income_segment==4)*df['work_veryhigh'].apply(np.log1p)",1 -"No attractions full-time worker, low income","@(df.income_segment==1)&(df['work_low']==0)",-999 -"No attractions full-time worker, medium income","@(df.income_segment==2)&(df['work_med']==0)",-999 -"No attractions full-time worker, high income","@(df.income_segment==3)&(df['work_high']==0)",-999 -"No attractions full-time worker, very high income","@(df.income_segment==4)&(df['work_veryhigh']==0)",-999 +Description,Expression,work_low,work_med,work_high,work_veryhigh +,_DIST@skims['DIST'],1,1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-0.8428,-0.8428,-0.8428,-0.8428 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-0.3104,-0.3104,-0.3104,-0.3104 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.3783,-0.3783,-0.3783,-0.3783 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.1285,-0.1285,-0.1285,-0.1285 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0917,-0.0917,-0.0917,-0.0917 +"Distance 0 to 5 mi, high and very high income",@_DIST.clip(upper=5),0.0,0.0,0.15,0.15 +"Distance 5+ mi, high and very high income",@(_DIST-5).clip(0),0.0,0.0,0.02,0.02 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000,-999.0000 diff --git a/activitysim/abm/test/configs_shadow/shadow_pricing.yaml b/activitysim/abm/test/configs_shadow/shadow_pricing.yaml new file mode 100644 index 000000000..3335b582a --- /dev/null +++ b/activitysim/abm/test/configs_shadow/shadow_pricing.yaml @@ -0,0 +1,7 @@ +inherit_settings: True + +# global switch to enable/disable loading of saved shadow prices +# (ignored if global use_shadow_pricing switch is False) +LOAD_SAVED_SHADOW_PRICES: True + +MAX_ITERATIONS_SAVED: 1 diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 69d6db656..28484249d 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -164,11 +164,6 @@ def test_mini_pipeline_run(): pipeline.run(models=_MODELS, resume_after=None) - # data_dir = inject.get_injectable('data_dir') - # school_destination_size = pipeline.get_table("school_shadow_prices") - # school_destination_size.to_csv(os.path.join(data_dir, 'school_shadow_prices.csv'), - # index=True, header=True) - regress_mini_auto() pipeline.run_model('cdap_simulate') @@ -325,7 +320,7 @@ def regress_tour_modes(tours_df): print("mode_df\n", tours_df[mode_cols]) """ - tour_id tour_mode person_id tour_type tour_num tour_category + tour_id tour_mode person_id tour_type tour_num tour_category tour_id 94247751 SHARED2FREE 3249922 othmaint 1 joint 94247765 WALK_LOC 3249922 work 1 mandatory @@ -419,7 +414,7 @@ def test_full_run2(): pipeline.close_pipeline() -def test_full_run_with_chunks(): +def test_full_run3_with_chunks(): # should get the same result with different chunk size @@ -437,7 +432,7 @@ def test_full_run_with_chunks(): pipeline.close_pipeline() -def test_full_run_stability(): +def test_full_run4_stability(): # hh should get the same result with different sample size @@ -452,7 +447,7 @@ def test_full_run_stability(): pipeline.close_pipeline() -def test_full_run_singleton(): +def test_full_run5_singleton(): # should wrk with only one hh diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index 2d905ff34..b19adebe0 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -172,8 +172,8 @@ def log_write_hwm(): d = HWM[0] for tag in d: hwm = d[tag] - logger.info("#chunk_hwm high_water_mark %s: %s (%s) in %s" % - (tag, hwm['mark'], hwm['info'], hwm['trace_label']), ) + logger.debug("#chunk_hwm high_water_mark %s: %s (%s) in %s" % + (tag, hwm['mark'], hwm['info'], hwm['trace_label']), ) # - elements shouldn't exceed chunk_size or effective_chunk_size of base chunker def check_chunk_size(hwm, chunk_size, label, max_leeway): @@ -187,7 +187,7 @@ def check_chunk_size(hwm, chunk_size, label, max_leeway): if len(HWM) > 1 and HWM[1]: assert 'elements' in HWM[1] # expect an 'elements' hwm dict for base chunker hwm = HWM[1].get('elements') - check_chunk_size(hwm, EFFECTIVE_CHUNK_SIZE[0], 'effective_chunk_size', max_leeway=1.05) + check_chunk_size(hwm, EFFECTIVE_CHUNK_SIZE[0], 'effective_chunk_size', max_leeway=1.1) check_chunk_size(hwm, CHUNK_SIZE[0], 'chunk_size', max_leeway=1) @@ -206,9 +206,9 @@ def rows_per_chunk(chunk_size, row_size, num_choosers, trace_label): effective_chunk_size = row_size * rpc num_chunks = (num_choosers // rpc) + (num_choosers % rpc > 0) - logger.info("#chunk_calc num_chunks: %s, rows_per_chunk: %s, " - "effective_chunk_size: %s, num_choosers: %s : %s" % - (num_chunks, rpc, effective_chunk_size, num_choosers, trace_label)) + logger.debug("#chunk_calc num_chunks: %s, rows_per_chunk: %s, " + "effective_chunk_size: %s, num_choosers: %s : %s" % + (num_chunks, rpc, effective_chunk_size, num_choosers, trace_label)) return rpc, effective_chunk_size diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index cf453053a..5372d3f11 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) -DUMP = False +DUMP = True def make_sample_choices( diff --git a/activitysim/core/tracing.py b/activitysim/core/tracing.py index 2bbc43bc3..cbaddce1e 100644 --- a/activitysim/core/tracing.py +++ b/activitysim/core/tracing.py @@ -171,7 +171,8 @@ def print_summary(label, df, describe=False, value_counts=False): logger.error("print_summary neither value_counts nor describe") if value_counts: - logger.info("%s value counts:\n%s" % (label, df.value_counts())) + n = 10 + logger.info("%s top %s value counts:\n%s" % (label, n, df.value_counts().nlargest(n))) if describe: logger.info("%s summary:\n%s" % (label, df.describe())) diff --git a/docs/core.rst b/docs/core.rst index c2b7deb8d..895af7866 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -173,7 +173,7 @@ coefficients) is specified in the YAML settings file. +========================================+==========================================================+=================+===============+ |DA - Unavailable | sov_available == False | -999 | | +----------------------------------------+----------------------------------------------------------+-----------------+---------------+ -|DA - In-vehicle time | @c_ivt*(@odt_skims['SOV_TIME'] + dot_skims['SOV_TIME']) | 1 | | +|DA - In-vehicle time | @c_ivt*(odt_skims['SOV_TIME'] + dot_skims['SOV_TIME']) | 1 | | +----------------------------------------+----------------------------------------------------------+-----------------+---------------+ |DAP - Unavailable for age less than 16 | age < 16 | | -999 | +----------------------------------------+----------------------------------------------------------+-----------------+---------------+ diff --git a/example/configs/destination_choice_size_terms.csv b/example/configs/destination_choice_size_terms.csv index 4f01529a9..ece155caa 100644 --- a/example/configs/destination_choice_size_terms.csv +++ b/example/configs/destination_choice_size_terms.csv @@ -1,8 +1,8 @@ selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE -work,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 -work,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 -work,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 -work,work_veryhigh,0,0.093,0.27,0.241,0.146,0.004,0.246,0,0,0,0 +workplace,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 +workplace,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 +workplace,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 +workplace,work_veryhigh,0,0.093,0.27,0.241,0.146,0.004,0.246,0,0,0,0 school,university,0,0,0,0,0,0,0,0,0,0.592,0.408 school,gradeschool,0,0,0,0,0,0,0,1,0,0,0 school,highschool,0,0,0,0,0,0,0,0,1,0,0 diff --git a/example/configs/school_location.csv b/example/configs/school_location.csv index c771116ca..893f79a06 100644 --- a/example/configs/school_location.csv +++ b/example/configs/school_location.csv @@ -1,11 +1,11 @@ Description,Expression,university,highschool,gradeschool -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-3.2451,-0.9523,-1.6419 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-2.7011,-0.5700,-0.5700 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.5707,-0.5700,-0.5700 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.5002,-0.1930,-0.2031 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0730,-0.1882,-0.0460 -Size variable,@df[segment].apply(np.log1p),1.0000,1.0000,1.0000 -No attractions,@df[segment]==0,-999.0000,-999.0000,-999.0000 +,_DIST@skims['DIST'],1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-3.2451,-0.9523,-1.6419 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-2.7011,-0.5700,-0.5700 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.5707,-0.5700,-0.5700 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.5002,-0.1930,-0.2031 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0730,-0.1882,-0.0460 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000 Mode choice logsum,mode_choice_logsum,0.5358,0.5358,0.5358 "Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1 - diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index c3c344096..2b78ac8a5 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -10,21 +10,25 @@ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +DEST_CHOICE_COLUMN_NAME: school_taz + SAMPLE_SPEC: school_location_sample.csv SPEC: school_location.csv LOGSUM_SETTINGS: tour_mode_choice.yaml LOGSUM_PREPROCESSOR: nontour_preprocessor +LOGSUM_TOUR_PURPOSE: + university: univ + highschool: school + gradeschool: school + annotate_persons: SPEC: annotate_persons_school DF: persons # - shadow pricing -MAX_SHADOW_PRICE_ITERATIONS: 5 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 - # required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons @@ -32,7 +36,11 @@ CHOOSER_TABLE_NAME: persons SELECTOR: school # chooser column with segment_id for this segment type -CHOOSER_SEGMENT_COLUMN: school_segment +CHOOSER_SEGMENT_COLUMN_NAME: school_segment + +# boolean column to filter choosers (True means keep) +CHOOSER_FILTER_COLUMN_NAME: is_student + # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: @@ -41,17 +49,8 @@ SEGMENT_IDS: gradeschool: 1 # model adds these tables (informational - not added if commented out) -#SHADOW_PRICE_TABLE: school_shadow_prices -#MODELED_SIZE_TABLE: school_modeled_size +SHADOW_PRICE_TABLE: school_shadow_prices +MODELED_SIZE_TABLE: school_modeled_size # not loaded if commented out -#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 - -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 - -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 10 +SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv diff --git a/example/configs/school_location_sample.csv b/example/configs/school_location_sample.csv index c66526b1a..8bbf5171e 100644 --- a/example/configs/school_location_sample.csv +++ b/example/configs/school_location_sample.csv @@ -1,8 +1,9 @@ Description,Expression,university,highschool,gradeschool -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-3.2451,-0.9523,-1.6419 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-2.7011,-0.5700,-0.5700 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.5707,-0.5700,-0.5700 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.5002,-0.1930,-0.2031 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0730,-0.1882,-0.0460 -Size variable,@df[segment].apply(np.log1p),1.0000,1.0000,1.0000 -No attractions,@df[segment]==0,-999.0000,-999.0000,-999.0000 +,_DIST@skims['DIST'],1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-3.2451,-0.9523,-1.6419 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-2.7011,-0.5700,-0.5700 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.5707,-0.5700,-0.5700 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.5002,-0.1930,-0.2031 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0730,-0.1882,-0.0460 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000 diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 2ea5e2dd8..45b74be98 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -15,11 +15,9 @@ check_for_variability: False # - shadow pricing global switches # turn shadow_pricing on and off for all models (e.g. school and work) -use_shadow_pricing: True +# shadow pricing is deprecated for less than full samples +use_shadow_pricing: False -# global switch to enable/disable loading of saved shadow prices -# (ignored if global use_shadow_pricing switch is False) -load_saved_shadow_prices: True # - tracing @@ -113,7 +111,4 @@ skim_time_periods: - MD - PM -shadow_pricing_models: - school: school_location - work: workplace_location diff --git a/example/configs/shadow_pricing.yaml b/example/configs/shadow_pricing.yaml new file mode 100644 index 000000000..95153e2f7 --- /dev/null +++ b/example/configs/shadow_pricing.yaml @@ -0,0 +1,34 @@ +shadow_pricing_models: + school: school_location + workplace: workplace_location + +# global switch to enable/disable loading of saved shadow prices +# (ignored if global use_shadow_pricing switch is False) +LOAD_SAVED_SHADOW_PRICES: True + +# number of shadow price iterations for cold start +MAX_ITERATIONS: 5 + +# number of shadow price iterations for warm start (after loading saved shadow_prices) +MAX_ITERATIONS_SAVED: 1 + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 10 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 10 + +# CTRAMP or daysim +#SHADOW_PRICE_METHOD: ctramp +SHADOW_PRICE_METHOD: daysim + +# ctramp-style shadow_pricing_method parameters +DAMPING_FACTOR: 0.3 + +# daysim-style shadow_pricing_method parameters +# FIXME should these be the same as PERCENT_TOLERANCE and FAIL_THRESHOLD above? +DAYSIM_ABSOLUTE_TOLERANCE: 50 +DAYSIM_PERCENT_TOLERANCE: 10 diff --git a/example/configs/workplace_location.csv b/example/configs/workplace_location.csv index 1aec2724d..f2c9bab4b 100644 --- a/example/configs/workplace_location.csv +++ b/example/configs/workplace_location.csv @@ -1,18 +1,13 @@ -Description,Expression,Alt -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-0.8428 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-0.3104 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.3783 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.1285 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0917 -"Distance 0 to 5 mi, high and very high income",@(df.income_segment>=3)*skims['DIST'].clip(upper=5),0.15 -"Distance 5+ mi, high and very high income",@(df.income_segment>=3)*(skims['DIST']-5).clip(0),0.02 -"Size variable full-time worker, low income","@(df.income_segment==1)*df['work_low'].apply(np.log1p)",1 -"Size variable full-time worker, medium income","@(df.income_segment==2)*df['work_med'].apply(np.log1p)",1 -"Size variable full-time worker, high income","@(df.income_segment==3)*df['work_high'].apply(np.log1p)",1 -"Size variable full-time worker, very high income","@(df.income_segment==4)*df['work_veryhigh'].apply(np.log1p)",1 -"No attractions full-time worker, low income","@(df.income_segment==1)&(df['work_low']==0)",-999 -"No attractions full-time worker, medium income","@(df.income_segment==2)&(df['work_med']==0)",-999 -"No attractions full-time worker, high income","@(df.income_segment==3)&(df['work_high']==0)",-999 -"No attractions full-time worker, very high income","@(df.income_segment==4)&(df['work_veryhigh']==0)",-999 -"Mode choice logsum",mode_choice_logsum,0.3 -"Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1 +Description,Expression,work_low,work_med,work_high,work_veryhigh +,_DIST@skims['DIST'],1,1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-0.8428,-0.8428,-0.8428,-0.8428 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-0.3104,-0.3104,-0.3104,-0.3104 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.3783,-0.3783,-0.3783,-0.3783 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.1285,-0.1285,-0.1285,-0.1285 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0917,-0.0917,-0.0917,-0.0917 +"Distance 0 to 5 mi, high and very high income",@_DIST.clip(upper=5),0.0,0.0,0.15,0.15 +"Distance 5+ mi, high and very high income",@(_DIST-5).clip(0),0.0,0.0,0.02,0.02 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000,-999.0000 +"Mode choice logsum",mode_choice_logsum,0.3,0.3,0.3,0.3 +"Sample of alternatives correction factor","@np.minimum(np.log(df.pick_count/df.prob), 60)",1,1,1,1 diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index 121524c28..e099d23ff 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -4,8 +4,12 @@ SIMULATE_CHOOSER_COLUMNS: - income_segment - TAZ +SAMPLE_SPEC: workplace_location_sample.csv +SPEC: workplace_location.csv + LOGSUM_SETTINGS: tour_mode_choice.yaml LOGSUM_PREPROCESSOR: nontour_preprocessor +LOGSUM_TOUR_PURPOSE: work # model-specific logsum-related settings CHOOSER_ORIG_COL_NAME: TAZ @@ -13,6 +17,7 @@ ALT_DEST_COL_NAME: alt_dest IN_PERIOD: 8 OUT_PERIOD: 17 +DEST_CHOICE_COLUMN_NAME: workplace_taz annotate_persons: SPEC: annotate_persons_workplace @@ -27,13 +32,13 @@ annotate_persons: CHOOSER_TABLE_NAME: persons_merged # size_terms selector -SELECTOR: work +SELECTOR: workplace # we can't use use household income_segment as this will also be set for non-workers -CHOOSER_SEGMENT_COLUMN: income_segment +CHOOSER_SEGMENT_COLUMN_NAME: income_segment # boolean column to filter choosers (True means keep) -CHOOSER_FILTER_COLUMN: is_worker +CHOOSER_FILTER_COLUMN_NAME: is_worker # FIXME - these are assigned to persons in annotate_persons. we need a better way to manage this SEGMENT_IDS: @@ -43,17 +48,10 @@ SEGMENT_IDS: work_veryhigh: 4 # model adds these tables (informational - not added if commented out) -#SHADOW_PRICE_TABLE: workplace_shadow_prices -#MODELED_SIZE_TABLE: workplace_modeled_size +SHADOW_PRICE_TABLE: workplace_shadow_prices +MODELED_SIZE_TABLE: workplace_modeled_size # not loaded if commented out -#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 +SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 10 diff --git a/example/configs/workplace_location_sample.csv b/example/configs/workplace_location_sample.csv index 5c4abd4f5..1559309b1 100644 --- a/example/configs/workplace_location_sample.csv +++ b/example/configs/workplace_location_sample.csv @@ -1,16 +1,11 @@ -Description,Expression,Alt -"Distance, piecewise linear from 0 to 1 miles",@skims['DIST'].clip(1),-0.8428 -"Distance, piecewise linear from 1 to 2 miles","@(skims['DIST']-1).clip(0,1)",-0.3104 -"Distance, piecewise linear from 2 to 5 miles","@(skims['DIST']-2).clip(0,3)",-0.3783 -"Distance, piecewise linear from 5 to 15 miles","@(skims['DIST']-5).clip(0,10)",-0.1285 -"Distance, piecewise linear for 15+ miles",@(skims['DIST']-15.0).clip(0),-0.0917 -"Distance 0 to 5 mi, high and very high income",@(df.income_segment>=3)*skims['DIST'].clip(upper=5),0.15 -"Distance 5+ mi, high and very high income",@(df.income_segment>=3)*(skims['DIST']-5).clip(0),0.02 -"Size variable full-time worker, low income","@(df.income_segment==1)*df['work_low'].apply(np.log1p)",1 -"Size variable full-time worker, medium income","@(df.income_segment==2)*df['work_med'].apply(np.log1p)",1 -"Size variable full-time worker, high income","@(df.income_segment==3)*df['work_high'].apply(np.log1p)",1 -"Size variable full-time worker, very high income","@(df.income_segment==4)*df['work_veryhigh'].apply(np.log1p)",1 -"No attractions full-time worker, low income","@(df.income_segment==1)&(df['work_low']==0)",-999 -"No attractions full-time worker, medium income","@(df.income_segment==2)&(df['work_med']==0)",-999 -"No attractions full-time worker, high income","@(df.income_segment==3)&(df['work_high']==0)",-999 -"No attractions full-time worker, very high income","@(df.income_segment==4)&(df['work_veryhigh']==0)",-999 +Description,Expression,work_low,work_med,work_high,work_veryhigh +,_DIST@skims['DIST'],1,1,1,1 +"Distance, piecewise linear from 0 to 1 miles",@_DIST.clip(1),-0.8428,-0.8428,-0.8428,-0.8428 +"Distance, piecewise linear from 1 to 2 miles","@(_DIST-1).clip(0,1)",-0.3104,-0.3104,-0.3104,-0.3104 +"Distance, piecewise linear from 2 to 5 miles","@(_DIST-2).clip(0,3)",-0.3783,-0.3783,-0.3783,-0.3783 +"Distance, piecewise linear from 5 to 15 miles","@(_DIST-5).clip(0,10)",-0.1285,-0.1285,-0.1285,-0.1285 +"Distance, piecewise linear for 15+ miles",@(_DIST-15.0).clip(0),-0.0917,-0.0917,-0.0917,-0.0917 +"Distance 0 to 5 mi, high and very high income",@_DIST.clip(upper=5),0.0,0.0,0.15,0.15 +"Distance 5+ mi, high and very high income",@(_DIST-5).clip(0),0.0,0.0,0.02,0.02 +Size variable,@df[segment_size].apply(np.log1p),1.0000,1.0000,1.0000,1.0000 +No attractions,@df[segment_size]==0,-999.0000,-999.0000,-999.0000,-999.0000 diff --git a/example_azure/benchmarks/benchmarks_full_run.txt b/example_azure/benchmarks/benchmarks_full_run.txt index 14aabea6c..a70ba5103 100644 --- a/example_azure/benchmarks/benchmarks_full_run.txt +++ b/example_azure/benchmarks/benchmarks_full_run.txt @@ -125,16 +125,16 @@ Time to execute everything : 3596.278 seconds (59.9 minutes) Max RAM 233.90GB -# with shadow pricing 5 iterations +# with shadow pricing 10 iterations households_sample_size: 0 -chunk_size: 90000000000 +chunk_size: 80000000000 num_processes: 60 -stagger: 5 +stagger: 0 -INFO - activitysim.core.mem - high water mark used: 325.70 timestamp: 14/12/2018 02:38:08 label: mp_households_15.non_mandatory_tour_destination.completed -INFO - activitysim.core.mem - high water mark rss: 557.75 timestamp: 14/12/2018 02:38:08 label: mp_households_15.non_mandatory_tour_destination.completed -INFO - activitysim.core.tracing - Time to execute everything : 6017.381 seconds (100.3 minutes) +INFO - activitysim.core.mem - high water mark used: 342.87 timestamp: 18/12/2018 00:56:39 label: mp_households_50.non_mandatory_tour_scheduling.completed +INFO - activitysim.core.mem - high water mark rss: 574.65 timestamp: 18/12/2018 00:56:39 label: mp_households_50.non_mandatory_tour_scheduling.completed +INFO - activitysim.core.tracing - Time to execute everything : 8189.908 seconds (136.5 minutes) ------------ azure Windows 2TB 128 processors diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 05ded19fc..783b383eb 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -59,6 +59,7 @@ export OMP_NUM_THREADS=1 python simulation.py -d /datadrive/work/data/full +tail -f output/log/mp_households_0-activitysim.log tar zcvf output.tar.gz output/log/ output/trace/ diff --git a/example_mp/configs/school_location.yaml b/example_mp/configs/school_location.yaml deleted file mode 100644 index 01ac27f50..000000000 --- a/example_mp/configs/school_location.yaml +++ /dev/null @@ -1,22 +0,0 @@ -inherit_settings: True - -# - shadow pricing - -MAX_SHADOW_PRICE_ITERATIONS: 10 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 - -# model adds these tables (informational - not added if commented out) -SHADOW_PRICE_TABLE: school_shadow_prices -MODELED_SIZE_TABLE: school_modeled_size - -# not loaded if commented out -#SAVED_SHADOW_PRICE_TABLE_NAME: school_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 - -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 - -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 10 diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 04a39eea9..00b2de67b 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -10,7 +10,6 @@ profile: False strict: False mem_tick: 0 use_shadow_pricing: True -load_saved_shadow_prices: True # - full sample - 2732722 households on 64 processor 432 GiB RAM @@ -32,7 +31,6 @@ stagger: 0 #strict: False #mem_tick: 30 #use_shadow_pricing: True -#load_saved_shadow_prices: False # ## - small sample #households_sample_size: 5000 diff --git a/example_mp/configs/shadow_pricing.yaml b/example_mp/configs/shadow_pricing.yaml new file mode 100644 index 000000000..abc859d83 --- /dev/null +++ b/example_mp/configs/shadow_pricing.yaml @@ -0,0 +1,33 @@ +inherit_settings: True + + +# global switch to enable/disable loading of saved shadow prices +# (ignored if global use_shadow_pricing switch is False) +LOAD_SAVED_SHADOW_PRICES: True + +# number of shadow price iterations for cold start +MAX_ITERATIONS: 5 + +# number of shadow price iterations for warm start (after loading saved shadow_prices) +MAX_ITERATIONS_SAVED: 1 + +# ignore criteria for zones smaller than size_threshold +SIZE_THRESHOLD: 10 + +# zone passes if modeled is within percent_tolerance of predicted_size +PERCENT_TOLERANCE: 5 + +# max percentage of zones allowed to fail +FAIL_THRESHOLD: 10 + +# CTRAMP or daysim +#SHADOW_PRICE_METHOD: ctramp +SHADOW_PRICE_METHOD: daysim + +# ctramp-style shadow_pricing_method parameters +DAMPING_FACTOR: 0.3 + +# daysim-style shadow_pricing_method parameters +# FIXME should these be the same as PERCENT_TOLERANCE and FAIL_THRESHOLD above? +DAYSIM_ABSOLUTE_TOLERANCE: 50 +DAYSIM_PERCENT_TOLERANCE: 10 diff --git a/example_mp/configs/workplace_location.yaml b/example_mp/configs/workplace_location.yaml deleted file mode 100644 index caf0fc8fc..000000000 --- a/example_mp/configs/workplace_location.yaml +++ /dev/null @@ -1,22 +0,0 @@ -inherit_settings: True - -# - shadow pricing - -MAX_SHADOW_PRICE_ITERATIONS: 10 -MAX_SHADOW_PRICE_ITERATIONS_SAVED: 1 - -# model adds these tables (informational - not added if commented out) -SHADOW_PRICE_TABLE: workplace_shadow_prices -MODELED_SIZE_TABLE: workplace_modeled_size - -# not loaded if commented out -#SAVED_SHADOW_PRICE_TABLE_NAME: workplace_shadow_prices.csv - -# ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 100 - -# zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 - -# max percentage of zones allowed to fail -FAIL_THRESHOLD: 10 From 1d031d62028e5c3b18e7cdaf0917f250a03432af Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Wed, 26 Dec 2018 00:15:20 -0500 Subject: [PATCH 063/122] individual value of time --- activitysim/abm/models/location_choice.py | 4 +- activitysim/abm/tables/shadow_pricing.py | 6 +- .../abm/test/configs/annotate_households.csv | 9 +- .../configs/annotate_persons_after_hh.csv | 5 + .../test/configs/initialize_households.yaml | 6 ++ activitysim/abm/test/configs/settings.yaml | 91 ++++++++++++++++- .../abm/test/configs/tour_mode_choice.yaml | 1 + ..._choice_annotate_choosers_preprocessor.csv | 3 +- activitysim/abm/test/data/override_hh_ids.csv | 2 +- activitysim/abm/test/run_mp.py | 19 ++-- activitysim/abm/test/test_pipeline.py | 39 +++++--- activitysim/core/assign.py | 2 + activitysim/core/interaction_sample.py | 2 +- activitysim/core/random.py | 99 +++++++++++++++++++ example/configs/annotate_households.csv | 7 ++ example/configs/annotate_persons_after_hh.csv | 5 + example/configs/initialize_households.yaml | 6 ++ example/configs/settings.yaml | 15 ++- example/configs/tour_mode_choice.yaml | 1 + ..._choice_annotate_choosers_preprocessor.csv | 3 +- example/configs/trip_mode_choice.yaml | 1 + ...ode_choice_annotate_trips_preprocessor.csv | 3 +- .../benchmarks/benchmarks_full_shadow.txt | 41 ++++++++ example_azure/ubuntu/step3_run_asim.txt | 9 +- example_mp/configs/settings.yaml | 6 +- example_mp/configs/shadow_pricing.yaml | 6 +- 26 files changed, 345 insertions(+), 46 deletions(-) create mode 100644 activitysim/abm/test/configs/annotate_persons_after_hh.csv create mode 100644 example/configs/annotate_persons_after_hh.csv create mode 100644 example_azure/benchmarks/benchmarks_full_shadow.txt diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index 601670e12..782f78274 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -328,9 +328,9 @@ def iterate_location_choice( logging.debug("%s max_iterations: %s" % (trace_label, max_iterations)) choices = None - for iteration in range(max_iterations): + for iteration in range(1, max_iterations + 1): - if iteration > 0: + if spc.use_shadow_pricing and iteration > 0: spc.update_shadow_prices() choices = run_location_choice( diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index e134a8900..64b17d6b5 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -52,6 +52,7 @@ class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) + self.saved_shadow_price_file_path = None # set by read_saved_shadow_prices if loaded self.selector = model_settings['SELECTOR'] @@ -115,7 +116,8 @@ def read_saved_shadow_prices(self, model_settings): file_path = config.data_file_path(saved_shadow_price_file_name, mandatory=False) if file_path: shadow_prices = pd.read_csv(file_path, index_col=0) - logging.info("reading saved_shadow_prices from %s" % (file_path)) + self.saved_shadow_price_file_path = file_path # informational + logging.info("loaded saved_shadow_prices from %s" % (file_path)) else: logging.warning("Could not find saved_shadow_prices file %s" % (file_path)) @@ -343,7 +345,7 @@ def shadow_price_adjusted_predicted_size(self): def write_trace_files(self, iteration): logger.info("write_trace_files iteration %s" % iteration) - if iteration == 0: + if iteration == 1: tracing.write_csv(self.predicted_size, 'shadow_price_%s_predicted_size_%s' % (self.selector, iteration), transpose=False) diff --git a/activitysim/abm/test/configs/annotate_households.csv b/activitysim/abm/test/configs/annotate_households.csv index 6d64cb5e8..68fbfc4b0 100644 --- a/activitysim/abm/test/configs/annotate_households.csv +++ b/activitysim/abm/test/configs/annotate_households.csv @@ -1,10 +1,17 @@ Description,Target,Expression #,, annotate households table after import -,_PERSON_COUNT,"lambda query, persons, households: persons.query(query).groupby('household_id').size().reindex(households.index).fillna(0)" +,_PERSON_COUNT,"lambda query, persons, households: persons.query(query).groupby('household_id').size().reindex(households.index).fillna(0).astype(np.int8)" #,,FIXME households.income can be negative, so we clip? income_in_thousands,income_in_thousands,(households.income / 1000).clip(lower=0) income_segment,income_segment,"pd.cut(income_in_thousands, bins=[-np.inf, 30, 60, 100, np.inf], labels=[1, 2, 3, 4]).astype(int)" #,, +,_MIN_VOT,setting('min_value_of_time') +,_MAX_VOT,setting('max_value_of_time') +,_MU,setting('distributed_vot_mu') +,_SIGMA,setting('distributed_vot_sigma') +median_value_of_time,median_value_of_time,"income_segment.map({k: v for k, v in setting('household_median_value_of_time').items()})" +hh_value_of_time,hh_value_of_time,"rng.lognormal_for_df(df, mu=np.log(median_value_of_time * _MU), sigma=_SIGMA).clip(_MIN_VOT, _MAX_VOT)" +#,, #num_workers was renamed in import,, #,num_workers,households.workers number of non_workers,num_non_workers,households.hhsize - households.num_workers diff --git a/activitysim/abm/test/configs/annotate_persons_after_hh.csv b/activitysim/abm/test/configs/annotate_persons_after_hh.csv new file mode 100644 index 000000000..59374d5bf --- /dev/null +++ b/activitysim/abm/test/configs/annotate_persons_after_hh.csv @@ -0,0 +1,5 @@ +Description,Target,Expression +#,, annotate persons table after annotate_households +#,, adults get full hh_value_of_time and children get 60% +,_hh_vot,"reindex(households.hh_value_of_time, persons.household_id)" +,value_of_time,"_hh_vot.where(persons.age>=18, _hh_vot * 0.667)" diff --git a/activitysim/abm/test/configs/initialize_households.yaml b/activitysim/abm/test/configs/initialize_households.yaml index 572d2c565..d8fdecc4e 100644 --- a/activitysim/abm/test/configs/initialize_households.yaml +++ b/activitysim/abm/test/configs/initialize_households.yaml @@ -16,3 +16,9 @@ annotate_tables: TABLES: - persons - land_use + - tablename: persons + annotate: + SPEC: annotate_persons_after_hh + DF: persons + TABLES: + - households diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 06c334b58..2efe3e116 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -1,18 +1,86 @@ +#input data store and skims input_store: mtc_asim.h5 skims_file: skims.omx +#number of households to simulate +households_sample_size: 100 +# simulate all households +#households_sample_size: 0 + +chunk_size: 0 + +# set false to disable variability check in simple_simulate and interaction_simulate +check_for_variability: False + # - shadow pricing global switches # turn shadow_pricing on and off for all models (e.g. school and work) -use_shadow_pricing: True +# shadow pricing is deprecated for less than full samples +use_shadow_pricing: False -households_sample_size: 100 -# trace household id; comment out for no trace -trace_hh_id: 961042 +# - tracing -# trace origin, destination in accessibility calculation +#trace household id; comment out or leave empty for no trace +#trace_hh_id: 1482966 +trace_hh_id: + +# trace origin, destination in accessibility calculation; comment out or leave empty for no trace #trace_od: [5, 11] +trace_od: + + +models: + - initialize_landuse + - compute_accessibility + - initialize_households + - school_location + - workplace_location + - auto_ownership_simulate + - cdap_simulate + - mandatory_tour_frequency + - mandatory_tour_scheduling + - joint_tour_frequency + - joint_tour_composition + - joint_tour_participation + - joint_tour_destination_sample + - joint_tour_destination_logsums + - joint_tour_destination_simulate + - joint_tour_scheduling + - non_mandatory_tour_frequency + - non_mandatory_tour_destination + - non_mandatory_tour_scheduling + - tour_mode_choice_simulate + - atwork_subtour_frequency + - atwork_subtour_destination_sample + - atwork_subtour_destination_logsums + - atwork_subtour_destination_simulate + - atwork_subtour_scheduling + - atwork_subtour_mode_choice + - stop_frequency + - trip_purpose + - trip_destination + - trip_purpose_and_destination + - trip_scheduling + - trip_mode_choice + - write_data_dictionary + - track_skim_usage + - write_tables + +# to resume after last successful checkpoint, specify resume_after: _ +#resume_after: trip_mode_choice + +output_tables: + action: include + prefix: final_ + tables: + - checkpoints +# - accessibility +# - land_use +# - households + - persons +# - trips +# - tours # area_types less than this are considered urban urban_threshold: 4 @@ -43,4 +111,17 @@ skim_time_periods: - MD - PM +# - value of time + +# value_of_time = lognormal(np.log(median_value_of_time * mu), sigma).clip(min_vot, max_vot) + +min_value_of_time: 0 +max_value_of_time: 50 +distributed_vot_mu: 0.684 +distributed_vot_sigma: 0.85 +household_median_value_of_time: + 1: 6 + 2: 8 + 3: 10 + 4: 12 diff --git a/activitysim/abm/test/configs/tour_mode_choice.yaml b/activitysim/abm/test/configs/tour_mode_choice.yaml index 513738f58..8c04be7e6 100644 --- a/activitysim/abm/test/configs/tour_mode_choice.yaml +++ b/activitysim/abm/test/configs/tour_mode_choice.yaml @@ -96,3 +96,4 @@ LOGSUM_CHOOSER_COLUMNS: - number_of_participants - tour_category - num_workers + - value_of_time diff --git a/activitysim/abm/test/configs/tour_mode_choice_annotate_choosers_preprocessor.csv b/activitysim/abm/test/configs/tour_mode_choice_annotate_choosers_preprocessor.csv index cd4409220..d6d9915ae 100644 --- a/activitysim/abm/test/configs/tour_mode_choice_annotate_choosers_preprocessor.csv +++ b/activitysim/abm/test/configs/tour_mode_choice_annotate_choosers_preprocessor.csv @@ -14,8 +14,7 @@ local,_DF_IS_TOUR,"'tour_type' in df.columns" ,is_indiv,~is_joint ,is_atwork_subtour,(df.tour_category=='joint') if 'tour_category' in df.columns else False #,, -,value_of_time,8.00 -,c_cost,(0.60 * c_ivt) / value_of_time +,c_cost,(0.60 * c_ivt) / df.value_of_time #,, ,dest_topology,"reindex(land_use.TOPOLOGY, df[dest_col_name])" ,terminal_time,"reindex(land_use.TERMINAL, df[dest_col_name])" diff --git a/activitysim/abm/test/data/override_hh_ids.csv b/activitysim/abm/test/data/override_hh_ids.csv index c25b7720a..5586cf78b 100644 --- a/activitysim/abm/test/data/override_hh_ids.csv +++ b/activitysim/abm/test/data/override_hh_ids.csv @@ -1,5 +1,5 @@ household_id -961042 +702445 608031 93713 93769 diff --git a/activitysim/abm/test/run_mp.py b/activitysim/abm/test/run_mp.py index c09a9e274..4ad52ccb5 100644 --- a/activitysim/abm/test/run_mp.py +++ b/activitysim/abm/test/run_mp.py @@ -42,24 +42,27 @@ def regress_mini_auto(): - auto_choice = pipeline.get_table("households").auto_ownership - # regression test: these are among the first 10 households in households table - hh_ids = [961042, 608031, 93713] - choices = [0, 0, 1] + hh_ids = [702445, 93713, 2525286, 945700] + choices = [1, 1, 1, 0] expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), name='auto_ownership') - print("auto_choice\n", auto_choice.head(3)) + auto_choice = pipeline.get_table("households").auto_ownership + print("auto_choice\n", auto_choice.head(4)) + + auto_choice = auto_choice.reindex(hh_ids) + """ auto_choice household_id - 961042 0 - 608031 0 + 702445 1 93713 1 + 2525286 1 + 945700 0 Name: auto_ownership, dtype: int64 """ - pdt.assert_series_equal(auto_choice.reindex(hh_ids), expected_choice) + pdt.assert_series_equal(auto_choice, expected_choice) def test_mp_run(): diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 28484249d..cd2705a1b 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -99,24 +99,27 @@ def test_rng_access(): def regress_mini_auto(): - auto_choice = pipeline.get_table("households").auto_ownership - # regression test: these are among the first 10 households in households table - hh_ids = [961042, 608031, 93713] - choices = [0, 0, 1] + hh_ids = [702445, 93713, 2525286, 945700] + choices = [1, 1, 1, 0] expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), name='auto_ownership') - print("auto_choice\n", auto_choice.head(3)) + auto_choice = pipeline.get_table("households").auto_ownership + print("auto_choice\n", auto_choice.head(4)) + + auto_choice = auto_choice.reindex(hh_ids) + """ auto_choice household_id - 961042 0 - 608031 0 + 702445 1 93713 1 + 2525286 1 + 945700 0 Name: auto_ownership, dtype: int64 """ - pdt.assert_series_equal(auto_choice.reindex(hh_ids), expected_choice) + pdt.assert_series_equal(auto_choice, expected_choice) def regress_mini_mtf(): @@ -274,7 +277,8 @@ def full_run(resume_after=None, chunk_size=0, chunk_size=chunk_size, trace_hh_id=trace_hh_id, trace_od=trace_od, - check_for_variability=check_for_variability) + check_for_variability=check_for_variability, + use_shadow_pricing=False) # shadow pricing breaks replicability when sample_size varies MODELS = settings['models'] @@ -312,7 +316,8 @@ def get_trace_csv(file_name): def regress_tour_modes(tours_df): - mode_cols = ['tour_mode', 'person_id', 'tour_type', 'tour_num', 'tour_category'] + mode_cols = ['tour_mode', 'person_id', 'tour_type', + 'tour_num', 'tour_category'] tours_df = tours_df[tours_df.household_id == HH_ID] tours_df = tours_df.sort_values(by=['person_id', 'tour_category', 'tour_num']) @@ -327,7 +332,7 @@ def regress_tour_modes(tours_df): 94247744 WALK 3249922 eatout 1 non_mandatory 94247771 SHARED3FREE 3249923 eat 1 atwork 94247794 WALK_LOC 3249923 work 1 mandatory - 94247773 DRIVEALONEFREE 3249923 social 1 non_mandatory + 94247793 DRIVEALONEFREE 3249923 social 1 non_mandatory """ EXPECT_PERSON_IDS = [ @@ -364,6 +369,18 @@ def regress_tour_modes(tours_df): def regress(): + persons_df = pipeline.get_table('persons') + persons_df = persons_df[persons_df.household_id == HH_ID] + print("persons_df\n", persons_df[['value_of_time', 'distance_to_work']]) + + """ + persons_df + person_id value_of_time distance_to_work + person_id + 3249922 23.349532 0.62 + 3249923 23.349532 0.62 + """ + tours_df = pipeline.get_table('tours') regress_tour_modes(tours_df) diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 85aff2903..6d6eb76ed 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -16,6 +16,7 @@ from activitysim.core import util from activitysim.core import config +from activitysim.core import pipeline logger = logging.getLogger(__name__) @@ -147,6 +148,7 @@ def local_utilities(): 'reindex': util.reindex, 'setting': config.setting, 'other_than': util.other_than, + 'rng': pipeline.get_rn_generator(), } return utility_dict diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 5372d3f11..cf453053a 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) -DUMP = True +DUMP = False def make_sample_choices( diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 3219b4071..3802965e8 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -181,6 +181,11 @@ def _generators_for_df(self, df): seeded and fast-forwarded on-the-fly to the appropriate position in the channel's random number stream for each row in df. + WARNING: + since we are reusing a single underlying randomstate, + prng must be called when yielded as generated sequence, + not serialized and called later after iterator finishes + Parameters ---------- df : pandas.DataFrame @@ -237,12 +242,69 @@ def random_for_df(self, df, step_name, n=1): assert self.step_name assert self.step_name == step_name + # - reminder: prng must be called when yielded as generated sequence, not serialized generators = self._generators_for_df(df) + rands = np.asanyarray([prng.rand(n) for prng in generators]) # update offset for rows we handled self.row_states.loc[df.index, 'offset'] += n return rands + def lognormal_for_df(self, df, step_name, mu, sigma): + """ + Return a floating point random number in lognormal distribution for each row in df + using the appropriate random channel for each row. + + Subsequent calls (in the same step) will return the next rand for each df row + + The resulting array will be the same length (and order) as df + This method is designed to support alternative selection from a probability array + + The columns in df are ignored; the index name and values are used to determine + which random number sequence to to use. + + If "true pseudo random" behavior is desired (i.e. NOT repeatable) the set_base_seed + method (q.v.) may be used to globally reseed all random streams. + + Parameters + ---------- + df : pandas.DataFrame or Series + df or series with index name and values corresponding to a registered channel + + mu : float or pd.Series or array of floats with one value per df row + sigma : float or array of floats with one value per df row + + Returns + ------- + rands : 2-D ndarray + array the same length as df, with n floats in range [0, 1) for each df row + """ + + assert self.step_name + assert self.step_name == step_name + + def to_series(x): + if np.isscalar(x): + return [x] * len(df) + elif isinstance(x, pd.Series): + return x.values + return x + + # - reminder: prng must be called when yielded as generated sequence, not serialized + generators = self._generators_for_df(df) + + mu = to_series(mu) + sigma = to_series(sigma) + + rands = \ + np.asanyarray([prng.lognormal(mean=mu[i], sigma=sigma[i]) + for i, prng in enumerate(generators)]) + + # update offset for rows we handled + self.row_states.loc[df.index, 'offset'] += 1 + + return rands + def choice_for_df(self, df, step_name, a, size, replace): """ Apply numpy.random.choice once for each row in df @@ -537,6 +599,43 @@ def random_for_df(self, df, n=1): rands = channel.random_for_df(df, self.step_name, n) return rands + def lognormal_for_df(self, df, mu, sigma): + """ + Return a single floating point random number in range [0, 1) for each row in df + using the appropriate random channel for each row. + + Subsequent calls (in the same step) will return the next rand for each df row + + The resulting array will be the same length (and order) as df + This method is designed to support alternative selection from a probability array + + The columns in df are ignored; the index name and values are used to determine + which random number sequence to to use. + + We assume that we can identify the channel to used based on the name of df.index + This channel should have already been registered by a call to add_channel (q.v.) + + If "true pseudo random" behavior is desired (i.e. NOT repeatable) the set_base_seed + method (q.v.) may be used to globally reseed all random streams. + + Parameters + ---------- + df : pandas.DataFrame + df with index name and values corresponding to a registered channel + + mu : float or array of floats with one value per df row + sigma : float or array of floats with one value per df row + + Returns + ------- + rands : 1-D ndarray the same length as df + a single float in lognormal distribution for each row in df + """ + + channel = self.get_channel_for_df(df) + rands = channel.lognormal_for_df(df, self.step_name, mu, sigma) + return rands + def choice_for_df(self, df, a, size, replace): """ Apply numpy.random.choice once for each row in df diff --git a/example/configs/annotate_households.csv b/example/configs/annotate_households.csv index 7b47137c0..68fbfc4b0 100644 --- a/example/configs/annotate_households.csv +++ b/example/configs/annotate_households.csv @@ -5,6 +5,13 @@ Description,Target,Expression income_in_thousands,income_in_thousands,(households.income / 1000).clip(lower=0) income_segment,income_segment,"pd.cut(income_in_thousands, bins=[-np.inf, 30, 60, 100, np.inf], labels=[1, 2, 3, 4]).astype(int)" #,, +,_MIN_VOT,setting('min_value_of_time') +,_MAX_VOT,setting('max_value_of_time') +,_MU,setting('distributed_vot_mu') +,_SIGMA,setting('distributed_vot_sigma') +median_value_of_time,median_value_of_time,"income_segment.map({k: v for k, v in setting('household_median_value_of_time').items()})" +hh_value_of_time,hh_value_of_time,"rng.lognormal_for_df(df, mu=np.log(median_value_of_time * _MU), sigma=_SIGMA).clip(_MIN_VOT, _MAX_VOT)" +#,, #num_workers was renamed in import,, #,num_workers,households.workers number of non_workers,num_non_workers,households.hhsize - households.num_workers diff --git a/example/configs/annotate_persons_after_hh.csv b/example/configs/annotate_persons_after_hh.csv new file mode 100644 index 000000000..59374d5bf --- /dev/null +++ b/example/configs/annotate_persons_after_hh.csv @@ -0,0 +1,5 @@ +Description,Target,Expression +#,, annotate persons table after annotate_households +#,, adults get full hh_value_of_time and children get 60% +,_hh_vot,"reindex(households.hh_value_of_time, persons.household_id)" +,value_of_time,"_hh_vot.where(persons.age>=18, _hh_vot * 0.667)" diff --git a/example/configs/initialize_households.yaml b/example/configs/initialize_households.yaml index 572d2c565..d8fdecc4e 100644 --- a/example/configs/initialize_households.yaml +++ b/example/configs/initialize_households.yaml @@ -16,3 +16,9 @@ annotate_tables: TABLES: - persons - land_use + - tablename: persons + annotate: + SPEC: annotate_persons_after_hh + DF: persons + TABLES: + - households diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 45b74be98..2efe3e116 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -78,7 +78,7 @@ output_tables: # - accessibility # - land_use # - households -# - persons + - persons # - trips # - tours @@ -111,4 +111,17 @@ skim_time_periods: - MD - PM +# - value of time +# value_of_time = lognormal(np.log(median_value_of_time * mu), sigma).clip(min_vot, max_vot) + +min_value_of_time: 0 +max_value_of_time: 50 +distributed_vot_mu: 0.684 +distributed_vot_sigma: 0.85 + +household_median_value_of_time: + 1: 6 + 2: 8 + 3: 10 + 4: 12 diff --git a/example/configs/tour_mode_choice.yaml b/example/configs/tour_mode_choice.yaml index 66205f67d..22ad9d561 100644 --- a/example/configs/tour_mode_choice.yaml +++ b/example/configs/tour_mode_choice.yaml @@ -96,3 +96,4 @@ LOGSUM_CHOOSER_COLUMNS: - number_of_participants - tour_category - num_workers + - value_of_time diff --git a/example/configs/tour_mode_choice_annotate_choosers_preprocessor.csv b/example/configs/tour_mode_choice_annotate_choosers_preprocessor.csv index 10c5c5db0..e5f7c793c 100644 --- a/example/configs/tour_mode_choice_annotate_choosers_preprocessor.csv +++ b/example/configs/tour_mode_choice_annotate_choosers_preprocessor.csv @@ -14,8 +14,7 @@ local,_DF_IS_TOUR,'tour_type' in df.columns ,is_indiv,~is_joint ,is_atwork_subtour,(df.tour_category=='atwork') if 'tour_category' in df.columns else False #,, -,value_of_time,8 -,c_cost,(0.60 * c_ivt) / value_of_time +,c_cost,(0.60 * c_ivt) / df.value_of_time #,, ,dest_topology,"reindex(land_use.TOPOLOGY, df[dest_col_name])" ,terminal_time,"reindex(land_use.TERMINAL, df[dest_col_name])" diff --git a/example/configs/trip_mode_choice.yaml b/example/configs/trip_mode_choice.yaml index f1da48e36..302b1b563 100644 --- a/example/configs/trip_mode_choice.yaml +++ b/example/configs/trip_mode_choice.yaml @@ -121,4 +121,5 @@ TOURS_MERGED_CHOOSER_COLUMNS: - parent_tour_id - tour_mode - duration + - value_of_time diff --git a/example/configs/trip_mode_choice_annotate_trips_preprocessor.csv b/example/configs/trip_mode_choice_annotate_trips_preprocessor.csv index 381374e2c..84293eb68 100644 --- a/example/configs/trip_mode_choice_annotate_trips_preprocessor.csv +++ b/example/configs/trip_mode_choice_annotate_trips_preprocessor.csv @@ -2,8 +2,7 @@ Description,Target,Expression ,is_joint,(df.number_of_participants > 1) ,is_indiv,(df.number_of_participants == 1) ,is_atwork_subtour,~df.parent_tour_id.isnull() -,value_of_time,8 -,c_cost,(0.60 * c_ivt) / value_of_time +,c_cost,(0.60 * c_ivt) / df.value_of_time #,, #atwork subtours,, #FIXME tripModeChoice uec wrongly conflates these with tour_mode_is_bike?,, diff --git a/example_azure/benchmarks/benchmarks_full_shadow.txt b/example_azure/benchmarks/benchmarks_full_shadow.txt new file mode 100644 index 000000000..5e29bc424 --- /dev/null +++ b/example_azure/benchmarks/benchmarks_full_shadow.txt @@ -0,0 +1,41 @@ +------------ azure linux 432GB 64 processors + +households_sample_size: 0 +chunk_size: 80000000000 +num_processes: 60 +stagger: 0 + + +# 64 processor, 432 GiB RAM, 864 GiB SSD Temp, $4.011/hour +Standard_E64_v3 + +# just run models through workplace_location + +school_location : 937.825 seconds (15.6 minutes) +workplace_location : 2038.713 seconds (34.0 minutes) + + +MAX_ITERATIONS: 10 +SHADOW_PRICE_METHOD: daysim + +INFO - activitysim.core.mem - high water mark used: 96.55 timestamp: 21/12/2018 20:48:39 label: mp_households_44.school_location.completed +INFO - activitysim.core.mem - high water mark rss: 338.87 timestamp: 21/12/2018 20:48:39 label: mp_households_44.school_location.completed +INFO - activitysim.core.tracing - Time to execute everything : 3210.843 seconds (53.5 minutes) + +TAR_TAG=azure-64-ubuntu-shadow_daysim +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/sp_daysim_output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz + + +MAX_ITERATIONS: 10 +SHADOW_PRICE_METHOD: ctramp + + +INFO - activitysim.core.tracing - Time to execute run_sub_simulations step mp_summarize : 9.111 seconds (0.2 minutes) +INFO - activitysim.core.mem - high water mark used: 96.08 timestamp: 21/12/2018 21:49:13 label: mp_households_13.school_location.completed +INFO - activitysim.core.mem - high water mark rss: 337.22 timestamp: 21/12/2018 21:49:13 label: mp_households_13.school_location.completed +INFO - activitysim.core.tracing - Time to execute everything : 3212.896 seconds (53.5 minutes) + + + +school_location : 937.825 seconds (15.6 minutes) +workplace_location : 2038.713 seconds (34.0 minutes) diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 783b383eb..8b80b22a1 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -22,6 +22,7 @@ VM_IP=$(az vm list-ip-addresses -n $AZ_VM_NAME --query [0].virtualMachine.networ # copy settings to example_mp/configs scp example_mp/configs/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml +scp example_mp/configs/shadow_pricing.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/shadow_pricing.yaml #scp example_mp/configs/logging.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/logging.yaml scp example_mp/configs/school_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/school_location.yaml scp example_mp/configs/workplace_location.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/workplace_location.yaml @@ -48,6 +49,10 @@ source activate asim #cp output/final_school_shadow_prices.csv /datadrive/work/data/full/school_shadow_prices.csv #cp output/final_workplace_shadow_prices.csv /datadrive/work/data/full/workplace_shadow_prices.csv +# - delete shadow prices from data dir +#rm /datadrive/work/data/full/school_shadow_prices.csv +#rm /datadrive/work/data/full/workplace_shadow_prices.csv + cd /datadrive/work/activitysim/example_mp #nano configs/settings.yaml @@ -65,8 +70,8 @@ tar zcvf output.tar.gz output/log/ output/trace/ exit -TAR_TAG=azure-64-ubuntu-shadow2 -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz +TAR_TAG=azure-64-ubuntu-shadow_ctramp +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/sp_ctramp_output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz ############### diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 00b2de67b..dbecea41d 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -121,10 +121,10 @@ output_tables: # - tours - school_shadow_prices - raw_school_destination_size - - scaled_school_destination_size + - school_destination_size - school_modeled_size - workplace_shadow_prices - - raw_work_destination_size - - scaled_work_destination_size + - raw_workplace_destination_size + - workplace_destination_size - workplace_modeled_size diff --git a/example_mp/configs/shadow_pricing.yaml b/example_mp/configs/shadow_pricing.yaml index abc859d83..290a260e4 100644 --- a/example_mp/configs/shadow_pricing.yaml +++ b/example_mp/configs/shadow_pricing.yaml @@ -6,16 +6,16 @@ inherit_settings: True LOAD_SAVED_SHADOW_PRICES: True # number of shadow price iterations for cold start -MAX_ITERATIONS: 5 +MAX_ITERATIONS: 10 # number of shadow price iterations for warm start (after loading saved shadow_prices) MAX_ITERATIONS_SAVED: 1 # ignore criteria for zones smaller than size_threshold -SIZE_THRESHOLD: 10 +SIZE_THRESHOLD: 50 # zone passes if modeled is within percent_tolerance of predicted_size -PERCENT_TOLERANCE: 5 +PERCENT_TOLERANCE: 10 # max percentage of zones allowed to fail FAIL_THRESHOLD: 10 From b1e7a725d2806447cf6940f08e111a39147d6eab Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 28 Dec 2018 15:29:11 -0500 Subject: [PATCH 064/122] off by one iteration number in location_choice --- activitysim/abm/models/location_choice.py | 2 +- .../benchmarks/benchmarks_full_shadow.txt | 11 +++ example_azure/example_mp/settings.yaml | 72 +++++++++++++++---- example_mp/configs/settings.yaml | 7 +- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index 782f78274..bd7567b2a 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -330,7 +330,7 @@ def iterate_location_choice( choices = None for iteration in range(1, max_iterations + 1): - if spc.use_shadow_pricing and iteration > 0: + if spc.use_shadow_pricing and iteration > 1: spc.update_shadow_prices() choices = run_location_choice( diff --git a/example_azure/benchmarks/benchmarks_full_shadow.txt b/example_azure/benchmarks/benchmarks_full_shadow.txt index 5e29bc424..58f47cb87 100644 --- a/example_azure/benchmarks/benchmarks_full_shadow.txt +++ b/example_azure/benchmarks/benchmarks_full_shadow.txt @@ -39,3 +39,14 @@ INFO - activitysim.core.tracing - Time to execute everything : 3212.896 seconds school_location : 937.825 seconds (15.6 minutes) workplace_location : 2038.713 seconds (34.0 minutes) + + +########## + + +# - copy shadow prices to data dir +scp ~/work/activitysim/example_azure/output_ubuntu/azure-64-ubuntu-shadow_daysim-output/trace/shadow_price_school_shadow_prices_9.csv $AZ_USERNAME@$VM_IP:/datadrive/work/data/full/school_shadow_prices.csv +scp ~/work/activitysim/example_azure/output_ubuntu/azure-64-ubuntu-shadow_daysim-output/trace/shadow_price_workplace_shadow_prices_9.csv $AZ_USERNAME@$VM_IP:/datadrive/work/data/full/workplace_shadow_prices.csv + + +scp ~/work/activitysim/example_azure/example_mp/settings.yaml $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/configs/settings.yaml diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml index a074ecbfc..35e95014f 100644 --- a/example_azure/example_mp/settings.yaml +++ b/example_azure/example_mp/settings.yaml @@ -1,39 +1,72 @@ inherit_settings: True -# - production config +# raise error if any sub-process fails without waiting for others to complete +fail_fast: True +# - ------------------------- production config multiprocess: True -mem_tick: 0 profile: False strict: False +mem_tick: 0 +use_shadow_pricing: True + -# Standard_E64s_v3 +# - full sample - 2732722 households on 64 processor 432 GiB RAM #households_sample_size: 0 -#chunk_size: 90000000000 +#chunk_size: 80000000000 #num_processes: 60 -#stagger: 5 +#stagger: 0 + +# - full sample - 2732722 households on Standard_M128s +#households_sample_size: 0 +#chunk_size: 0 +#num_processes: 124 +#stagger: 0 -# Standard_M128s -households_sample_size: 0 -chunk_size: 0 -num_processes: 120 +# - 20% sample - 2732722 households on 64 processor 432 GiB RAM +households_sample_size: 546544 +chunk_size: 20000000000 +num_processes: 15 stagger: 0 +## - ------------------------- dev config +#multiprocess: True +#profile: False +#strict: False +#mem_tick: 30 +#use_shadow_pricing: True +# +## - small sample +#households_sample_size: 5000 +#chunk_size: 500000000 +#num_processes: 2 +#stagger: 5 +# +# - stride sample +#households_sample_size: 0 +#chunk_size: 1000000000 +#num_processes: 1 +#households_sample_stride: [120,0] + # - tracing trace_hh_id: trace_od: +#trace_hh_id: 1482966 +#trace_od: [5, 11] +# to resume after last successful checkpoint, specify resume_after: _ +#resume_after: trip_purpose_and_destination models: + ### mp_initialize step - initialize_landuse - compute_accessibility - initialize_households + ### mp_households step - school_location - - _workplace_location_sample - - _workplace_location_logsums - - workplace_location_simulate + - workplace_location - auto_ownership_simulate - cdap_simulate - mandatory_tour_frequency @@ -61,6 +94,7 @@ models: - trip_purpose_and_destination - trip_scheduling - trip_mode_choice + ### mp_summarize step - write_data_dictionary - write_tables @@ -69,6 +103,9 @@ multiprocess_steps: begin: initialize_landuse - name: mp_households begin: school_location + #num_processes: 9 + #stagger: 30 + #chunk_size: 1000000000 slice: tables: - households @@ -76,9 +113,18 @@ multiprocess_steps: - name: mp_summarize begin: write_data_dictionary + output_tables: action: include prefix: final_ tables: - - checkpoints +# - checkpoints +# - accessibility +# - land_use +# - households +# - persons +# - trips +# - tours + - school_shadow_prices + - workplace_shadow_prices diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index dbecea41d..ea3551e77 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -120,11 +120,6 @@ output_tables: # - trips # - tours - school_shadow_prices - - raw_school_destination_size - - school_destination_size - - school_modeled_size - workplace_shadow_prices - - raw_workplace_destination_size - - workplace_destination_size - - workplace_modeled_size + From a7aa3050b8cb7fe8295cffbba16b39fb48501899 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 4 Jan 2019 11:56:54 -0500 Subject: [PATCH 065/122] ShadowPriceCalculator docstrings --- .../abm/models/joint_tour_participation.py | 2 +- activitysim/abm/tables/shadow_pricing.py | 356 +++++++++++++++--- activitysim/core/mp_tasks.py | 2 +- example_azure/example_mp/settings.yaml | 8 +- example_azure/ubuntu/step3_run_asim.txt | 4 +- 5 files changed, 315 insertions(+), 57 deletions(-) diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index ccc6d7724..2cec16278 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -310,7 +310,7 @@ def joint_tour_participation( pipeline.replace_table("joint_tour_participants", participants) # drop channel as we aren't using any more (and it has candidates that weren't chosen) - pipeline.get_rn_generator().drop_channel('joint_tours_participants') + pipeline.get_rn_generator().drop_channel('joint_tour_participants') # - assign joint tour 'point person' (participant_num == 1) point_persons = participants[participants.participant_num == 1] diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 64b17d6b5..755c41ed0 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -11,7 +11,6 @@ import time import multiprocessing import ctypes -import os from collections import OrderedDict @@ -28,28 +27,71 @@ logger = logging.getLogger(__name__) -# - reverse semaphores to synchronize concurrent access to shared data buffer -# we use the first two rows of the final column in numpy-wrapped shared data as 'reverse semaphores' -# (synchronize concurrent access to shared data resource rather than throttling access) +""" +ShadowPriceCalculator and associated utility methods + +See docstrings for documentation on: + +update_shadow_prices how shadow_price coefficients are calculated +synchronize_choices interprocess communication to compute aggregate modeled_size +check_fit convergence criteria for shadow_pric iteration + +""" + + +""" +Artisanal reverse semaphores to synchronize concurrent access to shared data buffer + +we use the first two rows of the final column in numpy-wrapped shared data as 'reverse semaphores' +(they synchronize concurrent access to shared data resource rather than throttling access) + +ShadowPriceCalculator.synchronize_choices coordinates access to the global aggregate zone counts +(local_modeled_size summed across all sub-processes) using these two semaphores +(which are really only tuples of indexes of locations in the shared data array. +""" TALLY_CHECKIN = (0, -1) TALLY_CHECKOUT = (1, -1) -def size_table_name(selector, scaled=True): - if scaled: - table_name = "%s_destination_size" % selector - else: - table_name = "raw_%s_destination_size" % selector - return table_name +def size_table_name(selector): + """ + Returns canonical destination size table name + + Parameters + ---------- + selector : str + e.g. school or workplace + Returns + ------- + table_name : str + """ + return "%s_destination_size" % selector -def get_size_table(selector, scaled=True): - return inject.get_table(size_table_name(selector, scaled)).to_frame() + +def get_size_table(selector): + return inject.get_table(size_table_name(selector)).to_frame() class ShadowPriceCalculator(object): def __init__(self, model_settings, shared_data=None, shared_data_lock=None): + """ + + Presence of shared_data is used as a flag for multiprocessing + If we are multiprocessing, shared_data should be a multiprocessing.RawArray buffer + to aggregate modeled_size across all sub-processes, and shared_data_lock should be + a multiprocessing.Lock object to coordinate access to that buffer. + + Optionally load saved shadow_prices from data_dir if config setting use_shadow_pricing + and shadow_setting LOAD_SAVED_SHADOW_PRICES are both True + + Parameters + ---------- + model_settings : dict + shared_data : multiprocessing.Array or None (if single process) + shared_data_lock : numpy array wrapping multiprocessing.RawArray or None (if single process) + """ self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) self.saved_shadow_price_file_path = None # set by read_saved_shadow_prices if loaded @@ -69,7 +111,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.shadow_settings = config.read_model_settings('shadow_pricing.yaml') # - destination_size_table (predicted_size) - self.predicted_size = get_size_table(self.selector, scaled=True) + self.predicted_size = get_size_table(self.selector) # - shared_data if shared_data is not None: @@ -93,7 +135,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): else: self.max_iterations = 1 - # - if we did't load saved shadow_prices, initialize shadow_prices to all ones + # - if we did't load saved shadow_prices, initialize all shadow_prices to one # this will start first iteration with no shadow price adjustment, if self.shadow_prices is None: self.shadow_prices = \ @@ -106,6 +148,18 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.max_rel_diff = pd.DataFrame(index=self.predicted_size.columns) def read_saved_shadow_prices(self, model_settings): + """ + Read saved shadow_prices from csv file in data_dir (so-called warm start) + returns None if no saved shadow price file name specified or named file not found + + Parameters + ---------- + model_settings : dict + + Returns + ------- + shadow_prices : pandas.DataFrame or None + """ shadow_prices = None @@ -124,11 +178,46 @@ def read_saved_shadow_prices(self, model_settings): return shadow_prices def synchronize_choices(self, local_modeled_size): + """ + We have to wait until all processes have computed choices and aggregated them by segment + and zone before we can compute global aggregate zone counts (by segment). Since the global + zone counts are in shared data, we have to coordinate access to the data structure across + sub-processes. - if self.shared_data is None: - return local_modeled_size + Note that all access to self.shared_data has to be protected by acquiring shared_data_lock + + ShadowPriceCalculator.synchronize_choices coordinates access to the global aggregate + zone counts (local_modeled_size summed across all sub-processes). + + * All processes wait (in case we are iterating) until any stragglers from the previous + iteration have exited the building. (TALLY_CHECKOUT goes to zero) + + * Processes then add their local counts into the shared_data and increment TALLY_CHECKIN + + * All processes wait until everybody has checked in (TALLY_CHECKIN == num_processes) + + * Processes make local copy of shared_data and check out (increment TALLY_CHECKOUT) + + * first_in process waits until all processes have checked out, then zeros shared_data + and clears semaphores + + Parameters + ---------- + local_modeled_size : pandas DataFrame + + + Returns + ------- + global_modeled_size_df : pandas DataFrame + local copy of shared global_modeled_size data as dataframe + with same shape and columns as local_modeled_size + """ + + # shouldn't be called if we are not multiprocessing + assert self.shared_data is not None num_processes = inject.get_injectable("num_processes") + assert num_processes > 1 def get_tally(t): with self.shared_data_lock: @@ -141,7 +230,7 @@ def wait(tally, target, tally_name): # - nobody checks in until checkout clears wait(TALLY_CHECKOUT, 0, 'TALLY_CHECKOUT') - # - add local_modeled_size data + # - add local_modeled_size data, increment TALLY_CHECKIN with self.shared_data_lock: first_in = self.shared_data[TALLY_CHECKIN] == 0 # add local data from df to shared data buffer @@ -152,7 +241,7 @@ def wait(tally, target, tally_name): # - wait until everybody else has checked in wait(TALLY_CHECKIN, num_processes, 'TALLY_CHECKIN') - # - copy shared data and check out + # - copy shared data, increment TALLY_CHECKIN with self.shared_data_lock: logger.info("copy shared_data") # numpy array with sum of local_modeled_size.values from all processes @@ -163,15 +252,32 @@ def wait(tally, target, tally_name): if first_in: wait(TALLY_CHECKOUT, num_processes, 'TALLY_CHECKOUT') with self.shared_data_lock: + # zero shared_data, clear TALLY_CHECKIN, and TALLY_CHECKOUT semaphores self.shared_data[:] = 0 logger.info("first_in clearing shared_data") # convert summed numpy array data to conform to original dataframe - return pd.DataFrame(data=global_modeled_size_array, - index=local_modeled_size.index, - columns=local_modeled_size.columns) + global_modeled_size_df = \ + pd.DataFrame(data=global_modeled_size_array, + index=local_modeled_size.index, + columns=local_modeled_size.columns) + + return global_modeled_size_df def set_choices(self, choices_df): + """ + aggregate individual location choices to modeled_size by zone and segment + + Parameters + ---------- + choices_df : pandas.DataFrame + dataframe with disaggregate location choices and at least two columns: + 'segment_id' : segment id tag for this individual + 'dest_choice' : zone id of location choice + Returns + ------- + updates self.modeled_size + """ assert 'dest_choice' in choices_df @@ -182,7 +288,12 @@ def set_choices(self, choices_df): modeled_size[c] = segment_choices.groupby('dest_choice').size() modeled_size = modeled_size.fillna(0).astype(int) - self.modeled_size = self.synchronize_choices(modeled_size) + if self.shared_data is None: + # - not multiprocessing + self.modeled_size = modeled_size + else: + # - if we are multiprocessing, we have to aggregate across sub-processes + self.modeled_size = self.synchronize_choices(modeled_size) def check_fit(self, iteration): """ @@ -201,6 +312,8 @@ def check_fit(self, iteration): """ + #fixme + if not self.use_shadow_pricing: return False @@ -259,6 +372,33 @@ def check_fit(self, iteration): return converged def update_shadow_prices(self): + """ + Adjust shadow_prices based on relative values of modeled_size and predicted_size. + + This is the heart of the shadow pricing algorithm. + + The presumption is that shadow_price_adjusted_predicted_size (along with other attractors) + is being used in a utility expression in a location choice model. The goal is to get the + aggregate location modeled size (choice aggregated by selector segment and zone) to match + predicted_size. Since the location choice model may not achieve that goal initially, we + create a 'shadow price' that tweaks the size_term to encourage the aggregate choices to + approach the desired target predicted_sizes. + + shadow_prices is a table of coefficient (for each zone and segment) that is increases or + decreases the size term according to whether the modelled population is less or greater + than the predicted_size. If too few total choices are made for a particular zone and + segment, then its shadow_price is increased, if too many, then it is decreased. + + Since the location choice is being made according to a variety of utilities in the + expression file, whose relative weights are unknown to this algorithm, the choice of + how to adjust the shadow_price is not completely straightforward. CTRAMP and daysim use + different strategies (see below) and there may not be a single method that works best for + all expression files. This would be a nice project for the mathematically inclined. + + Returns + ------- + updates self.shadow_prices + """ assert self.use_shadow_pricing @@ -330,7 +470,7 @@ def update_shadow_prices(self): new_shadow_prices = self.shadow_prices + adjustment else: - raise RuntimeError("unknown SHADOW_PRICE_METHOD %s" % self.shadow_price_method) + raise RuntimeError("unknown SHADOW_PRICE_METHOD %s" % shadow_price_method) # print("\nself.predicted_size\n", self.predicted_size.head()) # print("\nself.modeled_size\n", self.modeled_size.head()) @@ -340,15 +480,36 @@ def update_shadow_prices(self): self.shadow_prices = new_shadow_prices def shadow_price_adjusted_predicted_size(self): + """ + return predicted_sizes adjusted by current shadow_price for use in utility expressions + + Returns + ------- + pandas.DataFrame with same shape as predicted_size + """ return self.predicted_size * self.shadow_prices def write_trace_files(self, iteration): + """ + Write trace files for this iteration + Writes predicted_size, modeled_size, and shadow_prices tables + + Trace file names are tagged with selector and iteration number + (e.g. self.predicted_size => shadow_price_school_predicted_size_1) + + Parameters + ---------- + iteration: int + current iteration to tag trace file + """ logger.info("write_trace_files iteration %s" % iteration) if iteration == 1: + # write predicted_size only on first iteration, as it doesn't change tracing.write_csv(self.predicted_size, - 'shadow_price_%s_predicted_size_%s' % (self.selector, iteration), + 'shadow_price_%s_predicted_size' % self.selector, transpose=False) + tracing.write_csv(self.modeled_size, 'shadow_price_%s_modeled_size_%s' % (self.selector, iteration), transpose=False) @@ -358,10 +519,38 @@ def write_trace_files(self, iteration): def block_name(selector): + """ + return canonical block name for selector + + Ordinarilly and ideally this wold just be selector, but since mp_tasks saves all shared data + blocks in a common dict to pass to sub-tasks, we want to be able to handle an possible + collision between selector names and skim names. Otherwise, just use selector name. + + Parameters + ---------- + selector + + Returns + ------- + block_name : str + canonical block name + """ return selector def get_shadow_pricing_info(): + """ + return dict with info about dtype and shapes of predicted and modeled size tables + + block shape is (num_zones, num_segments + 1) + + + Returns + ------- + shadow_pricing_info: dict + 'dtype': , + 'block_shapes': dict {: } + """ land_use = inject.get_table('land_use') size_terms = inject.get_injectable('size_terms') @@ -377,28 +566,46 @@ def get_shadow_pricing_info(): sp_rows = len(land_use) sp_cols = len(size_terms[size_terms.selector == selector]) - # extra tally column + # extra tally column for TALLY_CHECKIN and TALLY_CHECKOUT semaphores blocks[block_name(selector)] = (sp_rows, sp_cols + 1) sp_dtype = np.int64 shadow_pricing_info = { 'dtype': sp_dtype, - 'blocks': blocks, + 'block_shapes': blocks, } return shadow_pricing_info -def buffers_for_shadow_pricing(shadow_pricing_info, shared=False): +def buffers_for_shadow_pricing(shadow_pricing_info): + """ + Allocate shared_data buffers for multiprocess shadow pricing + + Allocates one buffer per selector. Buffer datatype and shape specified by shadow_pricing_info + + buffers are multiprocessing.Array (RawArray protected by a multiprocessing.Lock wrapper) + We don't actually use the wrapped version as it slows access down and doesn't provide + protection for numpy-wrapped arrays, but it does provide a convenient way to bundle + RawArray and an associated lock. (ShadowPriceCalculator uses the lock to coordinate access to + the numpy-wrapped RawArray.) + + Parameters + ---------- + shadow_pricing_info : dict - assert shared + Returns + ------- + data_buffers : dict { : } + dict of multiprocessing.Array keyed by selector + """ dtype = shadow_pricing_info['dtype'] - blocks = shadow_pricing_info['blocks'] + block_shapes = shadow_pricing_info['block_shapes'] data_buffers = {} - for block_key, block_shape in iteritems(blocks): + for block_key, block_shape in iteritems(block_shapes): # buffer_size must be int (or p2.7 long), not np.int64 buffer_size = int(np.prod(block_shape)) @@ -412,39 +619,78 @@ def buffers_for_shadow_pricing(shadow_pricing_info, shared=False): else: raise RuntimeError("buffer_for_shadow_pricing unrecognized dtype %s" % dtype) - buffer = multiprocessing.Array(typecode, buffer_size) + shared_data_buffer = multiprocessing.Array(typecode, buffer_size) logger.info("buffer_for_shadow_pricing added block %s" % (block_key)) - data_buffers[block_key] = buffer + data_buffers[block_key] = shared_data_buffer return data_buffers def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): + """ + + Parameters + ---------- + data_buffers : dict of { : } + multiprocessing.Array is simply a convenient way to bundle Array and Lock + we extract the lock and wrap the RawArray in a numpy array for convenience in indexing + + The shared data buffer has shape ( + 1) + extra column is for reverse semaphores with TALLY_CHECKIN and TALLY_CHECKOUT + shadow_pricing_info : dict + dict of useful info + 'dtype': sp_dtype, + 'block_shapes' : OrderedDict({: }) + dict mapping selector to block shape (including extra column for semaphores) + e.g. {'school': (num_zones, num_segments + 1) + selector : str + location type selector (e.g. school or workplace) + + Returns + ------- + shared_data, shared_data_lock + shared_data : multiprocessing.Array or None (if single process) + shared_data_lock : numpy array wrapping multiprocessing.RawArray or None (if single process) + """ assert type(data_buffers) == dict dtype = shadow_pricing_info['dtype'] - blocks = shadow_pricing_info['blocks'] + block_shapes = shadow_pricing_info['block_shapes'] - if selector not in blocks: + if selector not in block_shapes: raise RuntimeError("Selector %s not in shadow_pricing_info" % selector) if block_name(selector) not in data_buffers: raise RuntimeError("Block %s not in data_buffers" % block_name(selector)) - shape = blocks[selector] + shape = block_shapes[selector] data = data_buffers[block_name(selector)] return np.frombuffer(data.get_obj(), dtype=dtype).reshape(shape), data.get_lock() def load_shadow_price_calculator(model_settings): + """ + Initialize ShadowPriceCalculator for model selector (e.g. school or workplace) + + If multiprocessing, get the shared_data buffer to coordinate global_predicted_size + calculation across sub-processes + + Parameters + ---------- + model_settings : dict + + Returns + ------- + spc : ShadowPriceCalculator + """ selector = model_settings['SELECTOR'] - # - data_buffers (if shared data) + # - get shared_data from data_buffers (if multiprocessing) data_buffers = inject.get_injectable('data_buffers', None) if data_buffers is not None: logger.info('Using existing data_buffers for shadow_price') @@ -470,6 +716,24 @@ def load_shadow_price_calculator(model_settings): def add_predicted_size_tables(): + """ + inject tour_destination_size_terms tables for each selector (e.g. school, workplace) + + Size tables are pandas dataframes with locations counts for selector by zone and segment + tour_destination_size_terms + + if using shadow pricing, we scale size_table counts to sample population + (in which case, they have to be created while single-process) + + Scaling is problematic as it breaks household result replicability across sample sizes + It also changes the magnitude of the size terms so if they are used as utilities in + expression files, their importance will diminish relative to other utilities as the sample + size decreases. + + Scaling makes most sense for a full sample in conjunction with shadow pricing, where + shadow prices can be adjusted iteratively to bring modelled counts into line with predicted + (size table) counts. + """ use_shadow_pricing = bool(config.setting('use_shadow_pricing')) @@ -504,14 +768,12 @@ def add_predicted_size_tables(): raw_size = tour_destination_size_terms(land_use, size_terms, selector).astype(np.float64) assert set(raw_size.columns) == set(segment_ids.keys()) - # this is just informational - for debugging shadow pricing - inject.add_table(size_table_name(selector, scaled=False), raw_size) - if use_shadow_pricing: - # - segment scale factor (modeled / predicted) keyed by segment_name - # scaling reconciles differences between synthetic population and zone demographics - # in a partial sample, it also scales predicted_size targets to sample population + # - scale size_table counts to sample population + # scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + + # segment scale factor (modeled / predicted) keyed by segment_name segment_scale_factors = {} for c in raw_size: # number of zone demographics predicted destination choices @@ -531,14 +793,10 @@ def add_predicted_size_tables(): segment_chooser_count, segment_scale_factors[c])) - # segment_scale_factors[c] = \ - # segment_chooser_counts[c] / np.maximum(segment_predicted_size, 1) - - # - scaled_size = zone_size * (total_segment_modeled / total_segment_predicted) + # FIXME - should we be rounding? scaled_size = (raw_size * segment_scale_factors).round() - else: # don't scale if not shadow_pricing (breaks partial sample replicability) - scaled_size = raw_size.copy() + scaled_size = raw_size - inject.add_table(size_table_name(selector, scaled=True), scaled_size) + inject.add_table(size_table_name(selector), scaled_size) diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 69b12dcfb..9a7158d8c 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -351,7 +351,7 @@ def allocate_shared_shadow_pricing_buffers(): logger.info("allocate_shared_shadow_pricing_buffers") shadow_pricing_info = get_shadow_pricing_info() - shadow_pricing_buffers = buffers_for_shadow_pricing(shadow_pricing_info, shared=True) + shadow_pricing_buffers = buffers_for_shadow_pricing(shadow_pricing_info) return shadow_pricing_buffers diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml index 35e95014f..005da9314 100644 --- a/example_azure/example_mp/settings.yaml +++ b/example_azure/example_mp/settings.yaml @@ -25,10 +25,10 @@ use_shadow_pricing: True #stagger: 0 # - 20% sample - 2732722 households on 64 processor 432 GiB RAM -households_sample_size: 546544 -chunk_size: 20000000000 -num_processes: 15 -stagger: 0 +#households_sample_size: 546544 +#chunk_size: 10000000000 +#num_processes: 15 +#stagger: 0 ## - ------------------------- dev config diff --git a/example_azure/ubuntu/step3_run_asim.txt b/example_azure/ubuntu/step3_run_asim.txt index 8b80b22a1..6009558d8 100644 --- a/example_azure/ubuntu/step3_run_asim.txt +++ b/example_azure/ubuntu/step3_run_asim.txt @@ -70,8 +70,8 @@ tar zcvf output.tar.gz output/log/ output/trace/ exit -TAR_TAG=azure-64-ubuntu-shadow_ctramp -scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/sp_ctramp_output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz +TAR_TAG=azure-64-ubuntu-shadow_daysim_warm +scp $AZ_USERNAME@$VM_IP:/datadrive/work/activitysim/example_mp/output.tar.gz example_azure/output_ubuntu/$TAR_TAG-output.tar.gz ############### From d1d369716d999784c964756170e3228d445043bb Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 4 Jan 2019 13:45:13 -0500 Subject: [PATCH 066/122] pycodestyle --- activitysim/abm/tables/shadow_pricing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 755c41ed0..9c3f5192a 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -312,7 +312,7 @@ def check_fit(self, iteration): """ - #fixme + # fixme if not self.use_shadow_pricing: return False From cd972e1ee70deb8bf962c2f1fdd73712cc757087 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Mon, 7 Jan 2019 15:59:02 -0500 Subject: [PATCH 067/122] location_choice docstrings and rename selector model_selector to avoid confusion with segment in shadow_pricing --- activitysim/abm/models/location_choice.py | 143 ++++++++++++++++-- activitysim/abm/tables/shadow_pricing.py | 139 +++++++++-------- activitysim/abm/tables/size_terms.py | 19 +-- .../configs/destination_choice_size_terms.csv | 2 +- .../abm/test/configs/school_location.yaml | 4 +- .../abm/test/configs/shadow_pricing.yaml | 4 + .../abm/test/configs/workplace_location.yaml | 4 +- activitysim/core/interaction_sample.py | 2 +- activitysim/core/mp_tasks.py | 23 ++- .../configs/destination_choice_size_terms.csv | 2 +- example/configs/school_location.yaml | 4 +- example/configs/workplace_location.yaml | 4 +- 12 files changed, 244 insertions(+), 106 deletions(-) diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index bd7567b2a..b5629ddf6 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -7,8 +7,6 @@ from future.utils import iteritems -from collections import OrderedDict - import logging import pandas as pd @@ -31,12 +29,70 @@ """ The school/workplace location model predicts the zones in which various people will work or attend school. + +For locations choices like workplace and school location, we have existing data about the actual +number of workers or students in the various destination zones, and we naturally want the results +of location choice to yield distributions the match these observed distributions as closely as +possible. To achieve this, we use start with size tables with the observed populations by zone +and segment (e.g. number of university, highschool, and gradeschool students in each zone) and +use those populations as attractors (positive utilities) so that high population zones will, +all things being equal, receive more choices. (For instance, we want university-goers to choose +school locations with in zones with university enrollments.) + +But since the choice algorithm can result in aggregate distributions of choices (modeled_size) +that don't match observed (predicted_size) counts. The shadow pricing algorithm attempts to +correct these misalignments, by iteratively running the choice model, comparing the modeled_size +of the zones segments to the predicted size, and computing a shadow_price coefficient that is +applied to the size term to boost or attenuate its influence. This iterative process can be +configures to continue until a specified closeness of fit is achieved, or a maximum number of +iterations has occurred. Since the iterative process can be expensive, a facility is provided +to save the computed shadow prices after every iteration, and to load pre-computed shadow prices +on subsequent runs (warm start) to cut down on runtimes. + +Since every individual (always person for now) belongs to at most one segment, each segment +(e.g. 'university', 'highschool' , 'gradeschool' for the 'school' location model) is handled +separately and sequentially withing each shadow-price iteration. + +The core algorithm has 3 parts: + +Because logsum calculations are expensive, rather than computing logsums for all destination +alternatives, we first build a sample of alternatives using simplified (no-logsum) utilities, +and compute logsums only for that sample, and finally chose from among the sampled alternatives. + +* run_location_sample - Build a sample destination alternatives using simplified choice criteria +* run_location_logsums - Compute logsums for travel to those alternatives +* run_location_simulate - Rerun the choice model using the logsums to make a final location choice + +With shadow pricing, and iterative treatment of each segment, the structure of the code is: + +:: + repeat + for each segment + run_location_sample + run_location_logsums + run_location_simulate + until convergence """ logger = logging.getLogger(__name__) def spec_for_segment(model_spec, segment_name): + """ + Select spec for specified segment from omnibus spec containing columns for each segment + + Parameters + ---------- + model_spec : pandas.DataFrame + omnibus spec file with expressions in index and one column per segment + segment_name : str + segment_name that is also column name in model_spec + + Returns + ------- + pandas.dataframe + canonical spec file with expressions in index and single column with utility coefficients + """ spec = model_spec[[segment_name]] @@ -49,20 +105,22 @@ def spec_for_segment(model_spec, segment_name): return spec -# we want to iterate over segment_ids in the same order every time -def order_dict_by_keys(segment_ids): - return OrderedDict([(k, segment_ids[k]) for k in sorted(segment_ids.keys())]) - - def run_location_sample( segment_name, persons_merged, skim_dict, dest_size_terms, model_settings, - chunk_size, trace_hh_id, trace_label): + chunk_size, trace_label): """ - build a table of persons * all zones in order to select a sample of alternative locations. + select a sample of alternative locations. + + Logsum calculations are expensive, so we build a table of persons * all zones + and then select a sample subset of potential locations + + The sample subset is generated by making multiple choices ( number of choices) + which results in sample containing up to choices for each choose (e.g. person) + and a pick_count indicating how many times that choice was selected for that chooser.) person_id, dest_TAZ, rand, pick_count 23750, 14, 0.565502716034, 4 @@ -185,7 +243,7 @@ def run_location_simulate( skim_dict, dest_size_terms, model_settings, - chunk_size, trace_hh_id, trace_label): + chunk_size, trace_label): """ run location model on location_sample annotated with mode_choice logsum to select a dest zone from sample alternatives @@ -207,7 +265,7 @@ def run_location_simulate( pd.merge(location_sample_df, dest_size_terms, left_on=alt_dest_col_name, right_index=True, how="left") - logger.info("Running location_simulate with %d persons" % len(choosers)) + logger.info("Running %s with %d persons" % (trace_label, len(choosers))) # create wrapper with keys for this lookup - in this case there is a TAZ in the choosers # and a TAZ in the alternatives which get merged during interaction @@ -243,12 +301,34 @@ def run_location_choice( model_settings, chunk_size, trace_hh_id, trace_label ): + """ + Run the three-part location choice algorithm to generate a location choice for each chooser + + Handle the various segments separately and in turn for simplicity of expression files + + Parameters + ---------- + persons_merged_df : pandas.DataFrame + persons table merged with households and land_use + skim_dict : skim.SkimDict + skim_stack : skim.SkimStack + dest_size_terms : pandas.DataFrame + shadow-price adjusted size terms with one row per zone and once column per segment + model_settings : dict + chunk_size : int + trace_hh_id : int + trace_label : str + + Returns + ------- + pandas.Series + location choices (zone ids) indexed by persons_merged_df.index + """ chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN_NAME'] - segment_ids = model_settings['SEGMENT_IDS'] - # we want to iterate over segment_ids in the same order for replicability - segment_ids = order_dict_by_keys(model_settings['SEGMENT_IDS']) + # maps segment names to compact (integer) ids + segment_ids = model_settings['SEGMENT_IDS'] choices_list = [] for segment_name, segment_id in iteritems(segment_ids): @@ -268,7 +348,6 @@ def run_location_choice( dest_size_terms, model_settings, chunk_size, - trace_hh_id, tracing.extend_trace_label(trace_label, 'sample.%s' % segment_name)) # - location_logsums @@ -293,7 +372,6 @@ def run_location_choice( dest_size_terms, model_settings, chunk_size, - trace_hh_id, tracing.extend_trace_label(trace_label, 'simulate.%s' % segment_name)) choices_list.append(choices) @@ -311,6 +389,29 @@ def iterate_location_choice( skim_dict, skim_stack, chunk_size, trace_hh_id, locutor, trace_label): + """ + iterate run_location_choice updating shadow pricing until convergence criteria satisfied + or max_iterations reached. + + (If use_shadow_pricing not enabled, then just iterate once) + + Parameters + ---------- + model_settings : dict + persons_merged : injected table + persons : injected table + skim_dict : skim.SkimDict + skim_stack : skim.SkimStack + chunk_size : int + trace_hh_id : int + locutor : bool + whether this process is the privileged logger of shadow_pricing when multiprocessing + trace_label : str + + Returns + ------- + adds choice column model_settings['DEST_CHOICE_COLUMN_NAME'] and annotations to persons table + """ # column containing segment id chooser_segment_column = model_settings['CHOOSER_SEGMENT_COLUMN_NAME'] @@ -393,6 +494,11 @@ def workplace_location( persons_merged, persons, skim_dict, skim_stack, chunk_size, trace_hh_id, locutor): + """ + workplace location choice model + + iterate_location_choice adds location choice column and annotations to persons table + """ trace_label = 'workplace_location' model_settings = config.read_model_settings('workplace_location.yaml') @@ -411,6 +517,11 @@ def school_location( skim_dict, skim_stack, chunk_size, trace_hh_id, locutor ): + """ + School location choice model + + iterate_location_choice adds location choice column and annotations to persons table + """ trace_label = 'school_location' model_settings = config.read_model_settings('school_location.yaml') diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 9c3f5192a..0ed307d2f 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -36,6 +36,20 @@ synchronize_choices interprocess communication to compute aggregate modeled_size check_fit convergence criteria for shadow_pric iteration +Import concepts and variables: + +model_selector: str + Identifies a specific location choice model (e.g. 'school', 'workplace') + The various models work similarly, but use different expression files, model settings, etc. + +segment: str + Identifies a specific demographic segment of a model (e.g. 'elementary' segment of 'school') + Models can have different size term coefficients (in destinatin_choice_size_terms file) and + different utility coefficients in models's location and location_sample csv expression files + +size_table: pandas.DataFrame + + """ @@ -53,24 +67,20 @@ TALLY_CHECKOUT = (1, -1) -def size_table_name(selector): +def size_table_name(model_selector): """ - Returns canonical destination size table name + Returns canonical name of injected destination predicted_size table Parameters ---------- - selector : str + model_selector : str e.g. school or workplace Returns ------- table_name : str """ - return "%s_destination_size" % selector - - -def get_size_table(selector): - return inject.get_table(size_table_name(selector)).to_frame() + return "%s_destination_size" % model_selector class ShadowPriceCalculator(object): @@ -96,7 +106,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) self.saved_shadow_price_file_path = None # set by read_saved_shadow_prices if loaded - self.selector = model_settings['SELECTOR'] + self.model_selector = model_settings['MODEL_SELECTOR'] full_model_run = config.setting('households_sample_size') == 0 if self.use_shadow_pricing and not full_model_run: @@ -111,7 +121,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): self.shadow_settings = config.read_model_settings('shadow_pricing.yaml') # - destination_size_table (predicted_size) - self.predicted_size = get_size_table(self.selector) + self.predicted_size = inject.get_table(size_table_name(self.model_selector)).to_frame() # - shared_data if shared_data is not None: @@ -171,9 +181,9 @@ def read_saved_shadow_prices(self, model_settings): if file_path: shadow_prices = pd.read_csv(file_path, index_col=0) self.saved_shadow_price_file_path = file_path # informational - logging.info("loaded saved_shadow_prices from %s" % (file_path)) + logging.info("loaded saved_shadow_prices from %s" % file_path) else: - logging.warning("Could not find saved_shadow_prices file %s" % (file_path)) + logging.warning("Could not find saved_shadow_prices file %s" % file_path) return shadow_prices @@ -223,12 +233,12 @@ def get_tally(t): with self.shared_data_lock: return self.shared_data[t] - def wait(tally, target, tally_name): + def wait(tally, target): while get_tally(tally) != target: time.sleep(1) # - nobody checks in until checkout clears - wait(TALLY_CHECKOUT, 0, 'TALLY_CHECKOUT') + wait(TALLY_CHECKOUT, 0) # - add local_modeled_size data, increment TALLY_CHECKIN with self.shared_data_lock: @@ -239,7 +249,7 @@ def wait(tally, target, tally_name): self.shared_data[TALLY_CHECKIN] += 1 # - wait until everybody else has checked in - wait(TALLY_CHECKIN, num_processes, 'TALLY_CHECKIN') + wait(TALLY_CHECKIN, num_processes) # - copy shared data, increment TALLY_CHECKIN with self.shared_data_lock: @@ -250,7 +260,7 @@ def wait(tally, target, tally_name): # - first in waits until all other processes have checked out, and cleans tub if first_in: - wait(TALLY_CHECKOUT, num_processes, 'TALLY_CHECKOUT') + wait(TALLY_CHECKOUT, num_processes) with self.shared_data_lock: # zero shared_data, clear TALLY_CHECKIN, and TALLY_CHECKOUT semaphores self.shared_data[:] = 0 @@ -354,14 +364,14 @@ def check_fit(self, iteration): converged = (total_fails <= max_fail) # for c in predicted_size: - # print("check_fit %s segment %s" % (self.selector, c)) + # print("check_fit %s segment %s" % (self.model_selector, c)) # print(" modeled %s" % (modeled_size[c].sum())) # print(" predicted %s" % (predicted_size[c].sum())) # print(" max abs diff %s" % (abs_diff[c].max())) # print(" max rel diff %s" % (rel_diff[c].max())) logging.info("check_fit %s iteration: %s converged: %s max_fail: %s total_fails: %s" % - (self.selector, iteration, converged, max_fail, total_fails)) + (self.model_selector, iteration, converged, max_fail, total_fails)) # - convergence stats if converged or iteration == self.max_iterations: @@ -379,9 +389,9 @@ def update_shadow_prices(self): The presumption is that shadow_price_adjusted_predicted_size (along with other attractors) is being used in a utility expression in a location choice model. The goal is to get the - aggregate location modeled size (choice aggregated by selector segment and zone) to match - predicted_size. Since the location choice model may not achieve that goal initially, we - create a 'shadow price' that tweaks the size_term to encourage the aggregate choices to + aggregate location modeled size (choice aggregated by model_selector segment and zone) to + match predicted_size. Since the location choice model may not achieve that goal initially, + we create a 'shadow price' that tweaks the size_term to encourage the aggregate choices to approach the desired target predicted_sizes. shadow_prices is a table of coefficient (for each zone and segment) that is increases or @@ -495,7 +505,7 @@ def write_trace_files(self, iteration): Write trace files for this iteration Writes predicted_size, modeled_size, and shadow_prices tables - Trace file names are tagged with selector and iteration number + Trace file names are tagged with model_selector and iteration number (e.g. self.predicted_size => shadow_price_school_predicted_size_1) Parameters @@ -507,35 +517,36 @@ def write_trace_files(self, iteration): if iteration == 1: # write predicted_size only on first iteration, as it doesn't change tracing.write_csv(self.predicted_size, - 'shadow_price_%s_predicted_size' % self.selector, + 'shadow_price_%s_predicted_size' % self.model_selector, transpose=False) tracing.write_csv(self.modeled_size, - 'shadow_price_%s_modeled_size_%s' % (self.selector, iteration), + 'shadow_price_%s_modeled_size_%s' % (self.model_selector, iteration), transpose=False) tracing.write_csv(self.shadow_prices, - 'shadow_price_%s_shadow_prices_%s' % (self.selector, iteration), + 'shadow_price_%s_shadow_prices_%s' % (self.model_selector, iteration), transpose=False) -def block_name(selector): +def block_name(model_selector): """ - return canonical block name for selector + return canonical block name for model_selector - Ordinarilly and ideally this wold just be selector, but since mp_tasks saves all shared data - blocks in a common dict to pass to sub-tasks, we want to be able to handle an possible - collision between selector names and skim names. Otherwise, just use selector name. + Ordinarily and ideally this would just be model_selector, but since mp_tasks saves all + shared data blocks in a common dict to pass to sub-tasks, we want to be able override + block naming convention to handle any collisions between model_selector names and skim names. + Until and unless that happens, we just use model_selector name. Parameters ---------- - selector + model_selector Returns ------- block_name : str canonical block name """ - return selector + return model_selector def get_shadow_pricing_info(): @@ -549,7 +560,7 @@ def get_shadow_pricing_info(): ------- shadow_pricing_info: dict 'dtype': , - 'block_shapes': dict {: } + 'block_shapes': dict {: } """ land_use = inject.get_table('land_use') @@ -557,17 +568,17 @@ def get_shadow_pricing_info(): shadow_settings = config.read_model_settings('shadow_pricing.yaml') - # shadow_pricing_models is dict of {: } + # shadow_pricing_models is dict of {: } shadow_pricing_models = shadow_settings['shadow_pricing_models'] blocks = OrderedDict() - for selector in shadow_pricing_models: + for model_selector in shadow_pricing_models: sp_rows = len(land_use) - sp_cols = len(size_terms[size_terms.selector == selector]) + sp_cols = len(size_terms[size_terms.model_selector == model_selector]) # extra tally column for TALLY_CHECKIN and TALLY_CHECKOUT semaphores - blocks[block_name(selector)] = (sp_rows, sp_cols + 1) + blocks[block_name(model_selector)] = (sp_rows, sp_cols + 1) sp_dtype = np.int64 @@ -583,7 +594,8 @@ def buffers_for_shadow_pricing(shadow_pricing_info): """ Allocate shared_data buffers for multiprocess shadow pricing - Allocates one buffer per selector. Buffer datatype and shape specified by shadow_pricing_info + Allocates one buffer per model_selector. + Buffer datatype and shape specified by shadow_pricing_info buffers are multiprocessing.Array (RawArray protected by a multiprocessing.Lock wrapper) We don't actually use the wrapped version as it slows access down and doesn't provide @@ -597,8 +609,8 @@ def buffers_for_shadow_pricing(shadow_pricing_info): Returns ------- - data_buffers : dict { : } - dict of multiprocessing.Array keyed by selector + data_buffers : dict { : } + dict of multiprocessing.Array keyed by model_selector """ dtype = shadow_pricing_info['dtype'] @@ -621,19 +633,19 @@ def buffers_for_shadow_pricing(shadow_pricing_info): shared_data_buffer = multiprocessing.Array(typecode, buffer_size) - logger.info("buffer_for_shadow_pricing added block %s" % (block_key)) + logger.info("buffer_for_shadow_pricing added block %s" % block_key) data_buffers[block_key] = shared_data_buffer return data_buffers -def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): +def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, model_selector): """ Parameters ---------- - data_buffers : dict of { : } + data_buffers : dict of { : } multiprocessing.Array is simply a convenient way to bundle Array and Lock we extract the lock and wrap the RawArray in a numpy array for convenience in indexing @@ -642,11 +654,11 @@ def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): shadow_pricing_info : dict dict of useful info 'dtype': sp_dtype, - 'block_shapes' : OrderedDict({: }) - dict mapping selector to block shape (including extra column for semaphores) + 'block_shapes' : OrderedDict({: }) + dict mapping model_selector to block shape (including extra column for semaphores) e.g. {'school': (num_zones, num_segments + 1) - selector : str - location type selector (e.g. school or workplace) + model_selector : str + location type model_selector (e.g. school or workplace) Returns ------- @@ -660,21 +672,21 @@ def shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector): dtype = shadow_pricing_info['dtype'] block_shapes = shadow_pricing_info['block_shapes'] - if selector not in block_shapes: - raise RuntimeError("Selector %s not in shadow_pricing_info" % selector) + if model_selector not in block_shapes: + raise RuntimeError("Model selector %s not in shadow_pricing_info" % model_selector) - if block_name(selector) not in data_buffers: - raise RuntimeError("Block %s not in data_buffers" % block_name(selector)) + if block_name(model_selector) not in data_buffers: + raise RuntimeError("Block %s not in data_buffers" % block_name(model_selector)) - shape = block_shapes[selector] - data = data_buffers[block_name(selector)] + shape = block_shapes[model_selector] + data = data_buffers[block_name(model_selector)] return np.frombuffer(data.get_obj(), dtype=dtype).reshape(shape), data.get_lock() def load_shadow_price_calculator(model_settings): """ - Initialize ShadowPriceCalculator for model selector (e.g. school or workplace) + Initialize ShadowPriceCalculator for model_selector (e.g. school or workplace) If multiprocessing, get the shared_data buffer to coordinate global_predicted_size calculation across sub-processes @@ -688,7 +700,7 @@ def load_shadow_price_calculator(model_settings): spc : ShadowPriceCalculator """ - selector = model_settings['SELECTOR'] + model_selector = model_settings['MODEL_SELECTOR'] # - get shared_data from data_buffers (if multiprocessing) data_buffers = inject.get_injectable('data_buffers', None) @@ -702,7 +714,8 @@ def load_shadow_price_calculator(model_settings): inject.add_injectable('shadow_pricing_info', shadow_pricing_info) # - extract data buffer and reshape as numpy array - data, lock = shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, selector) + data, lock = \ + shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, model_selector) else: data = None # ShadowPriceCalculator will allocate its own data lock = None @@ -717,9 +730,9 @@ def load_shadow_price_calculator(model_settings): def add_predicted_size_tables(): """ - inject tour_destination_size_terms tables for each selector (e.g. school, workplace) + inject tour_destination_size_terms tables for each model_selector (e.g. school, workplace) - Size tables are pandas dataframes with locations counts for selector by zone and segment + Size tables are pandas dataframes with locations counts for model_selector by zone and segment tour_destination_size_terms if using shadow pricing, we scale size_table counts to sample population @@ -744,14 +757,14 @@ def add_predicted_size_tables(): logger.warning('shadow_pricing_models list not found in shadow_pricing settings') return - # shadow_pricing_models is dict of {: } + # shadow_pricing_models is dict of {: } # since these are scaled to model size, they have to be created while single-process - for selector, model_name in iteritems(shadow_pricing_models): + for model_selector, model_name in iteritems(shadow_pricing_models): model_settings = config.read_model_settings(model_name) - assert selector == model_settings['SELECTOR'] + assert model_selector == model_settings['MODEL_SELECTOR'] segment_ids = model_settings['SEGMENT_IDS'] chooser_table_name = model_settings['CHOOSER_TABLE_NAME'] @@ -765,7 +778,7 @@ def add_predicted_size_tables(): # - raw_predicted_size land_use = inject.get_table('land_use') size_terms = inject.get_injectable('size_terms') - raw_size = tour_destination_size_terms(land_use, size_terms, selector).astype(np.float64) + raw_size = tour_destination_size_terms(land_use, size_terms, model_selector) assert set(raw_size.columns) == set(segment_ids.keys()) if use_shadow_pricing: @@ -799,4 +812,4 @@ def add_predicted_size_tables(): # don't scale if not shadow_pricing (breaks partial sample replicability) scaled_size = raw_size - inject.add_table(size_table_name(selector), scaled_size) + inject.add_table(size_table_name(model_selector), scaled_size) diff --git a/activitysim/abm/tables/size_terms.py b/activitysim/abm/tables/size_terms.py index 56c0dfb33..db2bed42e 100644 --- a/activitysim/abm/tables/size_terms.py +++ b/activitysim/abm/tables/size_terms.py @@ -60,14 +60,14 @@ def size_term(land_use, destination_choice_coeffs): return land_use[coeffs.index].dot(coeffs) -def tour_destination_size_terms(land_use, size_terms, selector): +def tour_destination_size_terms(land_use, size_terms, model_selector): """ Parameters ---------- land_use - pipeline table size_terms - pipeline table - selector - str + model_selector - str Returns ------- @@ -75,9 +75,9 @@ def tour_destination_size_terms(land_use, size_terms, selector): :: pandas.dataframe - one column per selector segment with index of land_use - e.g. for selector 'work', columns will be work_low, work_med, work_high, work_veryhigh - and for selector 'trip', columns will be eatout, escort, othdiscr, othmaint, ... + one column per model_selector segment with index of land_use + e.g. for model_selector 'workplace', columns will be work_low, work_med, ... + and for model_selector 'trip', columns will be eatout, escort, othdiscr, ... work_low work_med work_high work_veryhigh TAZ ... @@ -88,8 +88,8 @@ def tour_destination_size_terms(land_use, size_terms, selector): land_use = land_use.to_frame() - size_terms = size_terms[size_terms.selector == selector].copy() - del size_terms['selector'] + size_terms = size_terms[size_terms.model_selector == model_selector].copy() + del size_terms['model_selector'] df = pd.DataFrame({key: size_term(land_use, row) for key, row in size_terms.iterrows()}, index=land_use.index) @@ -101,9 +101,4 @@ def tour_destination_size_terms(land_use, size_terms, selector): if not (df.dtypes == 'float64').all(): logger.warning('Surprised to find that not all size_terms were float64!') - # - NARROW - # float16 has 3.3 decimal digits of precision, float32 has 7.2 - df = df.astype(np.float16, errors='raise') - assert np.isfinite(df.values).all() - return df diff --git a/activitysim/abm/test/configs/destination_choice_size_terms.csv b/activitysim/abm/test/configs/destination_choice_size_terms.csv index 72ad8c472..0d04253fd 100644 --- a/activitysim/abm/test/configs/destination_choice_size_terms.csv +++ b/activitysim/abm/test/configs/destination_choice_size_terms.csv @@ -1,4 +1,4 @@ -selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE +model_selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE workplace,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 workplace,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 workplace,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 diff --git a/activitysim/abm/test/configs/school_location.yaml b/activitysim/abm/test/configs/school_location.yaml index 2b78ac8a5..5fbfdd904 100644 --- a/activitysim/abm/test/configs/school_location.yaml +++ b/activitysim/abm/test/configs/school_location.yaml @@ -32,8 +32,8 @@ annotate_persons: # required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons -# size_terms selector -SELECTOR: school +# size_terms model_selector +MODEL_SELECTOR: school # chooser column with segment_id for this segment type CHOOSER_SEGMENT_COLUMN_NAME: school_segment diff --git a/activitysim/abm/test/configs/shadow_pricing.yaml b/activitysim/abm/test/configs/shadow_pricing.yaml index 337a01019..78837f89c 100644 --- a/activitysim/abm/test/configs/shadow_pricing.yaml +++ b/activitysim/abm/test/configs/shadow_pricing.yaml @@ -1,4 +1,8 @@ +# list model_selectors and model_names of models that use shadow pricing +# This list identifies which size_terms to preload (which must be done in single process mode +# so predicted_size tables can be scaled to population) +# (and allows models to be renamed without changing model_selectors) shadow_pricing_models: school: school_location workplace: workplace_location diff --git a/activitysim/abm/test/configs/workplace_location.yaml b/activitysim/abm/test/configs/workplace_location.yaml index e099d23ff..74e74a6f7 100644 --- a/activitysim/abm/test/configs/workplace_location.yaml +++ b/activitysim/abm/test/configs/workplace_location.yaml @@ -31,8 +31,8 @@ annotate_persons: # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -# size_terms selector -SELECTOR: workplace +# size_terms model_selector +MODEL_SELECTOR: workplace # we can't use use household income_segment as this will also be set for non-workers CHOOSER_SEGMENT_COLUMN_NAME: income_segment diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index cf453053a..b9867d613 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -140,7 +140,7 @@ def _interaction_sample( be merged with choosers because there are interaction terms or because alternatives are being sampled. - Parameters are same as for public function interaction_simulate + Parameters are same as for public function interaction_sa,ple spec : dataframe one row per spec expression and one col with utility coefficient diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index 9a7158d8c..e1e990629 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -1004,11 +1004,26 @@ def write_breadcrumbs(breadcrumbs): yaml.dump(breadcrumbs, f) -def is_sub_task(): +def if_sub_task(if_is, if_isnt): + """ + select one of two values depending whether current process is primary process or subtask - return inject.get_injectable('is_sub_task', False) + This is primarily intended for use in yaml files to select between (e.g.) logging levels + so main log file can display only warnings and errors from subtasks + In yaml file, it can be used like this: -def if_sub_task(if_is, if_isnt): + level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [WARNING, NOTSET] + + + Parameters + ---------- + if_is : (any type) value to return if process is a subtask + if_isnt : (any type) value to return if process is not a subtask + + Returns + ------- + (any type) (one of parameters if_is or if_isnt) + """ - return if_is if is_sub_task() else if_isnt + return if_is if inject.get_injectable('is_sub_task', False) else if_isnt diff --git a/example/configs/destination_choice_size_terms.csv b/example/configs/destination_choice_size_terms.csv index ece155caa..cfd404b14 100644 --- a/example/configs/destination_choice_size_terms.csv +++ b/example/configs/destination_choice_size_terms.csv @@ -1,4 +1,4 @@ -selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE +model_selector,segment,TOTHH,RETEMPN,FPSEMPN,HEREMPN,OTHEMPN,AGREMPN,MWTEMPN,AGE0519,HSENROLL,COLLFTE,COLLPTE workplace,work_low,0,0.129,0.193,0.383,0.12,0.01,0.164,0,0,0,0 workplace,work_med,0,0.12,0.197,0.325,0.139,0.008,0.21,0,0,0,0 workplace,work_high,0,0.11,0.207,0.284,0.154,0.006,0.239,0,0,0,0 diff --git a/example/configs/school_location.yaml b/example/configs/school_location.yaml index 2b78ac8a5..5fbfdd904 100644 --- a/example/configs/school_location.yaml +++ b/example/configs/school_location.yaml @@ -32,8 +32,8 @@ annotate_persons: # required by initialize_households when creating school_destination_size table CHOOSER_TABLE_NAME: persons -# size_terms selector -SELECTOR: school +# size_terms model_selector +MODEL_SELECTOR: school # chooser column with segment_id for this segment type CHOOSER_SEGMENT_COLUMN_NAME: school_segment diff --git a/example/configs/workplace_location.yaml b/example/configs/workplace_location.yaml index e099d23ff..74e74a6f7 100644 --- a/example/configs/workplace_location.yaml +++ b/example/configs/workplace_location.yaml @@ -31,8 +31,8 @@ annotate_persons: # income_segment is in households, but we want to count persons CHOOSER_TABLE_NAME: persons_merged -# size_terms selector -SELECTOR: workplace +# size_terms model_selector +MODEL_SELECTOR: workplace # we can't use use household income_segment as this will also be set for non-workers CHOOSER_SEGMENT_COLUMN_NAME: income_segment From e370f28ecc9bee27eaafc363408c11659ce80bcd Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Thu, 10 Jan 2019 17:29:35 -0500 Subject: [PATCH 068/122] documentation and cleanup of mp_tasks --- activitysim/abm/misc.py | 15 - .../abm/models/joint_tour_scheduling.py | 4 +- activitysim/abm/models/location_choice.py | 1 + .../abm/models/mandatory_scheduling.py | 4 +- .../abm/models/non_mandatory_scheduling.py | 4 +- .../test/test_vectorize_tour_scheduling.py | 5 +- .../models/util/vectorize_tour_scheduling.py | 12 +- activitysim/abm/tables/households.py | 20 +- activitysim/abm/tables/shadow_pricing.py | 18 +- activitysim/core/config.py | 53 +- activitysim/core/mp_tasks.py | 984 ++++++++++++++---- activitysim/core/pipeline.py | 37 +- .../benchmarks/benchmarks_full_run.txt | 4 - .../benchmarks/benchmarks_stride_run.txt | 1 - example_azure/example_mp/settings.yaml | 3 - example_mp/configs/settings.yaml | 3 - example_mp/simulation.py | 13 +- example_stride/.gitignore | 1 - example_stride/coalesce.py | 46 - example_stride/configs_report/logging.yaml | 57 - example_stride/configs_report/settings.yaml | 43 - example_stride/configs_sim/logging.yaml | 57 - example_stride/configs_sim/settings.yaml | 65 -- example_stride/output/.gitignore | 7 - example_stride/output/log/.gitignore | 2 - example_stride/output/trace/.gitignore | 1 - example_stride/script.txt | 21 - example_stride/simulation.py | 102 -- 28 files changed, 854 insertions(+), 729 deletions(-) delete mode 100644 example_stride/.gitignore delete mode 100644 example_stride/coalesce.py delete mode 100644 example_stride/configs_report/logging.yaml delete mode 100644 example_stride/configs_report/settings.yaml delete mode 100644 example_stride/configs_sim/logging.yaml delete mode 100644 example_stride/configs_sim/settings.yaml delete mode 100644 example_stride/output/.gitignore delete mode 100644 example_stride/output/log/.gitignore delete mode 100644 example_stride/output/trace/.gitignore delete mode 100644 example_stride/script.txt delete mode 100644 example_stride/simulation.py diff --git a/activitysim/abm/misc.py b/activitysim/abm/misc.py index 3213a0d6a..accd94420 100644 --- a/activitysim/abm/misc.py +++ b/activitysim/abm/misc.py @@ -28,21 +28,6 @@ def households_sample_size(settings, override_hh_ids): return len(override_hh_ids) -@inject.injectable(cache=True) -def households_sample_stride(settings): - - stride = settings.get('households_sample_stride', None) - - if stride and not (isinstance(stride, list) and - len(stride) == 2 and - all(isinstance(x, int) for x in stride)): - logger.warning("setting households_sample_stride should be a list of length 2, but was %s" % - stride) - stride = None - - return stride - - @inject.injectable(cache=True) def override_hh_ids(settings): diff --git a/activitysim/abm/models/joint_tour_scheduling.py b/activitysim/abm/models/joint_tour_scheduling.py index 1a4137f50..116045c2f 100644 --- a/activitysim/abm/models/joint_tour_scheduling.py +++ b/activitysim/abm/models/joint_tour_scheduling.py @@ -76,7 +76,7 @@ def joint_tour_scheduling( locals_dict=locals_d, trace_label=trace_label) - tdd_choices = vectorize_joint_tour_scheduling( + tdd_choices, timetable = vectorize_joint_tour_scheduling( joint_tours, joint_tour_participants, persons_merged, tdd_alts, @@ -85,6 +85,8 @@ def joint_tour_scheduling( chunk_size=chunk_size, trace_label=trace_label) + timetable.replace_table() + assign_in_place(tours, tdd_choices) pipeline.replace_table("tours", tours) diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index b5629ddf6..7221fd02f 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -66,6 +66,7 @@ With shadow pricing, and iterative treatment of each segment, the structure of the code is: :: + repeat for each segment run_location_sample diff --git a/activitysim/abm/models/mandatory_scheduling.py b/activitysim/abm/models/mandatory_scheduling.py index 1a514085b..fd739d36c 100644 --- a/activitysim/abm/models/mandatory_scheduling.py +++ b/activitysim/abm/models/mandatory_scheduling.py @@ -52,7 +52,7 @@ def mandatory_tour_scheduling(tours, model_constants = config.get_model_constants(model_settings) logger.info("Running mandatory_tour_scheduling with %d tours", len(tours)) - tdd_choices = vectorize_tour_scheduling( + tdd_choices, timetable = vectorize_tour_scheduling( mandatory_tours, persons_merged, tdd_alts, spec={'work': work_spec, 'school': school_spec}, @@ -60,6 +60,8 @@ def mandatory_tour_scheduling(tours, chunk_size=chunk_size, trace_label=trace_label) + timetable.replace_table() + assign_in_place(tours, tdd_choices) pipeline.replace_table("tours", tours) diff --git a/activitysim/abm/models/non_mandatory_scheduling.py b/activitysim/abm/models/non_mandatory_scheduling.py index d2655cecf..14e1144ef 100644 --- a/activitysim/abm/models/non_mandatory_scheduling.py +++ b/activitysim/abm/models/non_mandatory_scheduling.py @@ -61,13 +61,15 @@ def non_mandatory_tour_scheduling(tours, locals_dict=locals_d, trace_label=trace_label) - tdd_choices = vectorize_tour_scheduling( + tdd_choices, timetable = vectorize_tour_scheduling( non_mandatory_tours, persons_merged, tdd_alts, model_spec, constants=constants, chunk_size=chunk_size, trace_label=trace_label) + timetable.replace_table() + assign_in_place(tours, tdd_choices) pipeline.replace_table("tours", tours) diff --git a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py index a6eb7b8c3..feb51cb9d 100644 --- a/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/test/test_vectorize_tour_scheduling.py @@ -62,8 +62,9 @@ def test_vts(): inject.add_injectable("check_for_variability", True) - tdd_choices = vectorize_tour_scheduling(tours, persons, alts, spec, - constants={}, chunk_size=0, trace_label='test_vts') + tdd_choices, timetable = vectorize_tour_scheduling( + tours, persons, alts, spec, + constants={}, chunk_size=0, trace_label='test_vts') # FIXME - dead reckoning regression # there's no real logic here - this is just what came out of the monte carlo diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index d2a38d1f5..d57ce8141 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -335,6 +335,8 @@ def vectorize_tour_scheduling(tours, persons_merged, alts, spec, choices : Series A Series of choices where the index is the index of the tours DataFrame and the values are the index of the alts DataFrame. + timetable : TimeTable + persons timetable updated with tours (caller should replace_table for it to persist) """ trace_label = tracing.extend_trace_label(trace_label, 'vectorize_tour_scheduling') @@ -405,9 +407,7 @@ def vectorize_tour_scheduling(tours, persons_merged, alts, spec, # include the index of the choice in the tdd alts table tdd['tdd'] = choices - timetable.replace_table() - - return tdd + return tdd, timetable def vectorize_subtour_scheduling(parent_tours, subtours, persons_merged, alts, spec, @@ -575,6 +575,8 @@ def vectorize_joint_tour_scheduling( choices : Series A Series of choices where the index is the index of the tours DataFrame and the values are the index of the alts DataFrame. + persons_timetable : TimeTable + timetable updated with joint tours (caller should replace_table for it to persist) """ trace_label = tracing.extend_trace_label(trace_label, 'vectorize_joint_tour_scheduling') @@ -641,9 +643,7 @@ def vectorize_joint_tour_scheduling( # include the index of the choice in the tdd alts table tdd['tdd'] = choices - persons_timetable.replace_table() - # print "participant windows after scheduling\n", \ # persons_timetable.slice_windows_by_row_id(joint_tour_participants.person_id) - return tdd + return tdd, persons_timetable diff --git a/activitysim/abm/tables/households.py b/activitysim/abm/tables/households.py index 40e6eea9b..4a256460f 100644 --- a/activitysim/abm/tables/households.py +++ b/activitysim/abm/tables/households.py @@ -21,7 +21,7 @@ @inject.table() -def households(households_sample_size, households_sample_stride, override_hh_ids, trace_hh_id): +def households(households_sample_size, override_hh_ids, trace_hh_id): df_full = read_input_table("households") households_sliced = False @@ -79,24 +79,6 @@ def households(households_sample_size, households_sample_stride, override_hh_ids else: df = df_full - if households_sample_stride: - - # - possibly resampling - if households_sliced: - df_full = df - if override_hh_ids is not None: - logger.warning("Applying stride slice to override_hh_ids households.") - if households_sample_size > 0: - logger.warning("Applying stride slice to sampled households.") - - stride_len, offset, = households_sample_stride - - df = df_full[np.asanyarray(list(range(df_full.shape[0]))) % stride_len == offset] - households_sliced = True - - logger.info("stride (%s,%s) sliced %s of %s households" % - (stride_len, offset, df.shape[0], df_full.shape[0])) - # persons table inject.add_injectable('households_sliced', households_sliced) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index 0ed307d2f..45526f577 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -85,7 +85,7 @@ def size_table_name(model_selector): class ShadowPriceCalculator(object): - def __init__(self, model_settings, shared_data=None, shared_data_lock=None): + def __init__(self, model_settings, num_processes, shared_data=None, shared_data_lock=None): """ Presence of shared_data is used as a flag for multiprocessing @@ -103,6 +103,7 @@ def __init__(self, model_settings, shared_data=None, shared_data_lock=None): shared_data_lock : numpy array wrapping multiprocessing.RawArray or None (if single process) """ + self.num_processes = num_processes self.use_shadow_pricing = bool(config.setting('use_shadow_pricing')) self.saved_shadow_price_file_path = None # set by read_saved_shadow_prices if loaded @@ -225,9 +226,7 @@ def synchronize_choices(self, local_modeled_size): # shouldn't be called if we are not multiprocessing assert self.shared_data is not None - - num_processes = inject.get_injectable("num_processes") - assert num_processes > 1 + assert self.num_processes > 1 def get_tally(t): with self.shared_data_lock: @@ -249,7 +248,7 @@ def wait(tally, target): self.shared_data[TALLY_CHECKIN] += 1 # - wait until everybody else has checked in - wait(TALLY_CHECKIN, num_processes) + wait(TALLY_CHECKIN, self.num_processes) # - copy shared data, increment TALLY_CHECKIN with self.shared_data_lock: @@ -260,7 +259,7 @@ def wait(tally, target): # - first in waits until all other processes have checked out, and cleans tub if first_in: - wait(TALLY_CHECKOUT, num_processes) + wait(TALLY_CHECKOUT, self.num_processes) with self.shared_data_lock: # zero shared_data, clear TALLY_CHECKIN, and TALLY_CHECKOUT semaphores self.shared_data[:] = 0 @@ -298,7 +297,7 @@ def set_choices(self, choices_df): modeled_size[c] = segment_choices.groupby('dest_choice').size() modeled_size = modeled_size.fillna(0).astype(int) - if self.shared_data is None: + if self.num_processes == 1: # - not multiprocessing self.modeled_size = modeled_size else: @@ -700,6 +699,8 @@ def load_shadow_price_calculator(model_settings): spc : ShadowPriceCalculator """ + num_processes = inject.get_injectable('num_processes', 1) + model_selector = model_settings['MODEL_SELECTOR'] # - get shared_data from data_buffers (if multiprocessing) @@ -717,13 +718,14 @@ def load_shadow_price_calculator(model_settings): data, lock = \ shadow_price_data_from_buffers(data_buffers, shadow_pricing_info, model_selector) else: + assert num_processes == 1 data = None # ShadowPriceCalculator will allocate its own data lock = None # - ShadowPriceCalculator spc = ShadowPriceCalculator( model_settings, - data, lock) + num_processes, data, lock) return spc diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 1c95b591b..86c91d7c7 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -75,25 +75,6 @@ def str2bool(v): raise argparse.ArgumentTypeError('Boolean value expected.') -def str2stride(v): - - try: - stride_len, offset = v.split(',', 1) - stride_len = int(stride_len) - offset = int(offset) - - except Exception as err: - raise argparse.ArgumentTypeError('int list of length two expected.') - - if stride_len == 0: - raise argparse.ArgumentTypeError('stride_len cannot be 0.') - - if offset >= stride_len: - raise argparse.ArgumentTypeError('offset cannot be greater than stride_len.') - - return [stride_len, offset] - - @inject.injectable(cache=True) def settings(): return read_settings_file('settings.yaml', mandatory=True) @@ -127,8 +108,6 @@ def handle_standard_args(parser=None): parser.add_argument("-m", "--multiprocess", type=str2bool, nargs='?', const=True, help="run multiprocess (boolean flag, no arg defaults to true)") - parser.add_argument("-s", "--stride", type=str2stride, - help="households_sample_stride stride_len and offset -e.g. --stride=4,0") parser.add_argument("-p", "--pipeline", help="pipeline file name") args = parser.parse_args() @@ -139,27 +118,35 @@ def override_injectable(name, value): inject.add_injectable(name, value) injectables.append(name) + def override_setting(key, value): + new_settings = inject.get_injectable('settings') + new_settings[key] = value + inject.add_injectable('settings', new_settings) + if args.config: for dir in args.config: if not os.path.exists(dir): raise IOError("Could not find configs dir '%s'" % dir) override_injectable("configs_dir", args.config) + if args.data: for dir in args.data: if not os.path.exists(dir): raise IOError("Could not find data dir '%s'" % dir) override_injectable("data_dir", args.data) + if args.output: if not os.path.exists(args.output): raise IOError("Could not find output dir '%s'." % args.output) override_injectable("output_dir", args.output) - if args.stride: - override_injectable("households_sample_stride", args.stride) if args.pipeline: override_injectable("pipeline_file_name", args.pipeline) # - do these after potentially overriding configs_dir + # FIXME we don't currently pass settings as an injectable to mp_tasks.run_multiprocess + # these two make it through as they are incorporated into the run_list by parent + # but if a more extensible capability is desired, settings could be passed in injectables if args.resume is not None: override_setting('resume_after', args.resume) if args.multiprocess is not None: @@ -168,13 +155,6 @@ def override_injectable(name, value): return injectables -def override_setting(key, value): - - settings = inject.get_injectable('settings') - settings[key] = value - inject.add_injectable('settings', settings) - - def setting(key, default=None): return inject.get_injectable('settings').get(key, default) @@ -375,3 +355,16 @@ def backfill_settings(settings, backfill): (file_name, configs_dir)) return settings + + +def filter_warnings(): + """ + set warning filter to 'strict' if specified in settings + """ + + if setting('strict', False): # noqa: E402 + import warnings + warnings.filterwarnings('error', category=Warning) + warnings.filterwarnings('default', category=PendingDeprecationWarning, module='future') + warnings.filterwarnings('default', category=FutureWarning, module='pandas') + warnings.filterwarnings('default', category=RuntimeWarning, module='numpy') diff --git a/activitysim/core/mp_tasks.py b/activitysim/core/mp_tasks.py index e1e990629..23ca45944 100644 --- a/activitysim/core/mp_tasks.py +++ b/activitysim/core/mp_tasks.py @@ -33,44 +33,226 @@ # activitysim.abm imported for its side-effects (dependency injection) from activitysim import abm -from activitysim.abm.tables.skims import get_skim_info -from activitysim.abm.tables.skims import buffers_for_skims -from activitysim.abm.tables.skims import load_skims - -from activitysim.abm.tables.shadow_pricing import get_shadow_pricing_info -from activitysim.abm.tables.shadow_pricing import buffers_for_shadow_pricing +from activitysim.abm.tables import skims +from activitysim.abm.tables import shadow_pricing logger = logging.getLogger(__name__) +LAST_CHECKPOINT = '_' """ - child process methods (called within sub process) +mp_tasks - activitysim multiprocessing overview + +Activitysim runs a list of models sequentially, performing various computational operations +on tables. Model steps can modify values in existing tables, add columns, or create additional +tables. Activitysim provides the facility, via expression files, to specify vectorized operations +on data tables. The ability to vectorize operations depends upon the independence of the +computations performed on the vectorized elements. + +Python is agonizingly slow performing scalar operations sequentially on large datasets, so +vectorization (using pandas and/or numpy) is essential for good performance. + +Fortunately most activity based model simulation steps are row independent at the household, +person, tour, or trip level. The decisions for one household are independent of the choices +made by other households. Thus it is (generally speaking) possible to run an entire simulation +on a household sample with only one household, and get the same result for that household as +you would running the simulation on a thousand households. (See the shared data section below +for an exception to this highly convenient situation.) + +The random number generator supports this goal by providing streams of random numbers +for each households and person that are mutually independent and repeatable across model runs +and processes. + +To the extent that simulation model steps are row independent, we can implement most simulations +as a series of vectorized operations on pandas DataFrames and numpy arrays. These vectorized +operations are much faster than sequential python because they are implemented by native code +(compiled C) and are to some extent multi-threaded. But the benefits of numpy multi-processing are +limited because they only apply to atomic numpy or pandas calls, and as soon as control returns +to python it is single-threaded and slow. + +Multi-threading is not an attractive strategy to get around the python performance problem because +of the limitations imposed by python's global interpreter lock (GIL). Rather than struggling with +python multi-threading, this module uses the python multiprocessing to parallelize certain models. + +Because of activitysim's modular and extensible architecture, we don't hardwire the multiprocessing +architecture. The specification of which models should be run in parallel, how many processers +should be used, and the segmentation of the data between processes are all specified in the +settings config file. For conceptual simplicity, the single processing model as treated as +dominant (because even though in practice multiprocessing may be the norm for production runs, +the single-processing model will be used in development and debugging and keeping it dominant +will tend to concentrate the multiprocessing-specific code in one place and prevent multiprocessing +considerations from permeating the code base obscuring the model-specific logic. + +The primary function of the multiprocessing settings are to identify distinct stages of +computation, and to specify how many simultaneous processes should be used to perform them, +and how the data to be treated should be apportioned between those processes. We assume that +the data can be apportioned between subprocesses according to the index of a single primary table +(e.g. households) or else are by derivative or dependent tables that reference that table's index +(primary key) with a ref_col (foreign key) sharing the name of the primary table's key. + +Generally speaking, we assume that any new tables that are created are directly dependent on the +previously existing tables, and all rows in new tables are either attributable to previously +existing rows in the pipeline tables, or are global utility tables that are identical across +sub-processes. + +Note: There are a few exceptions to 'row independence', such as school and location choice models, +where the model behavior is externally constrained or adjusted. For instance, we want school +location choice to match known aggregate school enrollments by zone. Similarly, a parking model +(not yet implemented) might be constrained by availability. These situations require special +handling. + +:: + + models: + ### mp_initialize step + - initialize_landuse + - compute_accessibility + - initialize_households + ### mp_households step + - school_location + - workplace_location + - auto_ownership_simulate + ### mp_summarize step + - write_tables + + multiprocess_steps: + - name: mp_initialize + begin: initialize_landuse + - name: mp_households + begin: school_location + num_processes: 2 + slice: + tables: + - households + - persons + - name: mp_summarize + begin: write_tables + +The multiprocess_steps setting above annotates the models list to indicate that the simulation +should be broken into three steps. + +The first multiprocess_step (mp_initialize) begins with the initialize_landuse step and is +implicity single-process because there is no 'slice' key indicating how to apportion the tables. +This first step includes all models listed in the 'models' setting up until the first step +in the next multiprocess_steps. + +The second multiprocess_step (mp_households) starts with the school location model and continues +through auto_ownership_simulate. The 'slice' info indicates that the tables should be sliced by +households, and that persons is a dependent table and so and persons with a ref_col (foreign key +column with the same name as the Households table index) referencing a household record should be +taken to 'belong' to that household. Similarly, any other table that either share an index +(i.e. having the same name) with either the households or persons table, or have a ref_col to +either of their indexes, should also be considered a dependent table. + +The num_processes setting of 2 indicates that the pipeline should be split in two, and half of the +households should be apportioned into each subprocess pipeline, and all dependent tables should +likewise be apportioned accordingly. All other tables (e.g. land_use) that do share an index (name) +or have a ref_col should be considered mirrored and be included in their entirety. + +The primary table is sliced by num_processes-sized strides. (e.g. for num_processes == 2, the +sub-processes get every second record starting at offsets 0 and 1 respectively. All other dependent +tables slices are based (directly or indirectly) on this primary stride segmentation of the primary +table index. + +Two separate sub-process are launched (num_processes == 2) and each passed the name of their +apportioned pipeline file. They execute independently and if they terminate successfully, their +contents are then coalesced into a single pipeline file whose tables should then be essentially +the same as it had been generated by a single process. + +We assume that any new tables that are created by the sub-processes are directly dependent on the +previously primary tables or are mirrored. Thus we can coalesce the sub-process pipelines by +concatenating the primary and dependent tables and simply retaining any copy of the mirrored tables +(since they should all be identical.) + +The third multiprocess_step (mp_summarize) then is handled in single-process mode and runs the +write_tables model, writing the results, but also leaving the tables in the pipeline, with +essentially the same tables and results as if the whole simulation had been run as a single process. + """ +""" +shared data + +Although multiprocessing subprocesses each have their (apportioned) pipeline, they also share some +data passed to them by the parent process. Ther are essentially two types of shared data. + +read-only shared data + +Skim files are read-only and take up a lot of RAM, so we share them across sub-processes, loading +them into shared-memory (multiprocessing.sharedctypes.RawArray) in the parent process and passing +them to the child sub-processes when they are launched/forked. (unlike ordinary python data, +sharedctypes are not pickled and reconstituted, but passed through to the subprocess by address +when launch/forked multiprocessing.Process. Since they are read-only, no Locks are required to +access their data safely. The receiving process needs to know to wrap them using numpy.frombuffer +but they can thereafter be treated as ordinary numpy arrays. + +read-write shared memory + +There are a few circumstances in which the assumption of row independence breaks down. +This happens if the model must respect some aggregated resource or constraint such as school +enrollments or parking availability. In these cases, the individual choice models have to be +influenced or constrained in light of aggregate choices. + +Currently school and workplace location choice are the only such aggregate constraints. +The details of these are handled by the shadow_pricing module (q.v.), and our only concern here +is the need to provide shared read-write data buffers for communication between processes. +It is worth noting here that the sahred buffers are instances of multiprocessing.Array which +incorporates a multiprocessing.Lock object to mediate access of the underlying data. You might +think that the existence of such a lock would make shared access pretty straightforward, but +this is not the case as the level of locking is very low, reportedly not very performant, and +essentially useless in any event since we want to use numpy.frombuffer to wrap and handle them +as numpy arrays. The Lock is a convenient bundled locking primative, but shadow_pricing rolls +its own semaphore system using the Lock. + +FIXME - The code below knows that it need to allocate skim and shadow price buffers by calling +the appropriate methods in abm.tables.skims and abm.tables.shadow_pricing to allocate shared +buffers. This is not very extensible and should be generalized. + +""" -def filter_warnings(): +# FIXME - pathological knowledge of abm.tables.skims and abm.tables.shadow_pricing (see note above) + + +""" +### child process methods (called within sub process) +""" - if setting('strict', False): # noqa: E402 - import warnings - warnings.filterwarnings('error', category=Warning) - warnings.filterwarnings('default', category=PendingDeprecationWarning, module='future') - warnings.filterwarnings('default', category=FutureWarning, module='pandas') - warnings.filterwarnings('default', category=RuntimeWarning, module='numpy') +def pipeline_table_keys(pipeline_store): + """ + return dict of current (as of last checkpoint) pipeline tables + and their checkpoint-specific hdf5_keys + + This facilitates reading pipeline tables directly from a 'raw' open pandas.HDFStore without + opening it as a pipeline (e.g. when apportioning and coalescing pipelines) + + We currently only ever need to do this from the last checkpoint, so the ability to specify + checkpoint_name is not required, and thus omitted. + + Parameters + ---------- + pipeline_store : open hdf5 pipeline_store + + Returns + ------- + checkpoint_name : name of the checkpoint + checkpoint_tables : dict {: } -def pipeline_table_keys(pipeline_store, checkpoint_name=None): + """ checkpoints = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] - if checkpoint_name: - # specified checkpoint row as series - i = checkpoints[checkpoints[pipeline.CHECKPOINT_NAME] == checkpoint_name].index[0] - checkpoint = checkpoints.loc[i] - else: - # last checkpoint row as series - checkpoint = checkpoints.iloc[-1] - checkpoint_name = checkpoint.loc[pipeline.CHECKPOINT_NAME] + # don't currently need this capability... + # if checkpoint_name: + # # specified checkpoint row as series + # i = checkpoints[checkpoints[pipeline.CHECKPOINT_NAME] == checkpoint_name].index[0] + # checkpoint = checkpoints.loc[i] + # else: + + # last checkpoint row as series + checkpoint = checkpoints.iloc[-1] + checkpoint_name = checkpoint.loc[pipeline.CHECKPOINT_NAME] # series with table name as index and checkpoint_name as value checkpoint_tables = checkpoint[~checkpoint.index.isin(pipeline.NON_TABLE_COLUMNS)] @@ -86,12 +268,91 @@ def pipeline_table_keys(pipeline_store, checkpoint_name=None): return checkpoint_name, checkpoint_tables -def build_slice_rules(slice_info, tables): +def build_slice_rules(slice_info, pipeline_tables): + """ + based on slice_info for current step from run_list, generate a recipe for slicing + the tables in the pipeline (passed in tables parameter) + + slice_info is a dict with two well-known keys: + 'tables': required list of table names (order matters!) + 'except': optional list of tables not to slice even if they have a sliceable index name + + Note: tables listed in slice_info must appear in same order and before any others in tables dict + + The index of the first table in the 'tables' list is the primary_slicer. Subsequent tables + are dependent (have a column with the sae + + Any other tables listed ar dependent tables with either ref_cols to the primary_slicer + or with the same index (i.e. having an index with the same name). This cascades, so any + tables dependent on the primary_table can in turn have dependent tables that will be sliced + by index or ref_col. + + For instance, if the primary_slicer is households, then persons can be sliced because it + has a ref_col to (column with the same same name as) the household table index. And the + tours table can be sliced since it has a ref_col to persons. Tables can also be sliced + by index. For instance the person_windows table can be sliced because it has an index with + the same names as the persons table. + + # slice_info from multiprocess_steps: + :: + slice: + tables: + - households + - persons + + # tables from pipeline + +-----------------+--------------+---------------+ + | Table Name | Index | ref_col | + +=================+==============+===============+ + | households | household_id | | + +-----------------+--------------+---------------+ + | persons | person_id | household_id | + +-----------------+--------------+---------------+ + | person_windows | person_id | | + +-----------------+--------------+---------------+ + | accessibility | zone_id | | + +-----------------+--------------+---------------+ + + # generated slice_rules dict: + :: + + households: + slice_by: primary <- primary table is sliced in num_processors-sized strides + persons: + source: households + slice_by: column + column: household_id <- slice by ref_col (foreign key) to households + person_windows: + source: persons + slice_by: index <- slice by index of persons table + accessibility: + slice_by: <- mirrored (non-dependent) tables don't get sliced + land_use: + slice_by: + + Parameters + ---------- + slice_info : dict + 'slice' info from run_list for this step + + pipeline_tables : dict {, } + dict of all tables from the pipeline keyed by table name + + Returns + ------- + slice_rules : dict + """ slicer_table_names = slice_info['tables'] slicer_table_exceptions = slice_info.get('except', []) primary_slicer = slicer_table_names[0] + # - ensure that tables listed in slice_info appear in correct order and before any others + tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) + + for table_name in pipeline_tables.keys(): + tables[table_name] = pipeline_tables[table_name] + if primary_slicer not in tables: raise RuntimeError("primary slice table '%s' not in pipeline" % primary_slicer) @@ -144,18 +405,36 @@ def build_slice_rules(slice_info, tables): return slice_rules -def apportion_pipeline(sub_job_names, slice_info): +def apportion_pipeline(sub_proc_names, slice_info): + """ + apportion pipeline for multiprocessing step - pipeline_file_name = inject.get_injectable('pipeline_file_name') + create pipeline files for sub_procs, apportioning data based on slice_rules - tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) + Called at the beginning of a multiprocess step prior to launching the sub-processes + Pipeline files have well known names (pipeline file name prefixed by subjob name) + + Parameters + ---------- + sub_proc_names : list of str + names of the sub processes to apportion + slice_info : dict + slice_info from multiprocess_steps + + Returns + ------- + creates apportioned pipeline files for each sub job + """ + + pipeline_file_name = inject.get_injectable('pipeline_file_name') # get last checkpoint from first job pipeline pipeline_path = config.build_output_file_path(pipeline_file_name) logger.debug("apportion_pipeline pipeline_path: %s", pipeline_path) - # load all tables from pipeline + # - load all tables from pipeline + tables = {} with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: checkpoints_df = pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] @@ -164,7 +443,7 @@ def apportion_pipeline(sub_job_names, slice_info): checkpoint_name, hdf5_keys = pipeline_table_keys(pipeline_store) # ensure presence of slicer tables in pipeline - for table_name in tables: + for table_name in slice_info['tables']: if table_name not in hdf5_keys: raise RuntimeError("slicer table %s not found in pipeline" % table_name) @@ -181,14 +460,15 @@ def apportion_pipeline(sub_job_names, slice_info): checkpoints_df = checkpoints_df.tail(1).copy() checkpoints_df[list(tables.keys())] = checkpoint_name - # build slice rules for loaded tables + # - build slice rules for loaded tables slice_rules = build_slice_rules(slice_info, tables) - # allocate sliced tables - num_sub_jobs = len(sub_job_names) - for i in range(num_sub_jobs): + # - allocate sliced tables for each sub_proc + num_sub_procs = len(sub_proc_names) + for i in range(num_sub_procs): - process_name = sub_job_names[i] + # use well-known pipeline file name + process_name = sub_proc_names[i] pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) # remove existing file @@ -201,15 +481,17 @@ def apportion_pipeline(sub_job_names, slice_info): # remember sliced_tables so we can cascade slicing to other tables sliced_tables = {} + + # - for each table in pipeline for table_name, rule in iteritems(slice_rules): df = tables[table_name] if rule['slice_by'] == 'primary': - # slice primary apportion table by num_sub_jobs strides + # slice primary apportion table by num_sub_procs strides # this hopefully yields a more random distribution # (e.g.) households are ordered by size in input store - primary_df = df[np.asanyarray(list(range(df.shape[0]))) % num_sub_jobs == i] + primary_df = df[np.asanyarray(list(range(df.shape[0]))) % num_sub_procs == i] sliced_tables[table_name] = primary_df elif rule['slice_by'] == 'index': # slice a table with same index name as a known slicer @@ -220,16 +502,14 @@ def apportion_pipeline(sub_job_names, slice_info): source_df = sliced_tables[rule['source']] sliced_tables[table_name] = df[df[rule['column']].isin(source_df.index)] elif rule['slice_by'] is None: - # not all tables should be sliced (e.g. land_use) + # don't slice mirrored tables sliced_tables[table_name] = df else: raise RuntimeError("Unrecognized slice rule '%s' for table %s" % (rule['slice_by'], table_name)) + # - write table to pipeline hdf5_key = pipeline.pipeline_table_key(table_name, checkpoint_name) - - logger.debug("writing %s (%s) to %s in %s", - table_name, sliced_tables[table_name].shape, hdf5_key, pipeline_path) pipeline_store[hdf5_key] = sliced_tables[table_name] logger.debug("writing checkpoints (%s) to %s in %s", @@ -237,31 +517,34 @@ def apportion_pipeline(sub_job_names, slice_info): pipeline_store[pipeline.CHECKPOINT_TABLE_NAME] = checkpoints_df -def coalesce_pipelines(sub_process_names, slice_info, use_prefix=True): +def coalesce_pipelines(sub_proc_names, slice_info): + """ + Coalesce the data in the sub_processes apportioned pipelines back into a single pipeline - def sub_pipeline_path(name): - if use_prefix: - # name is prefix - path = config.build_output_file_path(pipeline_file_name, use_prefix=name) - elif os.path.exists(name): - # check if name is valid path - path = name - else: - # otherwise expect to find it on output dir - path = config.build_output_file_path(name) + We use slice_rules to distinguish sliced (apportioned) tables from mirrored tables. + + Sliced tables are concatenated to create a single omnibus table with data from all sub_procs + but mirrored tables are the same across all sub_procs, so we can grab a copy from any pipeline. + + Parameters + ---------- + sub_proc_names : list of str + slice_info : dict + slice_info from multiprocess_steps - return path + Returns + ------- + creates an omnibus pipeline with coalesced data from individual sub_proc pipelines + """ pipeline_file_name = inject.get_injectable('pipeline_file_name') logger.debug("coalesce_pipelines to: %s", pipeline_file_name) - # tables that are identical in every pipeline and so don't need to be concatenated + # - read all tables from first process pipeline + tables = {} + pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=sub_proc_names[0]) - tables = OrderedDict([(table_name, None) for table_name in slice_info['tables']]) - - # read all tables from first process pipeline - pipeline_path = sub_pipeline_path(sub_process_names[0]) with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: # hdf5_keys is a dict mapping table_name to pipeline hdf5_key @@ -271,20 +554,21 @@ def sub_pipeline_path(name): logger.debug("loading table %s %s", table_name, hdf5_key) tables[table_name] = pipeline_store[hdf5_key] - # use slice rules followed by apportion_pipeline to identify singleton tables + # - use slice rules followed by apportion_pipeline to identify mirrored tables + # (tables that are identical in every pipeline and so don't need to be concatenated) slice_rules = build_slice_rules(slice_info, tables) - singleton_table_names = [t for t, rule in iteritems(slice_rules) if rule['slice_by'] is None] - singleton_tables = {t: tables[t] for t in singleton_table_names} - omnibus_keys = {t: k for t, k in iteritems(hdf5_keys) if t not in singleton_table_names} + mirrored_table_names = [t for t, rule in iteritems(slice_rules) if rule['slice_by'] is None] + mirrored_tables = {t: tables[t] for t in mirrored_table_names} + omnibus_keys = {t: k for t, k in iteritems(hdf5_keys) if t not in mirrored_table_names} logger.debug("coalesce_pipelines to: %s", pipeline_file_name) - logger.debug("singleton_table_names: %s", singleton_table_names) + logger.debug("mirrored_table_names: %s", mirrored_table_names) logger.debug("omnibus_keys: %s", omnibus_keys) - # concat omnibus tables from all sub_processes + # assemble lists of omnibus tables from all sub_processes omnibus_tables = {table_name: [] for table_name in omnibus_keys} - for process_name in sub_process_names: - pipeline_path = sub_pipeline_path(process_name) + for process_name in sub_proc_names: + pipeline_path = config.build_output_file_path(pipeline_file_name, use_prefix=process_name) logger.info("coalesce pipeline %s", pipeline_path) with pd.HDFStore(pipeline_path, mode='r') as pipeline_store: @@ -293,10 +577,13 @@ def sub_pipeline_path(name): pipeline.open_pipeline() - for table_name in singleton_tables: - df = singleton_tables[table_name] - logger.info("adding singleton table %s %s", table_name, df.shape) + # - add mirrored tables to pipeline + for table_name in mirrored_tables: + df = mirrored_tables[table_name] + logger.info("adding mirrored table %s %s", table_name, df.shape) pipeline.replace_table(table_name, df) + + # - concatenate omnibus tables and add them to pipeline for table_name in omnibus_tables: df = pd.concat(omnibus_tables[table_name], sort=False) logger.info("adding omnibus table %s %s", table_name, df.shape) @@ -307,64 +594,32 @@ def sub_pipeline_path(name): pipeline.close_pipeline() -def load_skim_data(skim_buffer): - - logger.info("load_skim_data") - - omx_file_path = config.data_file_path(setting('skims_file')) - tags_to_load = setting('skim_time_periods')['labels'] - - skim_info = get_skim_info(omx_file_path, tags_to_load) - load_skims(omx_file_path, skim_info, skim_buffer) - - -def allocate_shared_skim_buffers(): - """ - This is called by the main process and allocate memory buffer to share with subprocs - - Returns - ------- - multiprocessing.RawArray +def setup_injectables_and_logging(injectables, locutor=True): """ + Setup injectables (passed by parent process) within sub process - logger.info("allocate_shared_skim_buffer") + we sometimes want only one of the sub-processes to perform an action (e.g. write shadow prices) + the locutor flag indicates that this sub process is the designated singleton spokesperson - omx_file_path = config.data_file_path(setting('skims_file')) - tags_to_load = setting('skim_time_periods')['labels'] - - # select the skims to load - skim_info = get_skim_info(omx_file_path, tags_to_load) - skim_buffers = buffers_for_skims(skim_info, shared=True) - - return skim_buffers - - -def allocate_shared_shadow_pricing_buffers(): - """ - This is called by the main process and allocate memory buffer to share with subprocs + Parameters + ---------- + injectables : dict {: } + dict of injectables passed by parent process + locutor : bool + is this sub process the designated spokesperson Returns ------- - multiprocessing.RawArray + injects injectables """ - logger.info("allocate_shared_shadow_pricing_buffers") - - shadow_pricing_info = get_shadow_pricing_info() - shadow_pricing_buffers = buffers_for_shadow_pricing(shadow_pricing_info) - - return shadow_pricing_buffers - - -def setup_injectables_and_logging(injectables, locutor=True): - for k, v in iteritems(injectables): inject.add_injectable(k, v) inject.add_injectable("is_sub_task", True) inject.add_injectable("locutor", locutor) - filter_warnings() + config.filter_warnings() process_name = multiprocessing.current_process().name inject.add_injectable("log_file_prefix", process_name) @@ -372,10 +627,27 @@ def setup_injectables_and_logging(injectables, locutor=True): def run_simulation(queue, step_info, resume_after, shared_data_buffer): + """ + run step models as subtask + + called once to run each individual sub process in multiprocess step + + Unless actually resuming resuming, resume_after will be None for first step, + and then FINAL for subsequent steps so pipelines opened to resume where previous step left off + + Parameters + ---------- + queue : multiprocessing.Queue + step_info : dict + step_info for current step from multiprocess_steps + resume_after : str or None + shared_data_buffer : dict + dict of shared data (e.g. skims and shadow_pricing) + """ models = step_info['models'] chunk_size = step_info['chunk_size'] - step_label = step_info['name'] + # step_label = step_info['name'] num_processes = step_info['num_processes'] inject.add_injectable('data_buffers', shared_data_buffer) @@ -386,11 +658,11 @@ def run_simulation(queue, step_info, resume_after, shared_data_buffer): logger.info('resume_after %s', resume_after) # if they specified a resume_after model, check to make sure it is checkpointed - if resume_after != '_' and resume_after \ - not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: + if resume_after != LAST_CHECKPOINT and \ + resume_after not in pipeline.get_checkpoints()[pipeline.CHECKPOINT_NAME].values: # if not checkpointed, then fall back to last checkpoint logger.info("resume_after checkpoint '%s' not in pipeline.", resume_after) - resume_after = '_' + resume_after = LAST_CHECKPOINT pipeline.open_pipeline(resume_after) last_checkpoint = pipeline.last_checkpoint() @@ -416,22 +688,30 @@ def run_simulation(queue, step_info, resume_after, shared_data_buffer): queue.put({'model': model, 'time': time.time()-t1}) - t0 = tracing.print_elapsed_time("run (%s models)" % len(models), t0) + tracing.print_elapsed_time("run (%s models)" % len(models), t0) pipeline.close_pipeline() -def profile_path(): - path = config.output_file_path('%s.prof' % multiprocessing.current_process().name) - return path - - """ - multiprocessing entry points +### multiprocessing sub-process entry points """ def mp_run_simulation(locutor, queue, injectables, step_info, resume_after, **kwargs): + """ + mp entry point for run_simulation + + Parameters + ---------- + locutor + queue + injectables + step_info + resume_after : bool + kwargs : dict + shared_data_buffers passed as kwargs to avoid picking dict + """ shared_data_buffer = kwargs # handle_standard_args() @@ -445,60 +725,159 @@ def mp_run_simulation(locutor, queue, injectables, step_info, resume_after, **kw logger.debug("injecting pipeline_file_prefix '%s'", pipeline_prefix) inject.add_injectable("pipeline_file_prefix", pipeline_prefix) - if setting('profile', False): - cProfile.runctx('run_simulation(queue, step_info, resume_after, skim_buffer)', - globals(), locals(), filename=profile_path()) - else: - run_simulation(queue, step_info, resume_after, shared_data_buffer) + run_simulation(queue, step_info, resume_after, shared_data_buffer) chunk.log_write_hwm() mem.log_hwm() -def mp_apportion_pipeline(injectables, sub_job_proc_names, slice_info): - setup_injectables_and_logging(injectables) +def mp_apportion_pipeline(injectables, sub_proc_names, slice_info): + """ + mp entry point for apportion_pipeline + + Parameters + ---------- + injectables : dict + injectables from parent + sub_proc_names : list of str + names of the sub processes to apportion + slice_info : dict + slice_info from multiprocess_steps + """ - if setting('profile', False): - cProfile.runctx('apportion_pipeline(sub_job_proc_names, slice_info)', - globals(), locals(), filename=profile_path()) - else: - apportion_pipeline(sub_job_proc_names, slice_info) + setup_injectables_and_logging(injectables) + apportion_pipeline(sub_proc_names, slice_info) def mp_setup_skims(injectables, **kwargs): + """ + Sub process to load skim data into shared_data + + There is no particular necessity to perform this in a sub process instead of the parent + except to ensure that this heavyweight task has no side-effects (e.g. loading injectables) + + Parameters + ---------- + injectables : dict + injectables from parent + kwargs : dict + shared_data_buffers passed as kwargs to avoid picking dict + """ + shared_data_buffer = kwargs setup_injectables_and_logging(injectables) - if setting('profile', False): - cProfile.runctx('load_skim_data(skim_buffer)', - globals(), locals(), filename=profile_path()) - else: - load_skim_data(shared_data_buffer) + omx_file_path = config.data_file_path(setting('skims_file')) + tags_to_load = setting('skim_time_periods')['labels'] + + skim_info = skims.get_skim_info(omx_file_path, tags_to_load) + skims.load_skims(omx_file_path, skim_info, shared_data_buffer) -def mp_coalesce_pipelines(injectables, sub_job_proc_names, slice_info): - setup_injectables_and_logging(injectables) +def mp_coalesce_pipelines(injectables, sub_proc_names, slice_info): + """ + mp entry point for coalesce_pipeline - if setting('profile', False): - cProfile.runctx('coalesce_pipelines(sub_job_proc_names, slice_info)', - globals(), locals(), filename=profile_path()) - else: - coalesce_pipelines(sub_job_proc_names, slice_info) + Parameters + ---------- + injectables : dict + injectables from parent + sub_proc_names : list of str + names of the sub processes to apportion + slice_info : dict + slice_info from multiprocess_steps + """ + + setup_injectables_and_logging(injectables) + coalesce_pipelines(sub_proc_names, slice_info) """ - main (parent) process methods +### main (parent) process methods """ +def allocate_shared_skim_buffers(): + """ + This is called by the main process to allocate shared memory buffer to share with subprocs + + Returns + ------- + skim_buffers : dict {: } + + """ + + logger.info("allocate_shared_skim_buffer") + + omx_file_path = config.data_file_path(setting('skims_file')) + tags_to_load = setting('skim_time_periods')['labels'] + + # select the skims to load + skim_info = skims.get_skim_info(omx_file_path, tags_to_load) + skim_buffers = skims.buffers_for_skims(skim_info, shared=True) + + return skim_buffers + + +def allocate_shared_shadow_pricing_buffers(): + """ + This is called by the main process and allocate memory buffer to share with subprocs + + Returns + ------- + multiprocessing.RawArray + """ + + logger.info("allocate_shared_shadow_pricing_buffers") + + shadow_pricing_info = shadow_pricing.get_shadow_pricing_info() + shadow_pricing_buffers = shadow_pricing.buffers_for_shadow_pricing(shadow_pricing_info) + + return shadow_pricing_buffers + + def run_sub_simulations( injectables, shared_data_buffers, step_info, process_names, resume_after, previously_completed, fail_fast): + """ + Launch sub processes to run models in step according to specification in step_info. + + If resume_after is LAST_CHECKPOINT, then pick up where previous run left off, using breadcrumbs + from previous run. If some sub-processes completed in the prior run, then skip rerunning them. + + If resume_after specifies a checkpiont, skip checkpoints that precede the resume_after + + Drop 'completed' breadcrumbs for this run as sub-processes terminate + + Wait for all sub-processes to terminate and return list of those that completed successfully. + Parameters + ---------- + injectables : dict + values to inject in subprocesses + shared_data_buffers : dict + dict of shared_data for sub-processes (e.g. skim and shadow pricing data) + step_info : dict + step_info from run_list + process_names : list of str + list of sub process names to in parallel + resume_after : str or None + name of simulation to resume after, or LAST_CHECKPOINT to resume where previous run left off + previously_completed : list of str + names of processes that successfully completed in previous run + fail_fast : bool + whether to raise error if a sub process terminates with nonzero exitcode + + Returns + ------- + completed : list of str + names of sub_processes that completed successfully + + """ def log_queued_messages(): - for i, process, queue in zip(list(range(num_simulations)), procs, queues): + for process, queue in zip(procs, queues): while not queue.empty(): msg = queue.get(block=False) logger.info("%s %s : %s", process.name, msg['model'], @@ -507,6 +886,7 @@ def log_queued_messages(): def check_proc_status(): # we want to drop 'completed' breadcrumb when it happens, lest we terminate + # if fail_fast flag is set raise for p in procs: if p.exitcode is None: pass # still running @@ -527,12 +907,15 @@ def check_proc_status(): raise RuntimeError("Process %s failed" % (p.name,)) def idle(seconds): + # idle for specified number of seconds, monitoring message queue and sub process status log_queued_messages() check_proc_status() mem.trace_memory_info() for _ in range(seconds): time.sleep(1) + # log queued messages as they are received log_queued_messages() + # monitor sub process status and drop breadcrumbs or fail_fast as they terminate check_proc_status() mem.trace_memory_info() @@ -541,16 +924,25 @@ def idle(seconds): t0 = tracing.print_elapsed_time() logger.info('run_sub_simulations step %s models resume_after %s', step_name, resume_after) + # if resuming and some processes completed successfully in previous run if previously_completed: - assert resume_after == '_' + assert resume_after is not None assert set(previously_completed).issubset(set(process_names)) - process_names = [name for name in process_names if name not in previously_completed] - logger.info('run_sub_simulations step %s: skipping %s previously completed subprocedures', - step_name, len(previously_completed)) + + if resume_after == LAST_CHECKPOINT: + # if we are resuming where previous run left off, then we can skip running + # any subprocudures that successfully complete the previous run + process_names = [name for name in process_names if name not in previously_completed] + logger.info('step %s: skipping %s previously completed subprocedures', + step_name, len(previously_completed)) + else: + # if we are resuming after a specific model, then force all subprocesses to run + # (assuming if they specified a model, they really want everything after that to run) + previously_completed = [] # if not the first step, resume_after the last checkpoint from the previous step if resume_after is None and step_info['step_num'] > 0: - resume_after = '_' + resume_after = LAST_CHECKPOINT num_simulations = len(process_names) procs = [] @@ -558,7 +950,7 @@ def idle(seconds): stagger_starts = step_info['stagger'] completed = set(previously_completed) - failed = set([]) # so we can log process failure when it happens + failed = set([]) # so we can log process failure first time it happens drop_breadcrumb(step_name, 'completed', list(completed)) for i, process_name in enumerate(process_names): @@ -584,9 +976,7 @@ def idle(seconds): idle(seconds=1) idle(seconds=0) - # no need to join explicitly since multiprocessing.active_children joins completed procs - # for p in procs: - # p.join() + # no need to join() explicitly since multiprocessing.active_children joins completed procs for p in procs: assert p.exitcode is not None @@ -603,6 +993,15 @@ def idle(seconds): def run_sub_task(p): + """ + Run process p synchroneously, + + Return when sub process terminates, or raise error if exitcode is nonzero + + Parameters + ---------- + p : multiprocessing.Process + """ logger.info("running sub_process %s", p.name) mem.trace_memory_info("%s.start" % p.name) @@ -628,6 +1027,26 @@ def run_sub_task(p): def drop_breadcrumb(step_name, crumb, value=True): + """ + Add (crumb: value) to specified step in breadcrumbs and flush breadcrumbs to file + run can be resumed with resume_after + + Breadcrumbs provides a record of steps that have been run for use when resuming + Basically, we want to know which steps have been run, which phases completed + (i.e. apportion, simulate, coalesce). For multi-processed simulate steps, we also + want to know which sub-processes completed successfully, because if resume_after + is LAST_CHECKPOINT we don't have to rerun the successful ones. + + Parameters + ---------- + step_name : str + crumb : str + value : yaml-writable value + + Returns + ------- + + """ breadcrumbs = inject.get_injectable('breadcrumbs', OrderedDict()) breadcrumbs.setdefault(step_name, {'name': step_name})[crumb] = value inject.add_injectable('breadcrumbs', breadcrumbs) @@ -635,6 +1054,29 @@ def drop_breadcrumb(step_name, crumb, value=True): def run_multiprocess(run_list, injectables): + """ + run the steps in run_list, possibly resuming after checkpoint specified by resume_after + + Steps may be either single or multi process. + For multi-process steps, we need to apportion pipelines before running sub processes + and coalesce them afterwards + + injectables arg allows propagation of setting values that were overridden on the command line + (parent process command line arguments are not available to sub-processes in Windows) + + * allocate shared data buffers for skims and shadow_pricing + * load shared skim data from OMX files + * run each (single or multiprocess) step in turn + + Drop breadcrumbs along the way to facilitate resuming in a later run + + Parameters + ---------- + run_list : dict + annotated run_list (including prior run breadcrumbs if resuming) + injectables : dict + dict of values to inject in sub-processes + """ mem.init_trace(setting('mem_tick')) mem.trace_memory_info("run_multiprocess.start") @@ -658,6 +1100,7 @@ def skip_phase(phase): def find_breadcrumb(crumb, default=None): return old_breadcrumbs.get(step_name, {}).get(crumb, default) + # - allocate shared data shared_data_buffers = {} t0 = tracing.print_elapsed_time() @@ -665,7 +1108,7 @@ def find_breadcrumb(crumb, default=None): t0 = tracing.print_elapsed_time('allocate shared skim buffer', t0) mem.trace_memory_info("allocate_shared_skim_buffer.completed") - # FIXME combine shared_skim_buffer and shared_shadow_pricing_buffer in shared_data_buffer + # combine shared_skim_buffer and shared_shadow_pricing_buffer in shared_data_buffer t0 = tracing.print_elapsed_time() shared_data_buffers.update(allocate_shared_shadow_pricing_buffers()) t0 = tracing.print_elapsed_time('allocate shared shadow_pricing buffer', t0) @@ -679,6 +1122,7 @@ def find_breadcrumb(crumb, default=None): ) t0 = tracing.print_elapsed_time('setup skims', t0) + # - for each step in run list for step_info in run_list['multiprocess_steps']: step_name = step_info['name'] @@ -704,12 +1148,13 @@ def find_breadcrumb(crumb, default=None): if not skip_phase('simulate'): resume_after = step_info.get('resume_after', None) - completed = find_breadcrumb('completed', default=[]) if resume_after == '_' else [] + previously_completed = find_breadcrumb('completed', default=[]) completed = run_sub_simulations(injectables, shared_data_buffers, step_info, - sub_proc_names, resume_after, completed, fail_fast) + sub_proc_names, + resume_after, previously_completed, fail_fast) if len(completed) != num_processes: raise RuntimeError("%s processes failed in step %s" % @@ -729,25 +1174,55 @@ def find_breadcrumb(crumb, default=None): def get_breadcrumbs(run_list): + """ + Read, validate, and annotate breadcrumb file from previous run + + if resume_after specifies a model name, we need to determine which step it falls within, + drop any subsequent steps, and set the 'simulate' and 'coalesce' to None so + + Extract from breadcrumbs file showing completed mp_households step with 2 processes: + :: + + - apportion: true + completed: [mp_households_0, mp_households_1] + name: mp_households + simulate: true + coalesce: true + + + Parameters + ---------- + run_list : dict + validated and annotated run_list from settings + + Returns + ------- + breadcrumbs : dict + validated and annotated breadcrumbs file from previous run + """ resume_after = run_list['resume_after'] assert resume_after is not None + # - read breadcrumbs file from previous run breadcrumbs = read_breadcrumbs() + # - can't resume multiprocess without breadcrumbs file if not breadcrumbs: logger.error("empty breadcrumbs for resume_after '%s'", resume_after) raise RuntimeError("empty breadcrumbs for resume_after '%s'" % resume_after) - if resume_after == '_': - resume_step_name = list(breadcrumbs.keys())[-1] - else: + # if resume_after is specified by name + if resume_after != LAST_CHECKPOINT: + # breadcrumbs for steps from previous run previous_steps = list(breadcrumbs.keys()) - # run_list step resume_after is in - resume_step_name = next((step['name'] for step in run_list['multiprocess_steps'] - if resume_after in step['models']), None) + # find the run_list step resume_after is in + resume_step = next((step for step in run_list['multiprocess_steps'] + if resume_after in step['models']), None) + + resume_step_name = resume_step['name'] if resume_step_name not in previous_steps: logger.error("resume_after model '%s' not in breadcrumbs", resume_after) @@ -757,19 +1232,13 @@ def get_breadcrumbs(run_list): for step in previous_steps[previous_steps.index(resume_step_name) + 1:]: del breadcrumbs[step] - multiprocess_step = next((step for step in run_list['multiprocess_steps'] - if step['name'] == resume_step_name), []) # type: dict - - if resume_after in multiprocess_step['models'][:-1]: - - # if resume_after is specified by name, and is not the last model in the step - # then we need to rerun the simulations, even if they succeeded - - if breadcrumbs[resume_step_name].get('simulate', None): - breadcrumbs[resume_step_name]['simulate'] = None - - if breadcrumbs[resume_step_name].get('coalesce', None): - breadcrumbs[resume_step_name]['coalesce'] = None + # if resume_after is not the last model in the step + # then we need to rerun the simulations in that step, even if they succeeded + if resume_after in resume_step['models'][:-1]: + if 'simulate' in breadcrumbs[resume_step_name]: + breadcrumbs[resume_step_name]['simulate'] = None + if 'coalesce' in breadcrumbs[resume_step_name]: + breadcrumbs[resume_step_name]['coalesce'] = None multiprocess_step_names = [step['name'] for step in run_list['multiprocess_steps']] if list(breadcrumbs.keys()) != multiprocess_step_names[:len(breadcrumbs)]: @@ -780,14 +1249,64 @@ def get_breadcrumbs(run_list): def get_run_list(): + """ + validate and annotate run_list from settings + + Assign defaults to missing settings (e.g. stagger, chunk_size) + Build individual step model lists based on step starts + If resuming, read breadcrumbs file for info on previous run execution status + + # annotated run_list with two steps, the second with 2 processors + :: + resume_after: None + multiprocess: True + models: + - initialize_landuse + - compute_accessibility + - initialize_households + - school_location + - workplace_location + + multiprocess_steps: + step: mp_initialize + begin: initialize_landuse + name: mp_initialize + models: + - initialize_landuse + - compute_accessibility + - initialize_households + num_processes: 1 + stagger: 5 + chunk_size: 0 + step_num: 0 + step: mp_households + begin: school_location + slice: {'tables': ['households', 'persons']} + name: mp_households + models: + - school_location + - workplace_location + num_processes: 2 + stagger: 5 + chunk_size: 10000 + step_num: 1 + + Returns + ------- + run_list : dict + validated and annotated run_list + """ models = setting('models', []) + multiprocess_steps = setting('multiprocess_steps', []) + resume_after = inject.get_injectable('resume_after', None) or setting('resume_after', None) multiprocess = inject.get_injectable('multiprocess', False) or setting('multiprocess', False) + + # default settings that can be overridden by settings in individual steps global_chunk_size = setting('chunk_size', 0) default_mp_processes = setting('num_processes', 0) or int(1 + multiprocessing.cpu_count() / 2.0) default_stagger = setting('stagger', 0) - multiprocess_steps = setting('multiprocess_steps', []) if multiprocess and multiprocessing.cpu_count() == 1: logger.warning("Can't multiprocess because there is only 1 cpu") @@ -839,9 +1358,6 @@ def get_run_list(): if num_processes == 0: logger.info("Setting num_processes = %s for step %s", num_processes, name) num_processes = default_mp_processes - # if num_processes == 1: - # raise RuntimeError("num_processes = 1 but found slice info for step %s" - # " in multiprocess_steps" % name) if num_processes > multiprocessing.cpu_count(): logger.warning("num_processes setting (%s) greater than cpu count (%s", num_processes, multiprocessing.cpu_count()) @@ -903,12 +1419,12 @@ def get_run_list(): " falls before that of prior step in models list" % (start_tag, start, name, istep)) - # - build step model lists + # - build individual step model lists based on starts starts.append(len(models)) # so last step gets remaining models in list for istep in range(num_steps): step_models = models[starts[istep]: starts[istep + 1]] - if step_models[-1][0] == '_': + if step_models[-1][0] == LAST_CHECKPOINT: raise RuntimeError("Final model '%s' in step %s models list not checkpointed" % (step_models[-1], name)) @@ -916,15 +1432,19 @@ def get_run_list(): run_list['multiprocess_steps'] = multiprocess_steps - # - add resume_breadcrumbs + # - add resume breadcrumbs if resume_after: breadcrumbs = get_breadcrumbs(run_list) if breadcrumbs: run_list['breadcrumbs'] = breadcrumbs - # - add resume_after to resume_step - # FIXME - are we assuming it is in last step? - istep = len(breadcrumbs) - 1 - multiprocess_steps[istep]['resume_after'] = resume_after + + # - add resume_after to last step + if resume_after is not None: + # get_breadcrumbs should have deleted breadcrumbs for any subsequent steps + istep = len(breadcrumbs) - 1 + assert resume_after == LAST_CHECKPOINT or \ + resume_after in multiprocess_steps[istep]['models'] + multiprocess_steps[istep]['resume_after'] = resume_after # - write run list to output dir # use log_file_path so we use (optional) log subdir and prefix process name @@ -935,71 +1455,101 @@ def get_run_list(): def print_run_list(run_list, output_file=None): + """ + Print run_list to stdout or file (informational - not read back in) + + Parameters + ---------- + run_list : dict + output_file : open file + """ if output_file is None: output_file = sys.stdout - s = 'print_run_list' - print(s, file=output_file) - print("resume_after:", run_list['resume_after'], file=output_file) print("multiprocess:", run_list['multiprocess'], file=output_file) - print("models", file=output_file) + print("models:", file=output_file) for m in run_list['models']: print(" - ", m, file=output_file) + # - print multiprocess_steps if run_list['multiprocess']: print("\nmultiprocess_steps:", file=output_file) for step in run_list['multiprocess_steps']: print(" step:", step['name'], file=output_file) for k in step: if isinstance(step[k], list): - print(" ", k, file=output_file) + print(" %s:" % k, file=output_file) for v in step[k]: print(" -", v, file=output_file) else: print(" %s: %s" % (k, step[k]), file=output_file) - if run_list.get('breadcrumbs'): - print("\nbreadcrumbs:", file=output_file) - print_breadcrumbs(run_list['breadcrumbs'], output_file) - - -def print_breadcrumbs(breadcrumbs, output_file=None): - - if output_file is None: - output_file = sys.stdout - - for step_name in breadcrumbs: - step = breadcrumbs[step_name] - print(" step:", step_name, file=output_file) - for k in step: - if isinstance(k, str): - print(" ", k, step[k], file=output_file) - else: - print(" ", k, file=output_file) - for v in step[k]: - print(" ", v, file=output_file) + # - print breadcrumbs + breadcrumbs = run_list.get('breadcrumbs') + if breadcrumbs: + print("\nbreadcrumbs:", file=output_file) + for step_name in breadcrumbs: + step = breadcrumbs[step_name] + print(" step:", step_name, file=output_file) + for k in step: + if isinstance(k, str): + print(" ", k, step[k], file=output_file) + else: + print(" ", k, file=output_file) + for v in step[k]: + print(" ", v, file=output_file) def breadcrumbs_file_path(): + # return path to breadcrumbs file in output_dir return config.build_output_file_path('breadcrumbs.yaml') def read_breadcrumbs(): + """ + Read breadcrumbs file from previous run + + write_breadcrumbs wrote OrderedDict steps as list so ordered is preserved + (step names are duplicated in steps) + + Returns + ------- + breadcrumbs : OrderedDict + """ file_path = breadcrumbs_file_path() if not os.path.exists(file_path): raise IOError("Could not find saved breadcrumbs file '%s'" % file_path) with open(file_path, 'r') as f: breadcrumbs = yaml.load(f) - + # convert array to ordered dict keyed by step name breadcrumbs = OrderedDict([(step['name'], step) for step in breadcrumbs]) return breadcrumbs def write_breadcrumbs(breadcrumbs): + """ + Write breadcrumbs file with execution history of multiprocess run + + Write steps as array so order is preserved (step names are duplicated in steps) + + Extract from breadcrumbs file showing completed mp_households step with 2 processes: + :: + + - apportion: true + coalesce: true + completed: [mp_households_0, mp_households_1] + name: mp_households + simulate: true + + Parameters + ---------- + breadcrumbs : OrderedDict + """ with open(breadcrumbs_file_path(), 'w') as f: + # write ordered dict as array breadcrumbs = [step for step in list(breadcrumbs.values())] yaml.dump(breadcrumbs, f) diff --git a/activitysim/core/pipeline.py b/activitysim/core/pipeline.py index 6eb6bcf4f..fbf989e53 100644 --- a/activitysim/core/pipeline.py +++ b/activitysim/core/pipeline.py @@ -41,6 +41,12 @@ # name of the first step/checkpoint created when teh pipeline is started INITIAL_CHECKPOINT_NAME = 'init' +# special value for resume_after meaning last checkpoint +LAST_CHECKPOINT = '_' + +# single character prefix for run_list model name to indicate that no checkpoint should be saved +NO_CHECKPOINT_PREFIX = '_' + class Pipeline(object): def __init__(self): @@ -72,6 +78,12 @@ def rng(self): _PIPELINE = Pipeline() +def be_open(): + + if not _PIPELINE.is_open: + raise RuntimeError("Pipeline is not open!") + + def pipeline_table_key(table_name, checkpoint_name): if checkpoint_name: key = "%s/%s" % (table_name, checkpoint_name) @@ -337,8 +349,7 @@ def load_checkpoint(checkpoint_name): checkpoints = read_df(CHECKPOINT_TABLE_NAME) - # '_' means load last checkpoint - if checkpoint_name == '_': + if checkpoint_name == LAST_CHECKPOINT: checkpoint_name = checkpoints[CHECKPOINT_NAME].iloc[-1] logger.info("loading checkpoint '%s'" % checkpoint_name) @@ -449,7 +460,7 @@ def run_model(model_name): args = {} # check for no_checkpoint prefix - if step_name[0] == '_': + if step_name[0] == NO_CHECKPOINT_PREFIX: step_name = step_name[1:] checkpoint = False else: @@ -484,7 +495,7 @@ def open_pipeline(resume_after=None): name of checkpoint to load from pipeline store """ - logger.info("open_pipeline...") + logger.info("open_pipeline") if _PIPELINE.is_open: raise RuntimeError("Pipeline is already open!") @@ -520,8 +531,7 @@ def last_checkpoint(): name of last checkpoint """ - if not _PIPELINE.is_open: - raise RuntimeError("Pipeline is not open!") + be_open() return _PIPELINE.last_checkpoint[CHECKPOINT_NAME] @@ -531,8 +541,7 @@ def close_pipeline(): Close any known open files """ - if not _PIPELINE.is_open: - raise RuntimeError("Pipeline is not open!") + be_open() close_open_files() @@ -567,7 +576,7 @@ def run(models, resume_after=None): open_pipeline(resume_after) t0 = print_elapsed_time('open_pipeline', t0) - if resume_after == '_': + if resume_after == LAST_CHECKPOINT: resume_after = _PIPELINE.last_checkpoint[CHECKPOINT_NAME] if resume_after: @@ -610,6 +619,8 @@ def get_table(table_name, checkpoint_name=None): df : pandas.DataFrame """ + be_open() + # orca table not in checkpoints (e.g. a merged table) if table_name not in _PIPELINE.last_checkpoint and orca.is_table(table_name): if checkpoint_name is not None: @@ -653,6 +664,8 @@ def get_checkpoints(): """ Get pandas dataframe of info about all checkpoints stored in pipeline + pipeline doesn't have to be open + Returns ------- checkpoints_df : pandas.DataFrame @@ -695,6 +708,8 @@ def replace_table(table_name, df): df : pandas DataFrame """ + be_open() + rewrap(table_name, df) _PIPELINE.replaced_tables[table_name] = True @@ -711,6 +726,8 @@ def extend_table(table_name, df, axis=0): df : pandas DataFrame """ + be_open() + assert axis in [0, 1] if orca.is_table(table_name): @@ -743,6 +760,8 @@ def extend_table(table_name, df, axis=0): def drop_table(table_name): + be_open() + if orca.is_table(table_name): logger.debug("drop_table dropping orca table '%s'" % table_name) diff --git a/example_azure/benchmarks/benchmarks_full_run.txt b/example_azure/benchmarks/benchmarks_full_run.txt index a70ba5103..675510e9d 100644 --- a/example_azure/benchmarks/benchmarks_full_run.txt +++ b/example_azure/benchmarks/benchmarks_full_run.txt @@ -2,7 +2,6 @@ # run1 mem_tick: 30 -profile: False strict: False households_sample_size: 0 chunk_size: 20000000000 @@ -15,7 +14,6 @@ INFO - activitysim.core.tracing - Time to execute everything : 7731.622 seconds # run2 mem_tick: 30 -profile: False strict: False households_sample_size: 0 chunk_size: 0 @@ -30,7 +28,6 @@ INFO - activitysim.core.tracing - Time to execute everything : 7880.258 seconds multiprocess: True mem_tick: 30 -profile: False strict: False households_sample_size: 0 chunk_size: 90000000000 @@ -64,7 +61,6 @@ export OMP_NUM_THREADS=1 multiprocess: True mem_tick: 0 -profile: False strict: False households_sample_size: 0 diff --git a/example_azure/benchmarks/benchmarks_stride_run.txt b/example_azure/benchmarks/benchmarks_stride_run.txt index b1b9fba2d..dd257a47b 100644 --- a/example_azure/benchmarks/benchmarks_stride_run.txt +++ b/example_azure/benchmarks/benchmarks_stride_run.txt @@ -5,7 +5,6 @@ python simulation.py -s 2,0 -d /datadrive/work/data/full multiprocess: True mem_tick: 0 -profile: False strict: False households_sample_size: 0 diff --git a/example_azure/example_mp/settings.yaml b/example_azure/example_mp/settings.yaml index 005da9314..060c5caa1 100644 --- a/example_azure/example_mp/settings.yaml +++ b/example_azure/example_mp/settings.yaml @@ -6,7 +6,6 @@ fail_fast: True # - ------------------------- production config multiprocess: True -profile: False strict: False mem_tick: 0 use_shadow_pricing: True @@ -33,7 +32,6 @@ use_shadow_pricing: True ## - ------------------------- dev config #multiprocess: True -#profile: False #strict: False #mem_tick: 30 #use_shadow_pricing: True @@ -48,7 +46,6 @@ use_shadow_pricing: True #households_sample_size: 0 #chunk_size: 1000000000 #num_processes: 1 -#households_sample_stride: [120,0] # - tracing trace_hh_id: diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index ea3551e77..1f8ee1cc4 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -6,7 +6,6 @@ fail_fast: True # - ------------------------- production config multiprocess: True -profile: False strict: False mem_tick: 0 use_shadow_pricing: True @@ -27,7 +26,6 @@ stagger: 0 ## - ------------------------- dev config #multiprocess: True -#profile: False #strict: False #mem_tick: 30 #use_shadow_pricing: True @@ -42,7 +40,6 @@ stagger: 0 #households_sample_size: 0 #chunk_size: 1000000000 #num_processes: 1 -#households_sample_stride: [120,0] # - tracing trace_hh_id: diff --git a/example_mp/simulation.py b/example_mp/simulation.py index 394f12e20..67703cf25 100644 --- a/example_mp/simulation.py +++ b/example_mp/simulation.py @@ -72,7 +72,7 @@ def log_settings(injectables): injectables = config.handle_standard_args() - mp_tasks.filter_warnings() + config.filter_warnings() tracing.config_logger() log_settings(injectables) @@ -92,11 +92,10 @@ def log_settings(injectables): else: injectables = None - if config.setting('profile', False): - import cProfile - cProfile.runctx('run(run_list, injectables)', - globals(), locals(), filename=config.output_file_path('simulation.prof')) - else: - run(run_list, injectables) + run(run_list, injectables) + + # pipeline will be close if multiprocessing + # if you want access to tables, BE SURE TO OPEN WITH '_' or all tables will be reinitialized + # pipeline.open_pipeline('_') t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_stride/.gitignore b/example_stride/.gitignore deleted file mode 100644 index c80916134..000000000 --- a/example_stride/.gitignore +++ /dev/null @@ -1 +0,0 @@ -output_*/ diff --git a/example_stride/coalesce.py b/example_stride/coalesce.py deleted file mode 100644 index 5d4079358..000000000 --- a/example_stride/coalesce.py +++ /dev/null @@ -1,46 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -from __future__ import (absolute_import, division, print_function, ) -from future.standard_library import install_aliases -install_aliases() # noqa: E402 - -import sys -import logging -import argparse -import os - -from activitysim.core import mem -from activitysim.core import inject -from activitysim.core import tracing -from activitysim.core import config -from activitysim.core import pipeline -from activitysim.core import mp_tasks -from activitysim.core import chunk - -# from activitysim import abm - - -logger = logging.getLogger('activitysim') - - -if __name__ == '__main__': - - inject.add_injectable('configs_dir', ['configs', '../example/configs']) - - config.handle_standard_args() - - mp_tasks.filter_warnings() - tracing.config_logger() - - t0 = tracing.print_elapsed_time() - - coalesce_rules = config.setting('coalesce') - - mp_tasks.coalesce_pipelines(coalesce_rules['names'], coalesce_rules['slice'], use_prefix=False) - - checkpoints_df = pipeline.get_checkpoints() - file_path = config.output_file_path('coalesce_checkpoints.csv') - checkpoints_df.to_csv(file_path, index=True) - - t0 = tracing.print_elapsed_time("everything", t0) diff --git a/example_stride/configs_report/logging.yaml b/example_stride/configs_report/logging.yaml deleted file mode 100644 index e4da6d784..000000000 --- a/example_stride/configs_report/logging.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# Config for logging -# ------------------ -# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema - -logging: - version: 1 - disable_existing_loggers: true - - - # Configuring the default (root) logger is highly recommended - root: - level: DEBUG - handlers: [console, logfile] - - loggers: - - activitysim: - level: DEBUG - handlers: [console, logfile] - propagate: false - - orca: - level: WARNING - handlers: [console, logfile] - propagate: false - - handlers: - - logfile: - class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] - mode: w - formatter: fileFormatter - level: NOTSET - - console: - class: logging.StreamHandler - stream: ext://sys.stdout - formatter: simpleFormatter - #level: WARNING - level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] - - formatters: - - simpleFormatter: - class: logging.Formatter - #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' - format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ - '%(processName)-10s %(levelname)s - %(name)s - %(message)s', - '%(levelname)s - %(name)s - %(message)s'] - datefmt: '%d/%m/%Y %H:%M:%S' - - fileFormatter: - class: logging.Formatter - format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' - datefmt: '%d/%m/%Y %H:%M:%S' - diff --git a/example_stride/configs_report/settings.yaml b/example_stride/configs_report/settings.yaml deleted file mode 100644 index 0bfa50ec4..000000000 --- a/example_stride/configs_report/settings.yaml +++ /dev/null @@ -1,43 +0,0 @@ - -inherit_settings: True - -multiprocess: False -mem_tick: 30 -profile: False -strict: False -num_processes: 1 - -households_sample_size: 0 -chunk_size: 0 - -# - tracing -trace_hh_id: -trace_od: - -resume_after: trip_mode_choice - -models: - - write_data_dictionary - - write_tables - -output_tables: - action: include - prefix: final_ - tables: - - checkpoints -# - accessibility -# - land_use -# - households -# - persons -# - trips -# - tours - - -coalesce: - names: - - output_0/pipeline.h5 - - output_1/pipeline.h5 - slice: - tables: - - households - - persons diff --git a/example_stride/configs_sim/logging.yaml b/example_stride/configs_sim/logging.yaml deleted file mode 100644 index e4da6d784..000000000 --- a/example_stride/configs_sim/logging.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# Config for logging -# ------------------ -# See http://docs.python.org/2.7/library/logging.config.html#configuration-dictionary-schema - -logging: - version: 1 - disable_existing_loggers: true - - - # Configuring the default (root) logger is highly recommended - root: - level: DEBUG - handlers: [console, logfile] - - loggers: - - activitysim: - level: DEBUG - handlers: [console, logfile] - propagate: false - - orca: - level: WARNING - handlers: [console, logfile] - propagate: false - - handlers: - - logfile: - class: logging.FileHandler - filename: !!python/object/apply:activitysim.core.config.log_file_path ['activitysim.log'] - mode: w - formatter: fileFormatter - level: NOTSET - - console: - class: logging.StreamHandler - stream: ext://sys.stdout - formatter: simpleFormatter - #level: WARNING - level: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [INFO, NOTSET] - - formatters: - - simpleFormatter: - class: logging.Formatter - #format: '%(processName)-10s %(levelname)s - %(name)s - %(message)s' - format: !!python/object/apply:activitysim.core.mp_tasks.if_sub_task [ - '%(processName)-10s %(levelname)s - %(name)s - %(message)s', - '%(levelname)s - %(name)s - %(message)s'] - datefmt: '%d/%m/%Y %H:%M:%S' - - fileFormatter: - class: logging.Formatter - format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' - datefmt: '%d/%m/%Y %H:%M:%S' - diff --git a/example_stride/configs_sim/settings.yaml b/example_stride/configs_sim/settings.yaml deleted file mode 100644 index f8657c0b4..000000000 --- a/example_stride/configs_sim/settings.yaml +++ /dev/null @@ -1,65 +0,0 @@ - -inherit_settings: True - - -# - dev config mini -multiprocess: True -mem_tick: 30 -profile: False -strict: False - -households_sample_size: 600 -chunk_size: 1500000000 -num_processes: 3 -stagger: 15 - - -# - tracing -trace_hh_id: -trace_od: - -models: - - initialize_landuse - - compute_accessibility - - initialize_households - - school_location - - _workplace_location_sample - - _workplace_location_logsums - - workplace_location_simulate - - auto_ownership_simulate - - cdap_simulate - - mandatory_tour_frequency - - mandatory_tour_scheduling - - joint_tour_frequency - - joint_tour_composition - - joint_tour_participation - - _joint_tour_destination_sample - - _joint_tour_destination_logsums - - joint_tour_destination_simulate - - joint_tour_scheduling - - non_mandatory_tour_frequency - - non_mandatory_tour_destination - - non_mandatory_tour_scheduling - - tour_mode_choice_simulate - - atwork_subtour_frequency - - _atwork_subtour_destination_sample - - _atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate - - atwork_subtour_scheduling - - atwork_subtour_mode_choice - - stop_frequency - - trip_purpose - - trip_destination - - trip_purpose_and_destination - - trip_scheduling - - trip_mode_choice - -multiprocess_steps: - - name: mp_initialize - begin: initialize_landuse - - name: mp_households - begin: school_location - slice: - tables: - - households - - persons diff --git a/example_stride/output/.gitignore b/example_stride/output/.gitignore deleted file mode 100644 index c925c5c3d..000000000 --- a/example_stride/output/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.csv -*.log -*.prof -*.h5 -*.txt -*.yaml - diff --git a/example_stride/output/log/.gitignore b/example_stride/output/log/.gitignore deleted file mode 100644 index 031b1db6c..000000000 --- a/example_stride/output/log/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.csv -*.log diff --git a/example_stride/output/trace/.gitignore b/example_stride/output/trace/.gitignore deleted file mode 100644 index afed0735d..000000000 --- a/example_stride/output/trace/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.csv diff --git a/example_stride/script.txt b/example_stride/script.txt deleted file mode 100644 index 29a30a69d..000000000 --- a/example_stride/script.txt +++ /dev/null @@ -1,21 +0,0 @@ - -#DATA_DIR=E:\Projects\Clients\ASIM\full_example_input_data -DATA_DIR=~/work/activitysim-data/mtc_tm1_sf/data - -rm -r output_0; cp -r output output_0 -python simulation.py -s 2,0 -o output_0 -c configs_sim -c ../example/configs -d $DATA_DIR - -rm -r output_1; cp -r output output_1 -python simulation.py -s 2,1 -o output_1 -c configs_sim -c ../example/configs -d $DATA_DIR - -rm -r output_report; cp -r output output_report -python coalesce.py -o output_report -c configs_report -c ../example/configs - -# run final report tasks -python simulation.py -o output_report -c configs_report -c ../example/configs - - -rm -r output_0 -rm -r output_1 -rm -r output_report - diff --git a/example_stride/simulation.py b/example_stride/simulation.py deleted file mode 100644 index 394f12e20..000000000 --- a/example_stride/simulation.py +++ /dev/null @@ -1,102 +0,0 @@ -# ActivitySim -# See full license in LICENSE.txt. - -from __future__ import (absolute_import, division, print_function, ) -from future.standard_library import install_aliases -install_aliases() # noqa: E402 - -import sys -import logging - -from activitysim.core import mem -from activitysim.core import inject -from activitysim.core import tracing -from activitysim.core import config -from activitysim.core import pipeline -from activitysim.core import mp_tasks -from activitysim.core import chunk - -# from activitysim import abm - - -logger = logging.getLogger('activitysim') - - -def cleanup_output_files(): - - active_log_files = \ - [h.baseFilename for h in logger.root.handlers if isinstance(h, logging.FileHandler)] - tracing.delete_output_files('log', ignore=active_log_files) - - tracing.delete_output_files('h5') - tracing.delete_output_files('csv') - tracing.delete_output_files('txt') - tracing.delete_output_files('yaml') - tracing.delete_output_files('prof') - - -def run(run_list, injectables=None): - - if run_list['multiprocess']: - logger.info("run multiprocess simulation") - mp_tasks.run_multiprocess(run_list, injectables) - else: - logger.info("run single process simulation") - pipeline.run(models=run_list['models'], resume_after=run_list['resume_after']) - pipeline.close_pipeline() - chunk.log_write_hwm() - - -def log_settings(injectables): - - settings = [ - 'households_sample_size', - 'chunk_size', - 'multiprocess', - 'num_processes', - 'resume_after', - ] - - for k in settings: - logger.info("setting %s: %s" % (k, config.setting(k))) - - for k in injectables: - logger.info("injectable %s: %s" % (k, inject.get_injectable(k))) - - -if __name__ == '__main__': - - # inject.add_injectable('data_dir', '/Users/jeff.doyle/work/activitysim-data/mtc_tm1/data') - inject.add_injectable('data_dir', '../example/data') - inject.add_injectable('configs_dir', ['configs', '../example/configs']) - - injectables = config.handle_standard_args() - - mp_tasks.filter_warnings() - tracing.config_logger() - - log_settings(injectables) - - t0 = tracing.print_elapsed_time() - - # cleanup if not resuming - if not config.setting('resume_after', False): - cleanup_output_files() - - run_list = mp_tasks.get_run_list() - - if run_list['multiprocess']: - # do this after config.handle_standard_args, as command line args may override injectables - injectables = list(set(injectables) | set(['data_dir', 'configs_dir', 'output_dir'])) - injectables = {k: inject.get_injectable(k) for k in injectables} - else: - injectables = None - - if config.setting('profile', False): - import cProfile - cProfile.runctx('run(run_list, injectables)', - globals(), locals(), filename=config.output_file_path('simulation.prof')) - else: - run(run_list, injectables) - - t0 = tracing.print_elapsed_time("everything", t0) From fa8da76dc227285706dc770f0a129ff7b4dd7288 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 11 Jan 2019 10:42:50 -0500 Subject: [PATCH 069/122] docstrings for skim OffsetMapper --- activitysim/core/skim.py | 65 ++++++++++++++++++++++------------------ activitysim/core/util.py | 2 +- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/activitysim/core/skim.py b/activitysim/core/skim.py index e3189e49c..f7ca29d6c 100644 --- a/activitysim/core/skim.py +++ b/activitysim/core/skim.py @@ -24,17 +24,33 @@ class OffsetMapper(object): + """ + Utility to map skim zone ids to ordinal offsets (e.g. numpy array indices) + + Can map either by a fixed offset (e.g. -1 to map 1-based to 0-based) + or by an explicit mapping of zone id to offset (slower but more flexible) + """ def __init__(self, offset_int=None): self.offset_series = None self.offset_int = offset_int def set_offset_list(self, offset_list): + """ + Specify the zone ids corresponding to the offsets (ordinal positions) + + set_offset_list([10, 20, 30, 40]) + map([30, 20, 40]) + returns offsets [2, 1, 3] + Parameters + ---------- + offset_list : list of int + """ assert isinstance(offset_list, list) assert self.offset_int is None - # for performance, check if this is a simple int-based series + # - for performance, check if this is a simple int-based series first_offset = offset_list[0] if (offset_list == list(range(first_offset, len(offset_list)+first_offset))): offset_int = -1 * first_offset @@ -49,6 +65,13 @@ def set_offset_list(self, offset_list): assert (offset_list == self.offset_series.index).all() def set_offset_int(self, offset_int): + """ + specify fixed offset (e.g. -1 to map 1-based to 0-based) + + Parameters + ---------- + offset_int : int + """ # should be some kind of integer assert int(offset_int) == offset_int @@ -61,6 +84,17 @@ def set_offset_int(self, offset_int): assert offset_int == self.offset_int def map(self, zone_ids): + """ + map zone_ids to offsets + + Parameters + ---------- + zone_ids + + Returns + ------- + offsets : numpy array of int + """ # print "\nmap_offsets zone_ids", zone_ids @@ -115,33 +149,6 @@ def get(self, orig, dest): """ - # if False: #fixme SLOWER - # # fixme - I don't think we need to support nan orig, dest values - # - # # only working with numpy in here - # orig = np.asanyarray(orig) - # dest = np.asanyarray(dest) - # out_shape = orig.shape - # - # # filter orig and dest to only the real-number pairs - # notnan = ~(np.isnan(orig) | np.isnan(dest)) - # - # orig = orig[notnan].astype('int') - # dest = dest[notnan].astype('int') - # - # orig = self.offset_mapper.map(orig) - # dest = self.offset_mapper.map(dest) - # - # result = self.data[orig, dest] - # - # # add the nans back to the result - # # (np.empty ensures result type is np.float64 to support nans) - # out = np.empty(out_shape) - # out[notnan] = result - # out[~notnan] = np.nan - # - # return out - # fixme - remove? assert not (np.isnan(orig) | np.isnan(dest)).any() @@ -403,7 +410,7 @@ def wrap(self, left_key, right_key, skim_key): class SkimStackWrapper(object): """ - A SkimStackWrapper object wraps a skims object to add an additional wrinkle of + A SkimStackWrapper object wraps a SkimStack object to add an additional wrinkle of lookup functionality. Upon init the separate skims objects are processed into a 3D matrix so that lookup of the different skims can be performed quickly for each row in the dataframe. In this very diff --git a/activitysim/core/util.py b/activitysim/core/util.py index c8f85e804..daac8e75f 100644 --- a/activitysim/core/util.py +++ b/activitysim/core/util.py @@ -229,7 +229,7 @@ def quick_loc_series(loc_list, target_series): left_df = pd.DataFrame({left_on: loc_list.values}) elif isinstance(loc_list, pd.Series): left_df = loc_list.to_frame(name=left_on) - elif isinstance(loc_list, np.ndarray): + elif isinstance(loc_list, np.ndarray) or isinstance(loc_list, list): left_df = pd.DataFrame({left_on: loc_list}) else: raise RuntimeError("quick_loc_series loc_list of unexpected type %s" % type(loc_list)) From 61b8d9791cc9b3bd6a4fadac17b6a2277ca508b8 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 11 Jan 2019 14:53:52 -0500 Subject: [PATCH 070/122] consolidate joint tour destination --- .../abm/models/joint_tour_destination.py | 125 +++++++++++------- activitysim/abm/test/configs/settings.yaml | 4 +- docs/abmexample.rst | 4 +- example/configs/settings.yaml | 4 +- example_mp/configs/settings.yaml | 4 +- 5 files changed, 81 insertions(+), 60 deletions(-) diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index 3d7484762..b2d897825 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -45,9 +45,8 @@ ]) -@inject.step() def joint_tour_destination_sample( - tours, + joint_tours, households_merged, skim_dict, land_use, size_terms, @@ -79,9 +78,8 @@ def joint_tour_destination_sample( Parameters ---------- - tours: pipeline table - households_merged : pipeline table - injected merge table created on the fly + joint_tours: pandas.DataFrame + households_merged : pandas.DataFrame skim_dict joint_tour_destination_sample_spec land_use @@ -92,7 +90,8 @@ def joint_tour_destination_sample( Returns ------- - none + choices : pandas.DataFrame + destination_sample df """ @@ -100,16 +99,6 @@ def joint_tour_destination_sample( model_settings = config.read_model_settings('joint_tour_destination.yaml') model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_destination_sample.csv') - joint_tours = tours.to_frame() - joint_tours = joint_tours[joint_tours.tour_category == 'joint'] - - # - if no joint tours - if joint_tours.shape[0] == 0: - tracing.no_results(trace_label) - return - - households_merged = households_merged.to_frame() - # same size terms as non_mandatory size_terms = tour_destination_size_terms(land_use, size_terms, 'non_mandatory') @@ -186,18 +175,18 @@ def joint_tour_destination_sample( # - NARROW choices['tour_type_id'] = choices['tour_type_id'].astype(np.uint8) - inject.add_table('joint_tour_destination_sample', choices) - if trace_hh_id: tracing.trace_df(choices, label="joint_tour_destination_sample", transpose=True) + return choices + -@inject.step() def joint_tour_destination_logsums( - tours, + joint_tours, persons_merged, + destination_sample, skim_dict, skim_stack, chunk_size, trace_hh_id): @@ -211,21 +200,9 @@ def joint_tour_destination_logsums( trace_label = 'joint_tour_destination_logsums' - # use inject.get_table as this won't exist if there are no joint_tours - destination_sample = inject.get_table('joint_tour_destination_sample', default=None) - if destination_sample is None: - tracing.no_results(trace_label) - return - - destination_sample = destination_sample.to_frame() - model_settings = config.read_model_settings('joint_tour_destination.yaml') logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - joint_tours = tours.to_frame() - joint_tours = joint_tours[joint_tours.tour_category == 'joint'] - - persons_merged = persons_merged.to_frame() joint_tours_merged = pd.merge(joint_tours, persons_merged, left_on='person_id', right_index=True, how='left') @@ -269,17 +246,18 @@ def joint_tour_destination_logsums( logsums = pd.concat(logsums_list) destination_sample['mode_choice_logsum'] = logsums - pipeline.replace_table("joint_tour_destination_sample", destination_sample) if trace_hh_id: - tracing.trace_df(destination_sample, - label="joint_tour_destination_logsums") + tracing.trace_df(destination_sample, label="joint_tour_destination_logsums") + + return destination_sample @inject.step() def joint_tour_destination_simulate( - tours, + joint_tours, households_merged, + destination_sample, skim_dict, land_use, size_terms, chunk_size, trace_hh_id): @@ -290,26 +268,14 @@ def joint_tour_destination_simulate( trace_label = 'joint_tour_destination_simulate' - # use inject.get_table as this won't exist if there are no joint_tours - destination_sample = inject.get_table('joint_tour_destination_sample', default=None) - if destination_sample is None: - tracing.no_results(trace_label) - return - model_settings = config.read_model_settings('joint_tour_destination.yaml') # - tour types are subset of non_mandatory tour types and use same expressions model_spec = simulate.read_model_spec(file_name='non_mandatory_tour_destination.csv') - destination_sample = destination_sample.to_frame() - tours = tours.to_frame() - joint_tours = tours[tours.tour_category == 'joint'] - # interaction_sample_simulate insists choosers appear in same order as alts joint_tours = joint_tours.sort_index() - households_merged = households_merged.to_frame() - destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'non_mandatory') alt_dest_col_name = model_settings["ALT_DEST_COL_NAME"] @@ -380,6 +346,69 @@ def joint_tour_destination_simulate( choices = pd.concat(choices_list) + return choices + + +@inject.step() +def joint_tour_destination( + tours, + persons_merged, + households_merged, + skim_dict, + skim_stack, + land_use, size_terms, + chunk_size, trace_hh_id): + """ + Run the three-part destination choice algorithm to choose a destination for each joint tour + + Parameters + ---------- + tours : injected table + households_merged : injected table + skim_dict : skim.SkimDict + land_use : injected table + size_terms : injected table + chunk_size : int + trace_hh_id : int or None + + Returns + ------- + adds/assigns choice column 'destination' for joint tours in tours table + """ + + tours = tours.to_frame() + joint_tours = tours[tours.tour_category == 'joint'] + + persons_merged = persons_merged.to_frame() + households_merged = households_merged.to_frame() + + # - if no joint tours + if joint_tours.shape[0] == 0: + tracing.no_results('joint_tour_destination') + return + + destination_sample = joint_tour_destination_sample( + joint_tours, + households_merged, + skim_dict, + land_use, size_terms, + chunk_size, trace_hh_id) + + destination_sample = joint_tour_destination_logsums( + joint_tours, + persons_merged, + destination_sample, + skim_dict, skim_stack, + chunk_size, trace_hh_id) + + choices = joint_tour_destination_simulate( + joint_tours, + households_merged, + destination_sample, + skim_dict, + land_use, size_terms, + chunk_size, trace_hh_id) + # add column as we want joint_tours table for tracing. joint_tours['destination'] = choices diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 2efe3e116..c3b590c03 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -43,9 +43,7 @@ models: - joint_tour_frequency - joint_tour_composition - joint_tour_participation - - joint_tour_destination_sample - - joint_tour_destination_logsums - - joint_tour_destination_simulate + - joint_tour_destination - joint_tour_scheduling - non_mandatory_tour_frequency - non_mandatory_tour_destination diff --git a/docs/abmexample.rst b/docs/abmexample.rst index c26aff96f..734baecb8 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -521,9 +521,7 @@ The ``models`` setting contains the specification of the data pipeline model ste - joint_tour_frequency - joint_tour_composition - joint_tour_participation - - joint_tour_destination_sample - - joint_tour_destination_logsums - - joint_tour_destination_simulate + - joint_tour_destination - joint_tour_scheduling - non_mandatory_tour_frequency - non_mandatory_tour_destination diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index 2efe3e116..c3b590c03 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -43,9 +43,7 @@ models: - joint_tour_frequency - joint_tour_composition - joint_tour_participation - - joint_tour_destination_sample - - joint_tour_destination_logsums - - joint_tour_destination_simulate + - joint_tour_destination - joint_tour_scheduling - non_mandatory_tour_frequency - non_mandatory_tour_destination diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index 1f8ee1cc4..d16811edf 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -65,9 +65,7 @@ models: - joint_tour_frequency - joint_tour_composition - joint_tour_participation - - _joint_tour_destination_sample - - _joint_tour_destination_logsums - - joint_tour_destination_simulate + - joint_tour_destination - joint_tour_scheduling - non_mandatory_tour_frequency - non_mandatory_tour_destination From 9ae088cfb526ac98f0d7167851b8c0b94ea23096 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 11 Jan 2019 16:33:14 -0500 Subject: [PATCH 071/122] consolidate atwork_subtour_destination --- .../abm/models/atwork_subtour_destination.py | 132 ++++++++++-------- .../abm/models/joint_tour_destination.py | 3 - activitysim/abm/test/configs/settings.yaml | 4 +- docs/abmexample.rst | 4 +- example/configs/settings.yaml | 4 +- example_mp/configs/settings.yaml | 4 +- 6 files changed, 75 insertions(+), 76 deletions(-) diff --git a/activitysim/abm/models/atwork_subtour_destination.py b/activitysim/abm/models/atwork_subtour_destination.py index 2ee1da094..f677a8d1d 100644 --- a/activitysim/abm/models/atwork_subtour_destination.py +++ b/activitysim/abm/models/atwork_subtour_destination.py @@ -26,35 +26,23 @@ DUMP = False -@inject.step() -def atwork_subtour_destination_sample(tours, - persons_merged, - skim_dict, - land_use, size_terms, - chunk_size, trace_hh_id): +def atwork_subtour_destination_sample( + tours, + persons_merged, + skim_dict, + destination_size_terms, + chunk_size, trace_hh_id): trace_label = 'atwork_subtour_location_sample' model_settings = config.read_model_settings('atwork_subtour_destination.yaml') model_spec = simulate.read_model_spec(file_name='atwork_subtour_destination_sample.csv') - persons_merged = persons_merged.to_frame() - - tours = tours.to_frame() - tours = tours[tours.tour_category == 'atwork'] - - # - if no atwork subtours - if tours.shape[0] == 0: - tracing.no_results(trace_label) - return - # merge persons into tours choosers = pd.merge(tours, persons_merged, left_on='person_id', right_index=True) # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] choosers = choosers[chooser_columns] - alternatives = tour_destination_size_terms(land_use, size_terms, 'atwork') - constants = config.get_model_constants(model_settings) sample_size = model_settings["SAMPLE_SIZE"] @@ -75,7 +63,7 @@ def atwork_subtour_destination_sample(tours, choices = interaction_sample( choosers, - alternatives, + alternatives=destination_size_terms, sample_size=sample_size, alt_col_name=alt_dest_col_name, spec=model_spec, @@ -86,13 +74,14 @@ def atwork_subtour_destination_sample(tours, choices['person_id'] = choosers.person_id - inject.add_table('atwork_subtour_destination_sample', choices) + return choices -@inject.step() -def atwork_subtour_destination_logsums(persons_merged, - skim_dict, skim_stack, - chunk_size, trace_hh_id): +def atwork_subtour_destination_logsums( + persons_merged, + destination_sample, + skim_dict, skim_stack, + chunk_size, trace_hh_id): """ add logsum column to existing atwork_subtour_destination_sample able @@ -117,18 +106,9 @@ def atwork_subtour_destination_logsums(persons_merged, trace_label = 'atwork_subtour_destination_logsums' - # use inject.get_table as this won't exist if there are no atwork subtours - destination_sample = inject.get_table('atwork_subtour_destination_sample', default=None) - if destination_sample is None: - tracing.no_results(trace_label) - return - model_settings = config.read_model_settings('atwork_subtour_destination.yaml') logsum_settings = config.read_model_settings(model_settings['LOGSUM_SETTINGS']) - destination_sample = destination_sample.to_frame() - persons_merged = persons_merged.to_frame() - # FIXME - MEMORY HACK - only include columns actually used in spec persons_merged = logsum.filter_chooser_columns(persons_merged, logsum_settings, model_settings) @@ -153,19 +133,19 @@ def atwork_subtour_destination_logsums(persons_merged, chunk_size, trace_hh_id, trace_label) - # "add_column series should have an index matching the table to which it is being added" - # when the index has duplicates, however, in the special case that the series index exactly - # matches the table index, then the series value order is preserved. logsums does have a - # matching index, since atwork_subtour_destination_sample was on left side of merge - inject.add_column("atwork_subtour_destination_sample", "mode_choice_logsum", logsums) + destination_sample['mode_choice_logsum'] = logsums + + return destination_sample @inject.step() -def atwork_subtour_destination_simulate(tours, - persons_merged, - skim_dict, - land_use, size_terms, - chunk_size, trace_hh_id): +def atwork_subtour_destination_simulate( + subtours, + persons_merged, + destination_sample, + skim_dict, + destination_size_terms, + chunk_size, trace_hh_id): """ atwork_subtour_destination model on atwork_subtour_destination_sample annotated with mode_choice logsum to select a destination from sample alternatives @@ -173,26 +153,15 @@ def atwork_subtour_destination_simulate(tours, trace_label = 'atwork_subtour_destination_simulate' - # use inject.get_table as this won't exist if there are no atwork subtours - destination_sample = inject.get_table('atwork_subtour_destination_sample', default=None) - if destination_sample is None: - tracing.no_results(trace_label) - return - - destination_sample = destination_sample.to_frame() - model_settings = config.read_model_settings('atwork_subtour_destination.yaml') model_spec = simulate.read_model_spec(file_name='atwork_subtour_destination.csv') - tours = tours.to_frame() - subtours = tours[tours.tour_category == 'atwork'] - # interaction_sample_simulate insists choosers appear in same order as alts subtours = subtours.sort_index() # merge persons into tours choosers = pd.merge(subtours, - persons_merged.to_frame(), + persons_merged, left_on='person_id', right_index=True) # FIXME - MEMORY HACK - only include columns actually used in spec chooser_columns = model_settings['SIMULATE_CHOOSER_COLUMNS'] @@ -202,9 +171,7 @@ def atwork_subtour_destination_simulate(tours, chooser_col_name = 'workplace_taz' # alternatives are pre-sampled and annotated with logsums and pick_count - # but we have to merge additional alt columns into alt sample list - destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'atwork') - + # but we have to merge destination_size_terms columns into alt sample list alternatives = \ pd.merge(destination_sample, destination_size_terms, left_on=alt_dest_col_name, right_index=True, how="left") @@ -239,17 +206,60 @@ def atwork_subtour_destination_simulate(tours, trace_label=trace_label, trace_choice_name='workplace_location') + return choices + + +@inject.step() +def atwork_subtour_destination( + tours, + persons_merged, + skim_dict, + skim_stack, + land_use, size_terms, + chunk_size, trace_hh_id): + + persons_merged = persons_merged.to_frame() + + tours = tours.to_frame() + subtours = tours[tours.tour_category == 'atwork'] + + # - if no atwork subtours + if tours.shape[0] == 0: + tracing.no_results('atwork_subtour_destination') + return + + destination_size_terms = tour_destination_size_terms(land_use, size_terms, 'atwork') + + destination_sample = atwork_subtour_destination_sample( + subtours, + persons_merged, + skim_dict, + destination_size_terms, + chunk_size, trace_hh_id) + + destination_sample = atwork_subtour_destination_logsums( + persons_merged, + destination_sample, + skim_dict, skim_stack, + chunk_size, trace_hh_id) + + choices = atwork_subtour_destination_simulate( + subtours, + persons_merged, + destination_sample, + skim_dict, + destination_size_terms, + chunk_size, trace_hh_id) + subtours['destination'] = choices assign_in_place(tours, subtours[['destination']]) pipeline.replace_table("tours", tours) - pipeline.drop_table('atwork_subtour_destination_sample') - tracing.print_summary('subtour destination', subtours.destination, describe=True) if trace_hh_id: tracing.trace_df(tours, - label=trace_label, + label='atwork_subtour_destination', columns=['destination']) diff --git a/activitysim/abm/models/joint_tour_destination.py b/activitysim/abm/models/joint_tour_destination.py index b2d897825..ed6f95fbc 100644 --- a/activitysim/abm/models/joint_tour_destination.py +++ b/activitysim/abm/models/joint_tour_destination.py @@ -415,9 +415,6 @@ def joint_tour_destination( assign_in_place(tours, joint_tours[['destination']]) pipeline.replace_table("tours", tours) - # drop bulky joint_tour_destination_sample table as we don't use it further - pipeline.drop_table('joint_tour_destination_sample') - tracing.print_summary('destination', joint_tours.destination, describe=True) if trace_hh_id: diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index c3b590c03..9bfbe437e 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -50,9 +50,7 @@ models: - non_mandatory_tour_scheduling - tour_mode_choice_simulate - atwork_subtour_frequency - - atwork_subtour_destination_sample - - atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate + - atwork_subtour_destination - atwork_subtour_scheduling - atwork_subtour_mode_choice - stop_frequency diff --git a/docs/abmexample.rst b/docs/abmexample.rst index 734baecb8..b976de557 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -528,9 +528,7 @@ The ``models`` setting contains the specification of the data pipeline model ste - non_mandatory_tour_scheduling - tour_mode_choice_simulate - atwork_subtour_frequency - - atwork_subtour_destination_sample - - atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate + - atwork_subtour_destination - atwork_subtour_scheduling - atwork_subtour_mode_choice - stop_frequency diff --git a/example/configs/settings.yaml b/example/configs/settings.yaml index c3b590c03..9bfbe437e 100644 --- a/example/configs/settings.yaml +++ b/example/configs/settings.yaml @@ -50,9 +50,7 @@ models: - non_mandatory_tour_scheduling - tour_mode_choice_simulate - atwork_subtour_frequency - - atwork_subtour_destination_sample - - atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate + - atwork_subtour_destination - atwork_subtour_scheduling - atwork_subtour_mode_choice - stop_frequency diff --git a/example_mp/configs/settings.yaml b/example_mp/configs/settings.yaml index d16811edf..aa7668555 100644 --- a/example_mp/configs/settings.yaml +++ b/example_mp/configs/settings.yaml @@ -72,9 +72,7 @@ models: - non_mandatory_tour_scheduling - tour_mode_choice_simulate - atwork_subtour_frequency - - _atwork_subtour_destination_sample - - _atwork_subtour_destination_logsums - - atwork_subtour_destination_simulate + - atwork_subtour_destination - atwork_subtour_scheduling - atwork_subtour_mode_choice - stop_frequency From 908f6822e4692d19b0c5448648ff55c941e7e3fa Mon Sep 17 00:00:00 2001 From: bstabler Date: Thu, 24 Jan 2019 14:36:15 -0800 Subject: [PATCH 072/122] add scripts to create inputs from the latest TM1 CT-RAMP example setup. Add under development script to create ActivityViz/ABMVIZ inputs for eventual visualization of model results. --- scripts/README.MD | 19 +- scripts/build_omx.py | 21 +- scripts/create_abmviz_inputs.py | 79 ++ scripts/data_mover.ipynb | 135 --- scripts/mtc_inputs.py | 26 + scripts/mtc_tm1_omx_export.s | 414 ++++++++ scripts/skim_manifest.csv | 1652 +++++++++++++++---------------- 7 files changed, 1369 insertions(+), 977 deletions(-) create mode 100644 scripts/create_abmviz_inputs.py delete mode 100644 scripts/data_mover.ipynb create mode 100644 scripts/mtc_inputs.py create mode 100644 scripts/mtc_tm1_omx_export.s diff --git a/scripts/README.MD b/scripts/README.MD index 5d27b18bf..02a144841 100644 --- a/scripts/README.MD +++ b/scripts/README.MD @@ -1,9 +1,16 @@ ## Scripts -Potentially out-of-date scripts used for various data processing activities: - - stack_mode_choice.py - reformat the very complicated MTC TM1 mode choice UEC Excel files into a more straightforward csv file. Unfortunately, there were some manual steps - for tour mode choice you have to run it twice since there are two slightly different variable sets that seem to occur. Hopefully we never will have to do this again so I'm not documenting this throughly. Not sure I could if I wanted to. - - stack_mode_choice2.py - has a couple of custom edits for trip_mode_choice rather than tour_mode choice. - - data_mover.ipynb - create HDF5 example data store from raw CSV files - - build_omx.py - create one big OMX file for all the MTC TM1 skims. Requires skim_manifest.csv. + +### Create Asim TM1 inputs from CT-RAMP TM1 model data + + - Open a DOS prompt in the mtc tm1 skims folder + - Run mtc_tm1_omx_export.s to convert Cube matrices to OMX (you need Cube 6.4.3 or 6.4.2 + OMXLib-x64.dll) + - Run build_omx.py to build one OMX file. Requires skim_manifest.csv. + - Run mtc_inputs.py to build mtc_asim.h5 (taz data and syn pop files) + +### Other scripts + - stack_mode_choice.py - reformat the MTC TM1 mode choice UEC Excel files into a more straightforward csv file. Unfortunately, there were some manual steps - for tour mode choice you have to run it twice since there are two slightly different variable sets that seem to occur. (this script is out-of-date) + - stack_mode_choice2.py - has a couple of custom edits for trip_mode_choice rather than tour_mode choice. (this script is out-of-date) - create_sf_example.py - create SF county only MTC TM1 example inputs - land use, syn pop, and skims - for testing the entire system with full functionality but less memory requirements. - - SandagNetworkLOS.py - convert SANDAG network los files on MTC box account to ActivitySim NetworkLOS format \ No newline at end of file + - make_pipeline_output.py - create table of pipeline table fields by creator for the rst docs + - create_abmviz_inputs.py - create abmviz input files (this script is not yet complete) \ No newline at end of file diff --git a/scripts/build_omx.py b/scripts/build_omx.py index 70dc78243..221393bf1 100644 --- a/scripts/build_omx.py +++ b/scripts/build_omx.py @@ -1,6 +1,7 @@ # ActivitySim # Copyright (C) 2016 RSG Inc # See full license in LICENSE.txt. +# run from the mtc tm1 skims folder import os @@ -29,11 +30,11 @@ def read_manifest(manifest_file_name): def omx_getMatrix(omx_file_name, omx_key): - with omx.openFile(omx_file_name, 'r') as omx_file: + with omx.open_file(omx_file_name, 'r') as omx_file: - if omx_key not in omx_file.listMatrices(): + if omx_key not in omx_file.list_matrices(): print "Source matrix with key '%s' not found in file '%s" % (omx_key, omx_file,) - print omx_file.listMatrices() + print omx_file.list_matrices() raise RuntimeError("Source matrix with key '%s' not found in file '%s" % (omx_key, omx_file,)) @@ -43,13 +44,13 @@ def omx_getMatrix(omx_file_name, omx_key): manifest_dir = '.' -source_data_dir = './source_skims' -dest_data_dir = './data' +source_data_dir = '.' +dest_data_dir = '.' manifest_file_name = os.path.join(manifest_dir, 'skim_manifest.csv') dest_file_name = os.path.join(dest_data_dir, 'skims.omx') -with omx.openFile(dest_file_name, 'a') as dest_omx: +with omx.open_file(dest_file_name, 'a') as dest_omx: manifest = read_manifest(manifest_file_name) @@ -63,18 +64,18 @@ def omx_getMatrix(omx_file_name, omx_key): dest_key = row.skim_key1 print "Reading '%s' from '%s' in %s" % (dest_key, row.source_key, source_file_name) - with omx.openFile(source_file_name, 'r') as source_omx: + with omx.open_file(source_file_name, 'r') as source_omx: - if row.source_key not in source_omx.listMatrices(): + if row.source_key not in source_omx.list_matrices(): print "Source matrix with key '%s' not found in file '%s" \ % (row.source_key, source_file_name,) - print source_omx.listMatrices() + print source_omx.list_matrices() raise RuntimeError("Source matrix with key '%s' not found in file '%s" % (row.source_key, dest_omx,)) data = source_omx[row.source_key] - if dest_key in dest_omx.listMatrices(): + if dest_key in dest_omx.list_matrices(): print "deleting existing dest key '%s'" % (dest_key,) dest_omx.removeNode(dest_omx.root.data, dest_key) diff --git a/scripts/create_abmviz_inputs.py b/scripts/create_abmviz_inputs.py new file mode 100644 index 000000000..452c3b142 --- /dev/null +++ b/scripts/create_abmviz_inputs.py @@ -0,0 +1,79 @@ + +# create abmviz input files +# Ben Stabler, ben.stabler@rsginc.com, 07/31/18 + +import pandas as pd + +pipeline_filename = 'output\pipeline.h5' + +trips_filename = 'output\ABMVIZ_trips.csv' +animatedmap_filename = 'output\ABMVIZ_3DAnimatedMapData.csv' +barchartandmap_filename = 'output\ABMVIZ_BarChartAndMapData.csv' +barchart_filename = 'output\ABMVIZ_BarChartData.csv' +radarchart_filename = 'output\ABMVIZ_RadarChartsData.csv' +timeuse_filename = 'output\ABMVIZ_TimeUseData.csv' +treemap_filename = 'output\ABMVIZ_TreeMapData.csv' + +# get pipeline trips table and add other required fields +pipeline = pd.io.pytables.HDFStore(pipeline_filename) +trips = pipeline['/trips/trip_mode_choice'] +households = pipeline['/households/joint_tour_frequency'] +households = households.set_index("hhno", drop=False) +tours = pipeline['/tours/tour_mode_choice_simulate'] +trips['home_taz'] = households.loc[trips['household_id']]['TAZ'].tolist() +trips['tour_start'] = tours.loc[trips['tour_id']]['start'].tolist() +trips['tour_end'] = tours.loc[trips['tour_id']]['end'].tolist() + +trips['in_or_out'] = 0 +trips['in_or_out'][trips['outbound'] == True] = 10 + +trips['inbound'] = ~trips['outbound'] +trips = trips.sort_values(['tour_id','inbound','trip_num']) + +trips['origin_purpose'] = 'Home' +trips['destination_purpose'] = trips['purpose'] +trips['origin_purpose_start'] = 1 +trips['destination_purpose_start'] = trips['depart'] +trips['origin_purpose_end'] = 1 +trips['destination_purpose_end'] = 1 + +trips.to_csv(trips_filename) + +# create 3D animated map file + +## create remainder of the day at home table +remainder = trips.groupby(['person_id']).max()[['home_taz','depart']] +remainder = pd.crosstab(remainder["home_taz"],remainder["depart"]) + +## loop by period and add trips to DayPop table + + +/* Person location by PERIOD of the day based on trips */ + INSERT INTO DAYPOP_TEMP (TAZ, PER, PERSONS) SELECT ORIG_TAZ, @hrStr AS PER, COUNT(*) AS PERSONS + FROM TRIPS + WHERE ORIG_PURPOSE_START_PERIOD < (@hr+1) AND DEPART_PERIOD > (@hr-1) + GROUP BY ORIG_TAZ + + + +DECLARE @minPERIOD AS INT +DECLARE @maxPERIOD AS INT +DECLARE @hr AS INT +DECLARE @hrStr AS VARCHAR(6) + +SET @minPERIOD = 1 +SET @maxPERIOD = 48 + +CREATE TABLE DAYPOP (TAZ INT, PER VARCHAR(6), PERSONS INT, PERSONSNOTATHOME INT) +CREATE TABLE DAYPOP_TEMP (TAZ INT, PER VARCHAR(6), PERSONS INT, PERSONSNOTATHOME INT) + +# create bar chart and map file + +# create bar chart file + +# create radar chart file + +# create timeuse file + +# create treemap file + diff --git a/scripts/data_mover.ipynb b/scripts/data_mover.ipynb deleted file mode 100644 index 266122276..000000000 --- a/scripts/data_mover.ipynb +++ /dev/null @@ -1,135 +0,0 @@ -{ - "worksheets": [ - { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "import pandas as pd\n", - "import os\n", - "from activitysim import activitysim as asim\n", - "# this is where I unzipped the MTC data\n", - "SRCDIR = \"/Users/ffoti/data/activitysim\"\n", - "# and where it's going to\n", - "TGTFILE = \"../example/data/mtc_asim.h5\"" - ], - "language": "python", - "prompt_number": 1 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "store = pd.HDFStore(TGTFILE, \"w\")" - ], - "language": "python", - "prompt_number": 2 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "col_map = {\n", - " \"HHID\": \"household_id\",\n", - " \"AGE\": \"age\",\n", - " \"SEX\": \"sex\",\n", - " \"hworkers\": \"workers\",\n", - " \"HINC\": \"income\",\n", - " \"AREATYPE\": \"area_type\"\n", - "}" - ], - "language": "python", - "prompt_number": 3 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "df = pd.read_csv(os.path.join(SRCDIR, \"landuse\", \"tazData.csv\"), index_col=\"ZONE\")\n", - "df.columns = [col_map.get(s, s) for s in df.columns]\n", - "store[\"land_use/taz_data\"] = df" - ], - "language": "python", - "prompt_number": 4 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "df = pd.read_csv(os.path.join(SRCDIR, \"skims\", \"accessibility.csv\"), index_col=\"taz\")\n", - "df.columns = [col_map.get(s, s) for s in df.columns]\n", - "store[\"skims/accessibility\"] = df" - ], - "language": "python", - "prompt_number": 5 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "df = pd.read_csv(os.path.join(SRCDIR, \"popsyn\", \"hhFile.p2011s3a1.2010.csv\"), index_col=\"HHID\")\n", - "df.columns = [col_map.get(s, s) for s in df.columns]\n", - "store[\"households\"] = df" - ], - "language": "python", - "prompt_number": 6 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "df = pd.read_csv(os.path.join(SRCDIR, \"popsyn\", \"personFile.p2011s3a1.2010.csv\"), index_col=\"PERID\")\n", - "df.columns = [col_map.get(s, s) for s in df.columns]\n", - "store[\"persons\"] = df" - ], - "language": "python", - "prompt_number": 7 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "store.close()" - ], - "language": "python", - "prompt_number": 8 - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "input": [ - "" - ], - "language": "python", - "prompt_number": 8 - } - ] - } - ], - "cells": [], - "metadata": { - "name": "", - "signature": "sha256:07f23263339f1751ee4eb702d126692b9ed2785fe8640a8906eb0fb110d9e67a" - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/scripts/mtc_inputs.py b/scripts/mtc_inputs.py new file mode 100644 index 000000000..67325a6d2 --- /dev/null +++ b/scripts/mtc_inputs.py @@ -0,0 +1,26 @@ + +# create mtc tm1 asim example data +# Ben Stabler, ben.stabler@rsginc.com, 01/24/19 +# run from the mtc tm1 skims folder + +import pandas as pd + +store = pd.HDFStore("mtc_asim.h5", "w") + +col_map = {"HHID":"household_id","AGE":"age", "SEX":"sex", "hworkers":"workers", "HINC":"income", "AREATYPE":"area_type"} + +df = pd.read_csv("../landuse/tazData.csv", index_col="ZONE") +df.columns = [col_map.get(s, s) for s in df.columns] +store["land_use/taz_data"] = df + +df = pd.read_csv("accessibility.csv", index_col="taz") +df.columns = [col_map.get(s, s) for s in df.columns] +store["skims/accessibility"] = df + +df = pd.read_csv("../popsyn/hhFile.csv", index_col="HHID") +df.columns = [col_map.get(s, s) for s in df.columns] +store["households"] = df + +df = pd.read_csv("../popsyn/personFile.csv", index_col="PERID") +df.columns = [col_map.get(s, s) for s in df.columns] +store["persons"] = df diff --git a/scripts/mtc_tm1_omx_export.s b/scripts/mtc_tm1_omx_export.s new file mode 100644 index 000000000..cc41e59ce --- /dev/null +++ b/scripts/mtc_tm1_omx_export.s @@ -0,0 +1,414 @@ + +; Export and import OMX matrices with Cube 6.4.3 +; Ben Stabler, ben.stabler@rsginc.com, 10/05/16 +; "C:\Program Files\Citilabs\CubeVoyager\VOYAGER.EXE" mtc_tm1_omx_export.s +; If using Cube 6.4.2, first copy OMXLib-x64.dll to C:\Program Files\Citilabs\CubeVoyager and rename as OMXLIB.DLL + +CONVERTMAT FROM='COM_HWYSKIMAM.tpp' TO='COM_HWYSKIMAM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='COM_HWYSKIMEA.tpp' TO='COM_HWYSKIMEA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='COM_HWYSKIMEV.tpp' TO='COM_HWYSKIMEV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='COM_HWYSKIMMD.tpp' TO='COM_HWYSKIMMD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='COM_HWYSKIMPM.tpp' TO='COM_HWYSKIMPM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='DA_AM.tpp' TO='DA_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='DA_EA.tpp' TO='DA_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='DA_EV.tpp' TO='DA_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='DA_MD.tpp' TO='DA_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='DA_PM.tpp' TO='DA_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='HWYSKMAM.tpp' TO='HWYSKMAM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='HWYSKMEA.tpp' TO='HWYSKMEA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='HWYSKMEV.tpp' TO='HWYSKMEV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='HWYSKMMD.tpp' TO='HWYSKMMD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='HWYSKMPM.tpp' TO='HWYSKMPM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='LRG_AM.tpp' TO='LRG_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='LRG_EA.tpp' TO='LRG_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='LRG_EV.tpp' TO='LRG_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='LRG_MD.tpp' TO='LRG_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='LRG_PM.tpp' TO='LRG_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='MED_AM.tpp' TO='MED_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='MED_EA.tpp' TO='MED_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='MED_EV.tpp' TO='MED_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='MED_MD.tpp' TO='MED_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='MED_PM.tpp' TO='MED_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='nonmotskm.tpp' TO='nonmotskm.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S2_AM.tpp' TO='S2_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S2_EA.tpp' TO='S2_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S2_EV.tpp' TO='S2_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S2_MD.tpp' TO='S2_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S2_PM.tpp' TO='S2_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S3_AM.tpp' TO='S3_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S3_EA.tpp' TO='S3_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S3_EV.tpp' TO='S3_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S3_MD.tpp' TO='S3_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='S3_PM.tpp' TO='S3_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='SML_AM.tpp' TO='SML_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='SML_EA.tpp' TO='SML_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='SML_EV.tpp' TO='SML_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='SML_MD.tpp' TO='SML_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='SML_PM.tpp' TO='SML_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_com_wlk.tpp' TO='trnskmam_drv_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_com_wlk_board_delay.tpp' TO='trnskmam_drv_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_com_wlk_ivtt_delay.tpp' TO='trnskmam_drv_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_com_wlk_temp.tpp' TO='trnskmam_drv_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_exp_wlk.tpp' TO='trnskmam_drv_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_exp_wlk_board_delay.tpp' TO='trnskmam_drv_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_exp_wlk_ivtt_delay.tpp' TO='trnskmam_drv_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_exp_wlk_temp.tpp' TO='trnskmam_drv_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_hvy_wlk.tpp' TO='trnskmam_drv_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_hvy_wlk_board_delay.tpp' TO='trnskmam_drv_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_hvy_wlk_ivtt_delay.tpp' TO='trnskmam_drv_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_hvy_wlk_temp.tpp' TO='trnskmam_drv_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_loc_wlk.tpp' TO='trnskmam_drv_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_loc_wlk_board_delay.tpp' TO='trnskmam_drv_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_loc_wlk_ivtt_delay.tpp' TO='trnskmam_drv_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_loc_wlk_temp.tpp' TO='trnskmam_drv_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_lrf_wlk.tpp' TO='trnskmam_drv_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_lrf_wlk_board_delay.tpp' TO='trnskmam_drv_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_lrf_wlk_ivtt_delay.tpp' TO='trnskmam_drv_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_lrf_wlk_temp.tpp' TO='trnskmam_drv_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_trn_wlk.tpp' TO='trnskmam_drv_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_trn_wlk_board_delay.tpp' TO='trnskmam_drv_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_trn_wlk_ivtt_delay.tpp' TO='trnskmam_drv_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_drv_trn_wlk_temp.tpp' TO='trnskmam_drv_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_drv.tpp' TO='trnskmam_wlk_com_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_drv_board_delay.tpp' TO='trnskmam_wlk_com_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_drv_ivtt_delay.tpp' TO='trnskmam_wlk_com_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_drv_temp.tpp' TO='trnskmam_wlk_com_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_wlk.tpp' TO='trnskmam_wlk_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_wlk_board_delay.tpp' TO='trnskmam_wlk_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_com_wlk_temp.tpp' TO='trnskmam_wlk_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_drv.tpp' TO='trnskmam_wlk_exp_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_drv_board_delay.tpp' TO='trnskmam_wlk_exp_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_drv_ivtt_delay.tpp' TO='trnskmam_wlk_exp_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_drv_temp.tpp' TO='trnskmam_wlk_exp_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_wlk.tpp' TO='trnskmam_wlk_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_wlk_board_delay.tpp' TO='trnskmam_wlk_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_exp_wlk_temp.tpp' TO='trnskmam_wlk_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_drv.tpp' TO='trnskmam_wlk_hvy_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_drv_board_delay.tpp' TO='trnskmam_wlk_hvy_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_drv_ivtt_delay.tpp' TO='trnskmam_wlk_hvy_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_drv_temp.tpp' TO='trnskmam_wlk_hvy_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_wlk.tpp' TO='trnskmam_wlk_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_wlk_board_delay.tpp' TO='trnskmam_wlk_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_hvy_wlk_temp.tpp' TO='trnskmam_wlk_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_drv.tpp' TO='trnskmam_wlk_loc_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_drv_board_delay.tpp' TO='trnskmam_wlk_loc_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_drv_ivtt_delay.tpp' TO='trnskmam_wlk_loc_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_drv_temp.tpp' TO='trnskmam_wlk_loc_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_wlk.tpp' TO='trnskmam_wlk_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_wlk_board_delay.tpp' TO='trnskmam_wlk_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_loc_wlk_temp.tpp' TO='trnskmam_wlk_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_drv.tpp' TO='trnskmam_wlk_lrf_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_drv_board_delay.tpp' TO='trnskmam_wlk_lrf_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_drv_ivtt_delay.tpp' TO='trnskmam_wlk_lrf_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_drv_temp.tpp' TO='trnskmam_wlk_lrf_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_wlk.tpp' TO='trnskmam_wlk_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_wlk_board_delay.tpp' TO='trnskmam_wlk_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_lrf_wlk_temp.tpp' TO='trnskmam_wlk_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_drv.tpp' TO='trnskmam_wlk_trn_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_drv_board_delay.tpp' TO='trnskmam_wlk_trn_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_drv_ivtt_delay.tpp' TO='trnskmam_wlk_trn_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_drv_temp.tpp' TO='trnskmam_wlk_trn_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_wlk.tpp' TO='trnskmam_wlk_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_wlk_board_delay.tpp' TO='trnskmam_wlk_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_wlk_ivtt_delay.tpp' TO='trnskmam_wlk_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmam_wlk_trn_wlk_temp.tpp' TO='trnskmam_wlk_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_com_wlk.tpp' TO='trnskmea_drv_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_com_wlk_board_delay.tpp' TO='trnskmea_drv_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_com_wlk_ivtt_delay.tpp' TO='trnskmea_drv_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_com_wlk_temp.tpp' TO='trnskmea_drv_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_exp_wlk.tpp' TO='trnskmea_drv_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_exp_wlk_board_delay.tpp' TO='trnskmea_drv_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_exp_wlk_ivtt_delay.tpp' TO='trnskmea_drv_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_exp_wlk_temp.tpp' TO='trnskmea_drv_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_hvy_wlk.tpp' TO='trnskmea_drv_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_hvy_wlk_board_delay.tpp' TO='trnskmea_drv_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_hvy_wlk_ivtt_delay.tpp' TO='trnskmea_drv_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_hvy_wlk_temp.tpp' TO='trnskmea_drv_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_loc_wlk.tpp' TO='trnskmea_drv_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_loc_wlk_board_delay.tpp' TO='trnskmea_drv_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_loc_wlk_ivtt_delay.tpp' TO='trnskmea_drv_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_loc_wlk_temp.tpp' TO='trnskmea_drv_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_lrf_wlk.tpp' TO='trnskmea_drv_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_lrf_wlk_board_delay.tpp' TO='trnskmea_drv_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_lrf_wlk_ivtt_delay.tpp' TO='trnskmea_drv_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_lrf_wlk_temp.tpp' TO='trnskmea_drv_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_trn_wlk.tpp' TO='trnskmea_drv_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_trn_wlk_board_delay.tpp' TO='trnskmea_drv_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_trn_wlk_ivtt_delay.tpp' TO='trnskmea_drv_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_drv_trn_wlk_temp.tpp' TO='trnskmea_drv_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_drv.tpp' TO='trnskmea_wlk_com_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_drv_board_delay.tpp' TO='trnskmea_wlk_com_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_drv_ivtt_delay.tpp' TO='trnskmea_wlk_com_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_drv_temp.tpp' TO='trnskmea_wlk_com_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_wlk.tpp' TO='trnskmea_wlk_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_wlk_board_delay.tpp' TO='trnskmea_wlk_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_com_wlk_temp.tpp' TO='trnskmea_wlk_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_drv.tpp' TO='trnskmea_wlk_exp_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_drv_board_delay.tpp' TO='trnskmea_wlk_exp_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_drv_ivtt_delay.tpp' TO='trnskmea_wlk_exp_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_drv_temp.tpp' TO='trnskmea_wlk_exp_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_wlk.tpp' TO='trnskmea_wlk_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_wlk_board_delay.tpp' TO='trnskmea_wlk_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_exp_wlk_temp.tpp' TO='trnskmea_wlk_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_drv.tpp' TO='trnskmea_wlk_hvy_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_drv_board_delay.tpp' TO='trnskmea_wlk_hvy_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_drv_ivtt_delay.tpp' TO='trnskmea_wlk_hvy_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_drv_temp.tpp' TO='trnskmea_wlk_hvy_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_wlk.tpp' TO='trnskmea_wlk_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_wlk_board_delay.tpp' TO='trnskmea_wlk_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_hvy_wlk_temp.tpp' TO='trnskmea_wlk_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_drv.tpp' TO='trnskmea_wlk_loc_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_drv_board_delay.tpp' TO='trnskmea_wlk_loc_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_drv_ivtt_delay.tpp' TO='trnskmea_wlk_loc_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_drv_temp.tpp' TO='trnskmea_wlk_loc_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_wlk.tpp' TO='trnskmea_wlk_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_wlk_board_delay.tpp' TO='trnskmea_wlk_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_loc_wlk_temp.tpp' TO='trnskmea_wlk_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_drv.tpp' TO='trnskmea_wlk_lrf_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_drv_board_delay.tpp' TO='trnskmea_wlk_lrf_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_drv_ivtt_delay.tpp' TO='trnskmea_wlk_lrf_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_drv_temp.tpp' TO='trnskmea_wlk_lrf_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_wlk.tpp' TO='trnskmea_wlk_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_wlk_board_delay.tpp' TO='trnskmea_wlk_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_lrf_wlk_temp.tpp' TO='trnskmea_wlk_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_drv.tpp' TO='trnskmea_wlk_trn_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_drv_board_delay.tpp' TO='trnskmea_wlk_trn_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_drv_ivtt_delay.tpp' TO='trnskmea_wlk_trn_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_drv_temp.tpp' TO='trnskmea_wlk_trn_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_wlk.tpp' TO='trnskmea_wlk_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_wlk_board_delay.tpp' TO='trnskmea_wlk_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_wlk_ivtt_delay.tpp' TO='trnskmea_wlk_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmea_wlk_trn_wlk_temp.tpp' TO='trnskmea_wlk_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_com_wlk.tpp' TO='trnskmev_drv_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_com_wlk_board_delay.tpp' TO='trnskmev_drv_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_com_wlk_ivtt_delay.tpp' TO='trnskmev_drv_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_com_wlk_temp.tpp' TO='trnskmev_drv_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_exp_wlk.tpp' TO='trnskmev_drv_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_exp_wlk_board_delay.tpp' TO='trnskmev_drv_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_exp_wlk_ivtt_delay.tpp' TO='trnskmev_drv_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_exp_wlk_temp.tpp' TO='trnskmev_drv_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_hvy_wlk.tpp' TO='trnskmev_drv_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_hvy_wlk_board_delay.tpp' TO='trnskmev_drv_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_hvy_wlk_ivtt_delay.tpp' TO='trnskmev_drv_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_hvy_wlk_temp.tpp' TO='trnskmev_drv_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_loc_wlk.tpp' TO='trnskmev_drv_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_loc_wlk_board_delay.tpp' TO='trnskmev_drv_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_loc_wlk_ivtt_delay.tpp' TO='trnskmev_drv_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_loc_wlk_temp.tpp' TO='trnskmev_drv_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_lrf_wlk.tpp' TO='trnskmev_drv_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_lrf_wlk_board_delay.tpp' TO='trnskmev_drv_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_lrf_wlk_ivtt_delay.tpp' TO='trnskmev_drv_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_lrf_wlk_temp.tpp' TO='trnskmev_drv_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_trn_wlk.tpp' TO='trnskmev_drv_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_trn_wlk_board_delay.tpp' TO='trnskmev_drv_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_trn_wlk_ivtt_delay.tpp' TO='trnskmev_drv_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_drv_trn_wlk_temp.tpp' TO='trnskmev_drv_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_drv.tpp' TO='trnskmev_wlk_com_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_drv_board_delay.tpp' TO='trnskmev_wlk_com_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_drv_ivtt_delay.tpp' TO='trnskmev_wlk_com_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_drv_temp.tpp' TO='trnskmev_wlk_com_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_wlk.tpp' TO='trnskmev_wlk_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_wlk_board_delay.tpp' TO='trnskmev_wlk_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_com_wlk_temp.tpp' TO='trnskmev_wlk_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_drv.tpp' TO='trnskmev_wlk_exp_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_drv_board_delay.tpp' TO='trnskmev_wlk_exp_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_drv_ivtt_delay.tpp' TO='trnskmev_wlk_exp_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_drv_temp.tpp' TO='trnskmev_wlk_exp_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_wlk.tpp' TO='trnskmev_wlk_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_wlk_board_delay.tpp' TO='trnskmev_wlk_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_exp_wlk_temp.tpp' TO='trnskmev_wlk_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_drv.tpp' TO='trnskmev_wlk_hvy_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_drv_board_delay.tpp' TO='trnskmev_wlk_hvy_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_drv_ivtt_delay.tpp' TO='trnskmev_wlk_hvy_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_drv_temp.tpp' TO='trnskmev_wlk_hvy_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_wlk.tpp' TO='trnskmev_wlk_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_wlk_board_delay.tpp' TO='trnskmev_wlk_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_hvy_wlk_temp.tpp' TO='trnskmev_wlk_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_drv.tpp' TO='trnskmev_wlk_loc_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_drv_board_delay.tpp' TO='trnskmev_wlk_loc_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_drv_ivtt_delay.tpp' TO='trnskmev_wlk_loc_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_drv_temp.tpp' TO='trnskmev_wlk_loc_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_wlk.tpp' TO='trnskmev_wlk_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_wlk_board_delay.tpp' TO='trnskmev_wlk_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_loc_wlk_temp.tpp' TO='trnskmev_wlk_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_drv.tpp' TO='trnskmev_wlk_lrf_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_drv_board_delay.tpp' TO='trnskmev_wlk_lrf_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_drv_ivtt_delay.tpp' TO='trnskmev_wlk_lrf_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_drv_temp.tpp' TO='trnskmev_wlk_lrf_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_wlk.tpp' TO='trnskmev_wlk_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_wlk_board_delay.tpp' TO='trnskmev_wlk_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_lrf_wlk_temp.tpp' TO='trnskmev_wlk_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_drv.tpp' TO='trnskmev_wlk_trn_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_drv_board_delay.tpp' TO='trnskmev_wlk_trn_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_drv_ivtt_delay.tpp' TO='trnskmev_wlk_trn_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_drv_temp.tpp' TO='trnskmev_wlk_trn_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_wlk.tpp' TO='trnskmev_wlk_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_wlk_board_delay.tpp' TO='trnskmev_wlk_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_wlk_ivtt_delay.tpp' TO='trnskmev_wlk_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmev_wlk_trn_wlk_temp.tpp' TO='trnskmev_wlk_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_com_wlk.tpp' TO='trnskmmd_drv_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_com_wlk_board_delay.tpp' TO='trnskmmd_drv_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_com_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_com_wlk_temp.tpp' TO='trnskmmd_drv_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_exp_wlk.tpp' TO='trnskmmd_drv_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_exp_wlk_board_delay.tpp' TO='trnskmmd_drv_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_exp_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_exp_wlk_temp.tpp' TO='trnskmmd_drv_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_hvy_wlk.tpp' TO='trnskmmd_drv_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_hvy_wlk_board_delay.tpp' TO='trnskmmd_drv_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_hvy_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_hvy_wlk_temp.tpp' TO='trnskmmd_drv_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_loc_wlk.tpp' TO='trnskmmd_drv_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_loc_wlk_board_delay.tpp' TO='trnskmmd_drv_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_loc_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_loc_wlk_temp.tpp' TO='trnskmmd_drv_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_lrf_wlk.tpp' TO='trnskmmd_drv_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_lrf_wlk_board_delay.tpp' TO='trnskmmd_drv_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_lrf_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_lrf_wlk_temp.tpp' TO='trnskmmd_drv_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_trn_wlk.tpp' TO='trnskmmd_drv_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_trn_wlk_board_delay.tpp' TO='trnskmmd_drv_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_trn_wlk_ivtt_delay.tpp' TO='trnskmmd_drv_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_drv_trn_wlk_temp.tpp' TO='trnskmmd_drv_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_drv.tpp' TO='trnskmmd_wlk_com_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_drv_board_delay.tpp' TO='trnskmmd_wlk_com_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_com_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_drv_temp.tpp' TO='trnskmmd_wlk_com_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_wlk.tpp' TO='trnskmmd_wlk_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_wlk_board_delay.tpp' TO='trnskmmd_wlk_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_com_wlk_temp.tpp' TO='trnskmmd_wlk_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_drv.tpp' TO='trnskmmd_wlk_exp_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_drv_board_delay.tpp' TO='trnskmmd_wlk_exp_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_exp_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_drv_temp.tpp' TO='trnskmmd_wlk_exp_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_wlk.tpp' TO='trnskmmd_wlk_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_wlk_board_delay.tpp' TO='trnskmmd_wlk_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_exp_wlk_temp.tpp' TO='trnskmmd_wlk_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_drv.tpp' TO='trnskmmd_wlk_hvy_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_drv_board_delay.tpp' TO='trnskmmd_wlk_hvy_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_hvy_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_drv_temp.tpp' TO='trnskmmd_wlk_hvy_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_wlk.tpp' TO='trnskmmd_wlk_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_wlk_board_delay.tpp' TO='trnskmmd_wlk_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_hvy_wlk_temp.tpp' TO='trnskmmd_wlk_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_drv.tpp' TO='trnskmmd_wlk_loc_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_drv_board_delay.tpp' TO='trnskmmd_wlk_loc_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_loc_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_drv_temp.tpp' TO='trnskmmd_wlk_loc_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_wlk.tpp' TO='trnskmmd_wlk_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_wlk_board_delay.tpp' TO='trnskmmd_wlk_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_loc_wlk_temp.tpp' TO='trnskmmd_wlk_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_drv.tpp' TO='trnskmmd_wlk_lrf_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_drv_board_delay.tpp' TO='trnskmmd_wlk_lrf_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_lrf_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_drv_temp.tpp' TO='trnskmmd_wlk_lrf_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_wlk.tpp' TO='trnskmmd_wlk_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_wlk_board_delay.tpp' TO='trnskmmd_wlk_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_lrf_wlk_temp.tpp' TO='trnskmmd_wlk_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_drv.tpp' TO='trnskmmd_wlk_trn_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_drv_board_delay.tpp' TO='trnskmmd_wlk_trn_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_drv_ivtt_delay.tpp' TO='trnskmmd_wlk_trn_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_drv_temp.tpp' TO='trnskmmd_wlk_trn_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_wlk.tpp' TO='trnskmmd_wlk_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_wlk_board_delay.tpp' TO='trnskmmd_wlk_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_wlk_ivtt_delay.tpp' TO='trnskmmd_wlk_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmmd_wlk_trn_wlk_temp.tpp' TO='trnskmmd_wlk_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_com_wlk.tpp' TO='trnskmpm_drv_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_com_wlk_board_delay.tpp' TO='trnskmpm_drv_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_com_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_com_wlk_temp.tpp' TO='trnskmpm_drv_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_exp_wlk.tpp' TO='trnskmpm_drv_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_exp_wlk_board_delay.tpp' TO='trnskmpm_drv_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_exp_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_exp_wlk_temp.tpp' TO='trnskmpm_drv_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_hvy_wlk.tpp' TO='trnskmpm_drv_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_hvy_wlk_board_delay.tpp' TO='trnskmpm_drv_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_hvy_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_hvy_wlk_temp.tpp' TO='trnskmpm_drv_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_loc_wlk.tpp' TO='trnskmpm_drv_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_loc_wlk_board_delay.tpp' TO='trnskmpm_drv_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_loc_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_loc_wlk_temp.tpp' TO='trnskmpm_drv_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_lrf_wlk.tpp' TO='trnskmpm_drv_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_lrf_wlk_board_delay.tpp' TO='trnskmpm_drv_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_lrf_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_lrf_wlk_temp.tpp' TO='trnskmpm_drv_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_trn_wlk.tpp' TO='trnskmpm_drv_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_trn_wlk_board_delay.tpp' TO='trnskmpm_drv_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_trn_wlk_ivtt_delay.tpp' TO='trnskmpm_drv_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_drv_trn_wlk_temp.tpp' TO='trnskmpm_drv_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_drv.tpp' TO='trnskmpm_wlk_com_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_drv_board_delay.tpp' TO='trnskmpm_wlk_com_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_com_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_drv_temp.tpp' TO='trnskmpm_wlk_com_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_wlk.tpp' TO='trnskmpm_wlk_com_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_wlk_board_delay.tpp' TO='trnskmpm_wlk_com_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_com_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_com_wlk_temp.tpp' TO='trnskmpm_wlk_com_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_drv.tpp' TO='trnskmpm_wlk_exp_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_drv_board_delay.tpp' TO='trnskmpm_wlk_exp_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_exp_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_drv_temp.tpp' TO='trnskmpm_wlk_exp_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_wlk.tpp' TO='trnskmpm_wlk_exp_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_wlk_board_delay.tpp' TO='trnskmpm_wlk_exp_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_exp_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_exp_wlk_temp.tpp' TO='trnskmpm_wlk_exp_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_drv.tpp' TO='trnskmpm_wlk_hvy_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_drv_board_delay.tpp' TO='trnskmpm_wlk_hvy_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_hvy_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_drv_temp.tpp' TO='trnskmpm_wlk_hvy_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_wlk.tpp' TO='trnskmpm_wlk_hvy_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_wlk_board_delay.tpp' TO='trnskmpm_wlk_hvy_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_hvy_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_hvy_wlk_temp.tpp' TO='trnskmpm_wlk_hvy_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_drv.tpp' TO='trnskmpm_wlk_loc_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_drv_board_delay.tpp' TO='trnskmpm_wlk_loc_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_loc_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_drv_temp.tpp' TO='trnskmpm_wlk_loc_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_wlk.tpp' TO='trnskmpm_wlk_loc_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_wlk_board_delay.tpp' TO='trnskmpm_wlk_loc_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_loc_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_loc_wlk_temp.tpp' TO='trnskmpm_wlk_loc_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_drv.tpp' TO='trnskmpm_wlk_lrf_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_drv_board_delay.tpp' TO='trnskmpm_wlk_lrf_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_lrf_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_drv_temp.tpp' TO='trnskmpm_wlk_lrf_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_wlk.tpp' TO='trnskmpm_wlk_lrf_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_wlk_board_delay.tpp' TO='trnskmpm_wlk_lrf_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_lrf_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_lrf_wlk_temp.tpp' TO='trnskmpm_wlk_lrf_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_drv.tpp' TO='trnskmpm_wlk_trn_drv.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_drv_board_delay.tpp' TO='trnskmpm_wlk_trn_drv_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_drv_ivtt_delay.tpp' TO='trnskmpm_wlk_trn_drv_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_drv_temp.tpp' TO='trnskmpm_wlk_trn_drv_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_wlk.tpp' TO='trnskmpm_wlk_trn_wlk.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_wlk_board_delay.tpp' TO='trnskmpm_wlk_trn_wlk_board_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_wlk_ivtt_delay.tpp' TO='trnskmpm_wlk_trn_wlk_ivtt_delay.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='trnskmpm_wlk_trn_wlk_temp.tpp' TO='trnskmpm_wlk_trn_wlk_temp.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='VSM_AM.tpp' TO='VSM_AM.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='VSM_EA.tpp' TO='VSM_EA.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='VSM_EV.tpp' TO='VSM_EV.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='VSM_MD.tpp' TO='VSM_MD.tpp.omx' FORMAT=OMX COMPRESSION=7 +CONVERTMAT FROM='VSM_PM.tpp' TO='VSM_PM.tpp.omx' FORMAT=OMX COMPRESSION=7 + + diff --git a/scripts/skim_manifest.csv b/scripts/skim_manifest.csv index 67e04bedd..afbecf72f 100644 --- a/scripts/skim_manifest.csv +++ b/scripts/skim_manifest.csv @@ -1,827 +1,827 @@ Token,File,Matrix,TimePeriod -SOV_TIME,hwyskmAM.omx,TIMEDA,AM -SOV_DIST,hwyskmAM.omx,DISTDA,AM -SOV_BTOLL,hwyskmAM.omx,BTOLLDA,AM -HOV2_TIME,hwyskmAM.omx,TIMES2,AM -HOV2_DIST,hwyskmAM.omx,DISTS2,AM -HOV2_BTOLL,hwyskmAM.omx,BTOLLS2,AM -HOV3_TIME,hwyskmAM.omx,TIMES3,AM -HOV3_DIST,hwyskmAM.omx,DISTS3,AM -HOV3_BTOLL,hwyskmAM.omx,BTOLLS3,AM -SOVTOLL_TIME,hwyskmAM.omx,TOLLTIMEDA,AM -SOVTOLL_DIST,hwyskmAM.omx,TOLLDISTDA,AM -SOVTOLL_BTOLL,hwyskmAM.omx,TOLLBTOLLDA,AM -SOVTOLL_VTOLL,hwyskmAM.omx,TOLLVTOLLDA,AM -HOV2TOLL_TIME,hwyskmAM.omx,TOLLTIMES2,AM -HOV2TOLL_DIST,hwyskmAM.omx,TOLLDISTS2,AM -HOV2TOLL_BTOLL,hwyskmAM.omx,TOLLBTOLLS2,AM -HOV2TOLL_VTOLL,hwyskmAM.omx,TOLLVTOLLS2,AM -HOV3TOLL_TIME,hwyskmAM.omx,TOLLTIMES3,AM -HOV3TOLL_DIST,hwyskmAM.omx,TOLLDISTS3,AM -HOV3TOLL_BTOLL,hwyskmAM.omx,TOLLBTOLLS3,AM -HOV3TOLL_VTOLL,hwyskmAM.omx,TOLLVTOLLS3,AM -SOV_TIME,hwyskmEA.omx,TIMEDA,EA -SOV_DIST,hwyskmEA.omx,DISTDA,EA -SOV_BTOLL,hwyskmEA.omx,BTOLLDA,EA -HOV2_TIME,hwyskmEA.omx,TIMES2,EA -HOV2_DIST,hwyskmEA.omx,DISTS2,EA -HOV2_BTOLL,hwyskmEA.omx,BTOLLS2,EA -HOV3_TIME,hwyskmEA.omx,TIMES3,EA -HOV3_DIST,hwyskmEA.omx,DISTS3,EA -HOV3_BTOLL,hwyskmEA.omx,BTOLLS3,EA -SOVTOLL_TIME,hwyskmEA.omx,TOLLTIMEDA,EA -SOVTOLL_DIST,hwyskmEA.omx,TOLLDISTDA,EA -SOVTOLL_BTOLL,hwyskmEA.omx,TOLLBTOLLDA,EA -SOVTOLL_VTOLL,hwyskmEA.omx,TOLLVTOLLDA,EA -HOV2TOLL_TIME,hwyskmEA.omx,TOLLTIMES2,EA -HOV2TOLL_DIST,hwyskmEA.omx,TOLLDISTS2,EA -HOV2TOLL_BTOLL,hwyskmEA.omx,TOLLBTOLLS2,EA -HOV2TOLL_VTOLL,hwyskmEA.omx,TOLLVTOLLS2,EA -HOV3TOLL_TIME,hwyskmEA.omx,TOLLTIMES3,EA -HOV3TOLL_DIST,hwyskmEA.omx,TOLLDISTS3,EA -HOV3TOLL_BTOLL,hwyskmEA.omx,TOLLBTOLLS3,EA -HOV3TOLL_VTOLL,hwyskmEA.omx,TOLLVTOLLS3,EA -SOV_TIME,hwyskmEV.omx,TIMEDA,EV -SOV_DIST,hwyskmEV.omx,DISTDA,EV -SOV_BTOLL,hwyskmEV.omx,BTOLLDA,EV -HOV2_TIME,hwyskmEV.omx,TIMES2,EV -HOV2_DIST,hwyskmEV.omx,DISTS2,EV -HOV2_BTOLL,hwyskmEV.omx,BTOLLS2,EV -HOV3_TIME,hwyskmEV.omx,TIMES3,EV -HOV3_DIST,hwyskmEV.omx,DISTS3,EV -HOV3_BTOLL,hwyskmEV.omx,BTOLLS3,EV -SOVTOLL_TIME,hwyskmEV.omx,TOLLTIMEDA,EV -SOVTOLL_DIST,hwyskmEV.omx,TOLLDISTDA,EV -SOVTOLL_BTOLL,hwyskmEV.omx,TOLLBTOLLDA,EV -SOVTOLL_VTOLL,hwyskmEV.omx,TOLLVTOLLDA,EV -HOV2TOLL_TIME,hwyskmEV.omx,TOLLTIMES2,EV -HOV2TOLL_DIST,hwyskmEV.omx,TOLLDISTS2,EV -HOV2TOLL_BTOLL,hwyskmEV.omx,TOLLBTOLLS2,EV -HOV2TOLL_VTOLL,hwyskmEV.omx,TOLLVTOLLS2,EV -HOV3TOLL_TIME,hwyskmEV.omx,TOLLTIMES3,EV -HOV3TOLL_DIST,hwyskmEV.omx,TOLLDISTS3,EV -HOV3TOLL_BTOLL,hwyskmEV.omx,TOLLBTOLLS3,EV -HOV3TOLL_VTOLL,hwyskmEV.omx,TOLLVTOLLS3,EV -SOV_TIME,hwyskmMD.omx,TIMEDA,MD -SOV_DIST,hwyskmMD.omx,DISTDA,MD -SOV_BTOLL,hwyskmMD.omx,BTOLLDA,MD -HOV2_TIME,hwyskmMD.omx,TIMES2,MD -HOV2_DIST,hwyskmMD.omx,DISTS2,MD -HOV2_BTOLL,hwyskmMD.omx,BTOLLS2,MD -HOV3_TIME,hwyskmMD.omx,TIMES3,MD -HOV3_DIST,hwyskmMD.omx,DISTS3,MD -HOV3_BTOLL,hwyskmMD.omx,BTOLLS3,MD -SOVTOLL_TIME,hwyskmMD.omx,TOLLTIMEDA,MD -SOVTOLL_DIST,hwyskmMD.omx,TOLLDISTDA,MD -SOVTOLL_BTOLL,hwyskmMD.omx,TOLLBTOLLDA,MD -SOVTOLL_VTOLL,hwyskmMD.omx,TOLLVTOLLDA,MD -HOV2TOLL_TIME,hwyskmMD.omx,TOLLTIMES2,MD -HOV2TOLL_DIST,hwyskmMD.omx,TOLLDISTS2,MD -HOV2TOLL_BTOLL,hwyskmMD.omx,TOLLBTOLLS2,MD -HOV2TOLL_VTOLL,hwyskmMD.omx,TOLLVTOLLS2,MD -HOV3TOLL_TIME,hwyskmMD.omx,TOLLTIMES3,MD -HOV3TOLL_DIST,hwyskmMD.omx,TOLLDISTS3,MD -HOV3TOLL_BTOLL,hwyskmMD.omx,TOLLBTOLLS3,MD -HOV3TOLL_VTOLL,hwyskmMD.omx,TOLLVTOLLS3,MD -SOV_TIME,hwyskmPM.omx,TIMEDA,PM -SOV_DIST,hwyskmPM.omx,DISTDA,PM -SOV_BTOLL,hwyskmPM.omx,BTOLLDA,PM -HOV2_TIME,hwyskmPM.omx,TIMES2,PM -HOV2_DIST,hwyskmPM.omx,DISTS2,PM -HOV2_BTOLL,hwyskmPM.omx,BTOLLS2,PM -HOV3_TIME,hwyskmPM.omx,TIMES3,PM -HOV3_DIST,hwyskmPM.omx,DISTS3,PM -HOV3_BTOLL,hwyskmPM.omx,BTOLLS3,PM -SOVTOLL_TIME,hwyskmPM.omx,TOLLTIMEDA,PM -SOVTOLL_DIST,hwyskmPM.omx,TOLLDISTDA,PM -SOVTOLL_BTOLL,hwyskmPM.omx,TOLLBTOLLDA,PM -SOVTOLL_VTOLL,hwyskmPM.omx,TOLLVTOLLDA,PM -HOV2TOLL_TIME,hwyskmPM.omx,TOLLTIMES2,PM -HOV2TOLL_DIST,hwyskmPM.omx,TOLLDISTS2,PM -HOV2TOLL_BTOLL,hwyskmPM.omx,TOLLBTOLLS2,PM -HOV2TOLL_VTOLL,hwyskmPM.omx,TOLLVTOLLS2,PM -HOV3TOLL_TIME,hwyskmPM.omx,TOLLTIMES3,PM -HOV3TOLL_DIST,hwyskmPM.omx,TOLLDISTS3,PM -HOV3TOLL_BTOLL,hwyskmPM.omx,TOLLBTOLLS3,PM -HOV3TOLL_VTOLL,hwyskmPM.omx,TOLLVTOLLS3,PM -DIST,nonmotskm.omx,DIST, -DISTWALK,nonmotskm.omx,DISTWALK, -DISTBIKE,nonmotskm.omx,DISTBIKE, -DRV_COM_WLK_WAIT,trnskmAM_DRV_COM_WLK.omx,wait,AM -DRV_COM_WLK_TOTIVT,trnskmAM_DRV_COM_WLK.omx,ivt,AM -DRV_COM_WLK_KEYIVT,trnskmAM_DRV_COM_WLK.omx,ivtCOM,AM -DRV_COM_WLK_FAR,trnskmAM_DRV_COM_WLK.omx,fare,AM -DRV_COM_WLK_DTIM,trnskmAM_DRV_COM_WLK.omx,dtime,AM -DRV_COM_WLK_DDIST,trnskmAM_DRV_COM_WLK.omx,ddist,AM -DRV_COM_WLK_WAUX,trnskmAM_DRV_COM_WLK.omx,waux,AM -DRV_COM_WLK_IWAIT,trnskmAM_DRV_COM_WLK.omx,iwait,AM -DRV_COM_WLK_XWAIT,trnskmAM_DRV_COM_WLK.omx,xwait,AM -DRV_COM_WLK_BOARDS,trnskmAM_DRV_COM_WLK.omx,boards,AM -DRV_EXP_WLK_WAIT,trnskmAM_DRV_EXP_WLK.omx,wait,AM -DRV_EXP_WLK_TOTIVT,trnskmAM_DRV_EXP_WLK.omx,ivt,AM -DRV_EXP_WLK_KEYIVT,trnskmAM_DRV_EXP_WLK.omx,ivtEXP,AM -DRV_EXP_WLK_FAR,trnskmAM_DRV_EXP_WLK.omx,fare,AM -DRV_EXP_WLK_DTIM,trnskmAM_DRV_EXP_WLK.omx,dtime,AM -DRV_EXP_WLK_WAUX,trnskmAM_DRV_EXP_WLK.omx,waux,AM -DRV_EXP_WLK_IWAIT,trnskmAM_DRV_EXP_WLK.omx,iwait,AM -DRV_EXP_WLK_XWAIT,trnskmAM_DRV_EXP_WLK.omx,xwait,AM -DRV_EXP_WLK_BOARDS,trnskmAM_DRV_EXP_WLK.omx,boards,AM -DRV_EXP_WLK_DDIST,trnskmAM_DRV_EXP_WLK.omx,ddist,AM -DRV_HVY_WLK_WAIT,trnskmAM_DRV_HVY_WLK.omx,wait,AM -DRV_HVY_WLK_TOTIVT,trnskmAM_DRV_HVY_WLK.omx,ivt,AM -DRV_HVY_WLK_KEYIVT,trnskmAM_DRV_HVY_WLK.omx,ivtHVY,AM -DRV_HVY_WLK_FAR,trnskmAM_DRV_HVY_WLK.omx,fare,AM -DRV_HVY_WLK_DTIM,trnskmAM_DRV_HVY_WLK.omx,dtime,AM -DRV_HVY_WLK_DDIST,trnskmAM_DRV_HVY_WLK.omx,ddist,AM -DRV_HVY_WLK_WAUX,trnskmAM_DRV_HVY_WLK.omx,waux,AM -DRV_HVY_WLK_IWAIT,trnskmAM_DRV_HVY_WLK.omx,iwait,AM -DRV_HVY_WLK_XWAIT,trnskmAM_DRV_HVY_WLK.omx,xwait,AM -DRV_HVY_WLK_BOARDS,trnskmAM_DRV_HVY_WLK.omx,boards,AM -DRV_LOC_WLK_WAIT,trnskmAM_DRV_LOC_WLK.omx,wait,AM -DRV_LOC_WLK_TOTIVT,trnskmAM_DRV_LOC_WLK.omx,ivt,AM -DRV_LOC_WLK_FAR,trnskmAM_DRV_LOC_WLK.omx,fare,AM -DRV_LOC_WLK_DTIM,trnskmAM_DRV_LOC_WLK.omx,dtime,AM -DRV_LOC_WLK_DDIST,trnskmAM_DRV_LOC_WLK.omx,ddist,AM -DRV_LOC_WLK_WAUX,trnskmAM_DRV_LOC_WLK.omx,waux,AM -DRV_LOC_WLK_IWAIT,trnskmAM_DRV_LOC_WLK.omx,iwait,AM -DRV_LOC_WLK_XWAIT,trnskmAM_DRV_LOC_WLK.omx,xwait,AM -DRV_LOC_WLK_BOARDS,trnskmAM_DRV_LOC_WLK.omx,boards,AM -DRV_LRF_WLK_WAIT,trnskmAM_DRV_LRF_WLK.omx,wait,AM -DRV_LRF_WLK_TOTIVT,trnskmAM_DRV_LRF_WLK.omx,ivt,AM -DRV_LRF_WLK_KEYIVT,trnskmAM_DRV_LRF_WLK.omx,ivtLRF,AM -DRV_LRF_WLK_FERRYIVT,trnskmAM_DRV_LRF_WLK.omx,ivtFerry,AM -DRV_LRF_WLK_FAR,trnskmAM_DRV_LRF_WLK.omx,fare,AM -DRV_LRF_WLK_DTIM,trnskmAM_DRV_LRF_WLK.omx,dtime,AM -DRV_LRF_WLK_DDIST,trnskmAM_DRV_LRF_WLK.omx,ddist,AM -DRV_LRF_WLK_WAUX,trnskmAM_DRV_LRF_WLK.omx,waux,AM -DRV_LRF_WLK_IWAIT,trnskmAM_DRV_LRF_WLK.omx,iwait,AM -DRV_LRF_WLK_XWAIT,trnskmAM_DRV_LRF_WLK.omx,xwait,AM -DRV_LRF_WLK_BOARDS,trnskmAM_DRV_LRF_WLK.omx,boards,AM -WLK_COM_DRV_WAIT,trnskmAM_WLK_COM_DRV.omx,wait,AM -WLK_COM_DRV_TOTIVT,trnskmAM_WLK_COM_DRV.omx,ivt,AM -WLK_COM_DRV_KEYIVT,trnskmAM_WLK_COM_DRV.omx,ivtCOM,AM -WLK_COM_DRV_FAR,trnskmAM_WLK_COM_DRV.omx,fare,AM -WLK_COM_DRV_DTIM,trnskmAM_WLK_COM_DRV.omx,dtime,AM -WLK_COM_DRV_DDIST,trnskmAM_WLK_COM_DRV.omx,ddist,AM -WLK_COM_DRV_WAUX,trnskmAM_WLK_COM_DRV.omx,waux,AM -WLK_COM_DRV_IWAIT,trnskmAM_WLK_COM_DRV.omx,iwait,AM -WLK_COM_DRV_XWAIT,trnskmAM_WLK_COM_DRV.omx,xwait,AM -WLK_COM_DRV_BOARDS,trnskmAM_WLK_COM_DRV.omx,boards,AM -WLK_COM_WLK_WAIT,trnskmAM_WLK_COM_WLK.omx,wait,AM -WLK_COM_WLK_TOTIVT,trnskmAM_WLK_COM_WLK.omx,ivt,AM -WLK_COM_WLK_KEYIVT,trnskmAM_WLK_COM_WLK.omx,ivtCOM,AM -WLK_COM_WLK_FAR,trnskmAM_WLK_COM_WLK.omx,fare,AM -WLK_COM_WLK_WAUX,trnskmAM_WLK_COM_WLK.omx,waux,AM -WLK_COM_WLK_IWAIT,trnskmAM_WLK_COM_WLK.omx,iwait,AM -WLK_COM_WLK_XWAIT,trnskmAM_WLK_COM_WLK.omx,xwait,AM -WLK_COM_WLK_BOARDS,trnskmAM_WLK_COM_WLK.omx,boards,AM -WLK_EXP_DRV_WAIT,trnskmAM_WLK_EXP_DRV.omx,wait,AM -WLK_EXP_DRV_TOTIVT,trnskmAM_WLK_EXP_DRV.omx,ivt,AM -WLK_EXP_DRV_KEYIVT,trnskmAM_WLK_EXP_DRV.omx,ivtEXP,AM -WLK_EXP_DRV_FAR,trnskmAM_WLK_EXP_DRV.omx,fare,AM -WLK_EXP_DRV_DTIM,trnskmAM_WLK_EXP_DRV.omx,dtime,AM -WLK_EXP_DRV_WAUX,trnskmAM_WLK_EXP_DRV.omx,waux,AM -WLK_EXP_DRV_IWAIT,trnskmAM_WLK_EXP_DRV.omx,iwait,AM -WLK_EXP_DRV_XWAIT,trnskmAM_WLK_EXP_DRV.omx,xwait,AM -WLK_EXP_DRV_BOARDS,trnskmAM_WLK_EXP_DRV.omx,boards,AM -WLK_EXP_DRV_DDIST,trnskmAM_WLK_EXP_DRV.omx,ddist,AM -WLK_EXP_WLK_WAIT,trnskmAM_WLK_EXP_WLK.omx,wait,AM -WLK_EXP_WLK_TOTIVT,trnskmAM_WLK_EXP_WLK.omx,ivt,AM -WLK_EXP_WLK_KEYIVT,trnskmAM_WLK_EXP_WLK.omx,ivtEXP,AM -WLK_EXP_WLK_FAR,trnskmAM_WLK_EXP_WLK.omx,fare,AM -WLK_EXP_WLK_WAUX,trnskmAM_WLK_EXP_WLK.omx,waux,AM -WLK_EXP_WLK_IWAIT,trnskmAM_WLK_EXP_WLK.omx,iwait,AM -WLK_EXP_WLK_XWAIT,trnskmAM_WLK_EXP_WLK.omx,xwait,AM -WLK_EXP_WLK_BOARDS,trnskmAM_WLK_EXP_WLK.omx,boards,AM -WLK_HVY_DRV_WAIT,trnskmAM_WLK_HVY_DRV.omx,wait,AM -WLK_HVY_DRV_TOTIVT,trnskmAM_WLK_HVY_DRV.omx,ivt,AM -WLK_HVY_DRV_KEYIVT,trnskmAM_WLK_HVY_DRV.omx,ivtHVY,AM -WLK_HVY_DRV_FAR,trnskmAM_WLK_HVY_DRV.omx,fare,AM -WLK_HVY_DRV_DTIM,trnskmAM_WLK_HVY_DRV.omx,dtime,AM -WLK_HVY_DRV_DDIST,trnskmAM_WLK_HVY_DRV.omx,ddist,AM -WLK_HVY_DRV_WAUX,trnskmAM_WLK_HVY_DRV.omx,waux,AM -WLK_HVY_DRV_IWAIT,trnskmAM_WLK_HVY_DRV.omx,iwait,AM -WLK_HVY_DRV_XWAIT,trnskmAM_WLK_HVY_DRV.omx,xwait,AM -WLK_HVY_DRV_BOARDS,trnskmAM_WLK_HVY_DRV.omx,boards,AM -WLK_HVY_WLK_WAIT,trnskmAM_WLK_HVY_WLK.omx,wait,AM -WLK_HVY_WLK_TOTIVT,trnskmAM_WLK_HVY_WLK.omx,ivt,AM -WLK_HVY_WLK_KEYIVT,trnskmAM_WLK_HVY_WLK.omx,ivtHVY,AM -WLK_HVY_WLK_FAR,trnskmAM_WLK_HVY_WLK.omx,fare,AM -WLK_HVY_WLK_WAUX,trnskmAM_WLK_HVY_WLK.omx,waux,AM -WLK_HVY_WLK_IWAIT,trnskmAM_WLK_HVY_WLK.omx,iwait,AM -WLK_HVY_WLK_XWAIT,trnskmAM_WLK_HVY_WLK.omx,xwait,AM -WLK_HVY_WLK_BOARDS,trnskmAM_WLK_HVY_WLK.omx,boards,AM -WLK_LOC_DRV_WAIT,trnskmAM_WLK_LOC_DRV.omx,wait,AM -WLK_LOC_DRV_TOTIVT,trnskmAM_WLK_LOC_DRV.omx,ivt,AM -WLK_LOC_DRV_FAR,trnskmAM_WLK_LOC_DRV.omx,fare,AM -WLK_LOC_DRV_DTIM,trnskmAM_WLK_LOC_DRV.omx,dtime,AM -WLK_LOC_DRV_DDIST,trnskmAM_WLK_LOC_DRV.omx,ddist,AM -WLK_LOC_DRV_WAUX,trnskmAM_WLK_LOC_DRV.omx,waux,AM -WLK_LOC_DRV_IWAIT,trnskmAM_WLK_LOC_DRV.omx,iwait,AM -WLK_LOC_DRV_XWAIT,trnskmAM_WLK_LOC_DRV.omx,xwait,AM -WLK_LOC_DRV_BOARDS,trnskmAM_WLK_LOC_DRV.omx,boards,AM -WLK_LOC_WLK_WAIT,trnskmAM_WLK_LOC_WLK.omx,wait,AM -WLK_LOC_WLK_TOTIVT,trnskmAM_WLK_LOC_WLK.omx,ivt,AM -WLK_LOC_WLK_FAR,trnskmAM_WLK_LOC_WLK.omx,fare,AM -WLK_LOC_WLK_WAUX,trnskmAM_WLK_LOC_WLK.omx,waux,AM -WLK_LOC_WLK_IWAIT,trnskmAM_WLK_LOC_WLK.omx,iwait,AM -WLK_LOC_WLK_XWAIT,trnskmAM_WLK_LOC_WLK.omx,xwait,AM -WLK_LOC_WLK_BOARDS,trnskmAM_WLK_LOC_WLK.omx,boards,AM -WLK_LRF_DRV_WAIT,trnskmAM_WLK_LRF_DRV.omx,wait,AM -WLK_LRF_DRV_TOTIVT,trnskmAM_WLK_LRF_DRV.omx,ivt,AM -WLK_LRF_DRV_KEYIVT,trnskmAM_WLK_LRF_DRV.omx,ivtLRF,AM -WLK_LRF_DRV_FERRYIVT,trnskmAM_WLK_LRF_DRV.omx,ivtFerry,AM -WLK_LRF_DRV_FAR,trnskmAM_WLK_LRF_DRV.omx,fare,AM -WLK_LRF_DRV_DTIM,trnskmAM_WLK_LRF_DRV.omx,dtime,AM -WLK_LRF_DRV_DDIST,trnskmAM_WLK_LRF_DRV.omx,ddist,AM -WLK_LRF_DRV_WAUX,trnskmAM_WLK_LRF_DRV.omx,waux,AM -WLK_LRF_DRV_IWAIT,trnskmAM_WLK_LRF_DRV.omx,iwait,AM -WLK_LRF_DRV_XWAIT,trnskmAM_WLK_LRF_DRV.omx,xwait,AM -WLK_LRF_DRV_BOARDS,trnskmAM_WLK_LRF_DRV.omx,boards,AM -WLK_LRF_WLK_WAIT,trnskmAM_WLK_LRF_WLK.omx,wait,AM -WLK_LRF_WLK_TOTIVT,trnskmAM_WLK_LRF_WLK.omx,ivt,AM -WLK_LRF_WLK_KEYIVT,trnskmAM_WLK_LRF_WLK.omx,ivtLRF,AM -WLK_LRF_WLK_FERRYIVT,trnskmAM_WLK_LRF_WLK.omx,ivtFerry,AM -WLK_LRF_WLK_FAR,trnskmAM_WLK_LRF_WLK.omx,fare,AM -WLK_LRF_WLK_WAUX,trnskmAM_WLK_LRF_WLK.omx,waux,AM -WLK_LRF_WLK_IWAIT,trnskmAM_WLK_LRF_WLK.omx,iwait,AM -WLK_LRF_WLK_XWAIT,trnskmAM_WLK_LRF_WLK.omx,xwait,AM -WLK_LRF_WLK_BOARDS,trnskmAM_WLK_LRF_WLK.omx,boards,AM -DRV_COM_WLK_WAIT,trnskmEA_DRV_COM_WLK.omx,wait,EA -DRV_COM_WLK_TOTIVT,trnskmEA_DRV_COM_WLK.omx,ivt,EA -DRV_COM_WLK_KEYIVT,trnskmEA_DRV_COM_WLK.omx,ivtCOM,EA -DRV_COM_WLK_FAR,trnskmEA_DRV_COM_WLK.omx,fare,EA -DRV_COM_WLK_DTIM,trnskmEA_DRV_COM_WLK.omx,dtime,EA -DRV_COM_WLK_DDIST,trnskmEA_DRV_COM_WLK.omx,ddist,EA -DRV_COM_WLK_WAUX,trnskmEA_DRV_COM_WLK.omx,waux,EA -DRV_COM_WLK_IWAIT,trnskmEA_DRV_COM_WLK.omx,iwait,EA -DRV_COM_WLK_XWAIT,trnskmEA_DRV_COM_WLK.omx,xwait,EA -DRV_COM_WLK_BOARDS,trnskmEA_DRV_COM_WLK.omx,boards,EA -DRV_EXP_WLK_WAIT,trnskmEA_DRV_EXP_WLK.omx,wait,EA -DRV_EXP_WLK_TOTIVT,trnskmEA_DRV_EXP_WLK.omx,ivt,EA -DRV_EXP_WLK_KEYIVT,trnskmEA_DRV_EXP_WLK.omx,ivtEXP,EA -DRV_EXP_WLK_FAR,trnskmEA_DRV_EXP_WLK.omx,fare,EA -DRV_EXP_WLK_DTIM,trnskmEA_DRV_EXP_WLK.omx,dtime,EA -DRV_EXP_WLK_WAUX,trnskmEA_DRV_EXP_WLK.omx,waux,EA -DRV_EXP_WLK_IWAIT,trnskmEA_DRV_EXP_WLK.omx,iwait,EA -DRV_EXP_WLK_XWAIT,trnskmEA_DRV_EXP_WLK.omx,xwait,EA -DRV_EXP_WLK_BOARDS,trnskmEA_DRV_EXP_WLK.omx,boards,EA -DRV_EXP_WLK_DDIST,trnskmEA_DRV_EXP_WLK.omx,ddist,EA -DRV_HVY_WLK_WAIT,trnskmEA_DRV_HVY_WLK.omx,wait,EA -DRV_HVY_WLK_TOTIVT,trnskmEA_DRV_HVY_WLK.omx,ivt,EA -DRV_HVY_WLK_KEYIVT,trnskmEA_DRV_HVY_WLK.omx,ivtHVY,EA -DRV_HVY_WLK_FAR,trnskmEA_DRV_HVY_WLK.omx,fare,EA -DRV_HVY_WLK_DTIM,trnskmEA_DRV_HVY_WLK.omx,dtime,EA -DRV_HVY_WLK_DDIST,trnskmEA_DRV_HVY_WLK.omx,ddist,EA -DRV_HVY_WLK_WAUX,trnskmEA_DRV_HVY_WLK.omx,waux,EA -DRV_HVY_WLK_IWAIT,trnskmEA_DRV_HVY_WLK.omx,iwait,EA -DRV_HVY_WLK_XWAIT,trnskmEA_DRV_HVY_WLK.omx,xwait,EA -DRV_HVY_WLK_BOARDS,trnskmEA_DRV_HVY_WLK.omx,boards,EA -DRV_LOC_WLK_WAIT,trnskmEA_DRV_LOC_WLK.omx,wait,EA -DRV_LOC_WLK_TOTIVT,trnskmEA_DRV_LOC_WLK.omx,ivt,EA -DRV_LOC_WLK_FAR,trnskmEA_DRV_LOC_WLK.omx,fare,EA -DRV_LOC_WLK_DTIM,trnskmEA_DRV_LOC_WLK.omx,dtime,EA -DRV_LOC_WLK_DDIST,trnskmEA_DRV_LOC_WLK.omx,ddist,EA -DRV_LOC_WLK_WAUX,trnskmEA_DRV_LOC_WLK.omx,waux,EA -DRV_LOC_WLK_IWAIT,trnskmEA_DRV_LOC_WLK.omx,iwait,EA -DRV_LOC_WLK_XWAIT,trnskmEA_DRV_LOC_WLK.omx,xwait,EA -DRV_LOC_WLK_BOARDS,trnskmEA_DRV_LOC_WLK.omx,boards,EA -DRV_LRF_WLK_WAIT,trnskmEA_DRV_LRF_WLK.omx,wait,EA -DRV_LRF_WLK_TOTIVT,trnskmEA_DRV_LRF_WLK.omx,ivt,EA -DRV_LRF_WLK_KEYIVT,trnskmEA_DRV_LRF_WLK.omx,ivtLRF,EA -DRV_LRF_WLK_FERRYIVT,trnskmEA_DRV_LRF_WLK.omx,ivtFerry,EA -DRV_LRF_WLK_FAR,trnskmEA_DRV_LRF_WLK.omx,fare,EA -DRV_LRF_WLK_DTIM,trnskmEA_DRV_LRF_WLK.omx,dtime,EA -DRV_LRF_WLK_DDIST,trnskmEA_DRV_LRF_WLK.omx,ddist,EA -DRV_LRF_WLK_WAUX,trnskmEA_DRV_LRF_WLK.omx,waux,EA -DRV_LRF_WLK_IWAIT,trnskmEA_DRV_LRF_WLK.omx,iwait,EA -DRV_LRF_WLK_XWAIT,trnskmEA_DRV_LRF_WLK.omx,xwait,EA -DRV_LRF_WLK_BOARDS,trnskmEA_DRV_LRF_WLK.omx,boards,EA -WLK_COM_DRV_WAIT,trnskmEA_WLK_COM_DRV.omx,wait,EA -WLK_COM_DRV_TOTIVT,trnskmEA_WLK_COM_DRV.omx,ivt,EA -WLK_COM_DRV_KEYIVT,trnskmEA_WLK_COM_DRV.omx,ivtCOM,EA -WLK_COM_DRV_FAR,trnskmEA_WLK_COM_DRV.omx,fare,EA -WLK_COM_DRV_DTIM,trnskmEA_WLK_COM_DRV.omx,dtime,EA -WLK_COM_DRV_DDIST,trnskmEA_WLK_COM_DRV.omx,ddist,EA -WLK_COM_DRV_WAUX,trnskmEA_WLK_COM_DRV.omx,waux,EA -WLK_COM_DRV_IWAIT,trnskmEA_WLK_COM_DRV.omx,iwait,EA -WLK_COM_DRV_XWAIT,trnskmEA_WLK_COM_DRV.omx,xwait,EA -WLK_COM_DRV_BOARDS,trnskmEA_WLK_COM_DRV.omx,boards,EA -WLK_COM_WLK_WAIT,trnskmEA_WLK_COM_WLK.omx,wait,EA -WLK_COM_WLK_TOTIVT,trnskmEA_WLK_COM_WLK.omx,ivt,EA -WLK_COM_WLK_KEYIVT,trnskmEA_WLK_COM_WLK.omx,ivtCOM,EA -WLK_COM_WLK_FAR,trnskmEA_WLK_COM_WLK.omx,fare,EA -WLK_COM_WLK_WAUX,trnskmEA_WLK_COM_WLK.omx,waux,EA -WLK_COM_WLK_IWAIT,trnskmEA_WLK_COM_WLK.omx,iwait,EA -WLK_COM_WLK_XWAIT,trnskmEA_WLK_COM_WLK.omx,xwait,EA -WLK_COM_WLK_BOARDS,trnskmEA_WLK_COM_WLK.omx,boards,EA -WLK_EXP_DRV_WAIT,trnskmEA_WLK_EXP_DRV.omx,wait,EA -WLK_EXP_DRV_TOTIVT,trnskmEA_WLK_EXP_DRV.omx,ivt,EA -WLK_EXP_DRV_KEYIVT,trnskmEA_WLK_EXP_DRV.omx,ivtEXP,EA -WLK_EXP_DRV_FAR,trnskmEA_WLK_EXP_DRV.omx,fare,EA -WLK_EXP_DRV_DTIM,trnskmEA_WLK_EXP_DRV.omx,dtime,EA -WLK_EXP_DRV_DDIST,trnskmEA_WLK_EXP_DRV.omx,ddist,EA -WLK_EXP_DRV_WAUX,trnskmEA_WLK_EXP_DRV.omx,waux,EA -WLK_EXP_DRV_IWAIT,trnskmEA_WLK_EXP_DRV.omx,iwait,EA -WLK_EXP_DRV_XWAIT,trnskmEA_WLK_EXP_DRV.omx,xwait,EA -WLK_EXP_DRV_BOARDS,trnskmEA_WLK_EXP_DRV.omx,boards,EA -WLK_EXP_WLK_WAIT,trnskmEA_WLK_EXP_WLK.omx,wait,EA -WLK_EXP_WLK_TOTIVT,trnskmEA_WLK_EXP_WLK.omx,ivt,EA -WLK_EXP_WLK_KEYIVT,trnskmEA_WLK_EXP_WLK.omx,ivtEXP,EA -WLK_EXP_WLK_FAR,trnskmEA_WLK_EXP_WLK.omx,fare,EA -WLK_EXP_WLK_WAUX,trnskmEA_WLK_EXP_WLK.omx,waux,EA -WLK_EXP_WLK_IWAIT,trnskmEA_WLK_EXP_WLK.omx,iwait,EA -WLK_EXP_WLK_XWAIT,trnskmEA_WLK_EXP_WLK.omx,xwait,EA -WLK_EXP_WLK_BOARDS,trnskmEA_WLK_EXP_WLK.omx,boards,EA -WLK_HVY_DRV_WAIT,trnskmEA_WLK_HVY_DRV.omx,wait,EA -WLK_HVY_DRV_TOTIVT,trnskmEA_WLK_HVY_DRV.omx,ivt,EA -WLK_HVY_DRV_KEYIVT,trnskmEA_WLK_HVY_DRV.omx,ivtHVY,EA -WLK_HVY_DRV_FAR,trnskmEA_WLK_HVY_DRV.omx,fare,EA -WLK_HVY_DRV_DTIM,trnskmEA_WLK_HVY_DRV.omx,dtime,EA -WLK_HVY_DRV_DDIST,trnskmEA_WLK_HVY_DRV.omx,ddist,EA -WLK_HVY_DRV_WAUX,trnskmEA_WLK_HVY_DRV.omx,waux,EA -WLK_HVY_DRV_IWAIT,trnskmEA_WLK_HVY_DRV.omx,iwait,EA -WLK_HVY_DRV_XWAIT,trnskmEA_WLK_HVY_DRV.omx,xwait,EA -WLK_HVY_DRV_BOARDS,trnskmEA_WLK_HVY_DRV.omx,boards,EA -WLK_HVY_WLK_WAIT,trnskmEA_WLK_HVY_WLK.omx,wait,EA -WLK_HVY_WLK_TOTIVT,trnskmEA_WLK_HVY_WLK.omx,ivt,EA -WLK_HVY_WLK_KEYIVT,trnskmEA_WLK_HVY_WLK.omx,ivtHVY,EA -WLK_HVY_WLK_FAR,trnskmEA_WLK_HVY_WLK.omx,fare,EA -WLK_HVY_WLK_WAUX,trnskmEA_WLK_HVY_WLK.omx,waux,EA -WLK_HVY_WLK_IWAIT,trnskmEA_WLK_HVY_WLK.omx,iwait,EA -WLK_HVY_WLK_XWAIT,trnskmEA_WLK_HVY_WLK.omx,xwait,EA -WLK_HVY_WLK_BOARDS,trnskmEA_WLK_HVY_WLK.omx,boards,EA -WLK_LOC_DRV_WAIT,trnskmEA_WLK_LOC_DRV.omx,wait,EA -WLK_LOC_DRV_TOTIVT,trnskmEA_WLK_LOC_DRV.omx,ivt,EA -WLK_LOC_DRV_FAR,trnskmEA_WLK_LOC_DRV.omx,fare,EA -WLK_LOC_DRV_DTIM,trnskmEA_WLK_LOC_DRV.omx,dtime,EA -WLK_LOC_DRV_DDIST,trnskmEA_WLK_LOC_DRV.omx,ddist,EA -WLK_LOC_DRV_WAUX,trnskmEA_WLK_LOC_DRV.omx,waux,EA -WLK_LOC_DRV_IWAIT,trnskmEA_WLK_LOC_DRV.omx,iwait,EA -WLK_LOC_DRV_XWAIT,trnskmEA_WLK_LOC_DRV.omx,xwait,EA -WLK_LOC_DRV_BOARDS,trnskmEA_WLK_LOC_DRV.omx,boards,EA -WLK_LOC_WLK_WAIT,trnskmEA_WLK_LOC_WLK.omx,wait,EA -WLK_LOC_WLK_TOTIVT,trnskmEA_WLK_LOC_WLK.omx,ivt,EA -WLK_LOC_WLK_FAR,trnskmEA_WLK_LOC_WLK.omx,fare,EA -WLK_LOC_WLK_WAUX,trnskmEA_WLK_LOC_WLK.omx,waux,EA -WLK_LOC_WLK_IWAIT,trnskmEA_WLK_LOC_WLK.omx,iwait,EA -WLK_LOC_WLK_XWAIT,trnskmEA_WLK_LOC_WLK.omx,xwait,EA -WLK_LOC_WLK_BOARDS,trnskmEA_WLK_LOC_WLK.omx,boards,EA -WLK_LRF_DRV_WAIT,trnskmEA_WLK_LRF_DRV.omx,wait,EA -WLK_LRF_DRV_TOTIVT,trnskmEA_WLK_LRF_DRV.omx,ivt,EA -WLK_LRF_DRV_KEYIVT,trnskmEA_WLK_LRF_DRV.omx,ivtLRF,EA -WLK_LRF_DRV_FERRYIVT,trnskmEA_WLK_LRF_DRV.omx,ivtFerry,EA -WLK_LRF_DRV_FAR,trnskmEA_WLK_LRF_DRV.omx,fare,EA -WLK_LRF_DRV_DTIM,trnskmEA_WLK_LRF_DRV.omx,dtime,EA -WLK_LRF_DRV_DDIST,trnskmEA_WLK_LRF_DRV.omx,ddist,EA -WLK_LRF_DRV_WAUX,trnskmEA_WLK_LRF_DRV.omx,waux,EA -WLK_LRF_DRV_IWAIT,trnskmEA_WLK_LRF_DRV.omx,iwait,EA -WLK_LRF_DRV_XWAIT,trnskmEA_WLK_LRF_DRV.omx,xwait,EA -WLK_LRF_DRV_BOARDS,trnskmEA_WLK_LRF_DRV.omx,boards,EA -WLK_LRF_WLK_WAIT,trnskmEA_WLK_LRF_WLK.omx,wait,EA -WLK_LRF_WLK_TOTIVT,trnskmEA_WLK_LRF_WLK.omx,ivt,EA -WLK_LRF_WLK_KEYIVT,trnskmEA_WLK_LRF_WLK.omx,ivtLRF,EA -WLK_LRF_WLK_FERRYIVT,trnskmEA_WLK_LRF_WLK.omx,ivtFerry,EA -WLK_LRF_WLK_FAR,trnskmEA_WLK_LRF_WLK.omx,fare,EA -WLK_LRF_WLK_WAUX,trnskmEA_WLK_LRF_WLK.omx,waux,EA -WLK_LRF_WLK_IWAIT,trnskmEA_WLK_LRF_WLK.omx,iwait,EA -WLK_LRF_WLK_XWAIT,trnskmEA_WLK_LRF_WLK.omx,xwait,EA -WLK_LRF_WLK_BOARDS,trnskmEA_WLK_LRF_WLK.omx,boards,EA -DRV_COM_WLK_WAIT,trnskmEV_DRV_COM_WLK.omx,wait,EV -DRV_COM_WLK_TOTIVT,trnskmEV_DRV_COM_WLK.omx,ivt,EV -DRV_COM_WLK_KEYIVT,trnskmEV_DRV_COM_WLK.omx,ivtCOM,EV -DRV_COM_WLK_FAR,trnskmEV_DRV_COM_WLK.omx,fare,EV -DRV_COM_WLK_DTIM,trnskmEV_DRV_COM_WLK.omx,dtime,EV -DRV_COM_WLK_DDIST,trnskmEV_DRV_COM_WLK.omx,ddist,EV -DRV_COM_WLK_WAUX,trnskmEV_DRV_COM_WLK.omx,waux,EV -DRV_COM_WLK_IWAIT,trnskmEV_DRV_COM_WLK.omx,iwait,EV -DRV_COM_WLK_XWAIT,trnskmEV_DRV_COM_WLK.omx,xwait,EV -DRV_COM_WLK_BOARDS,trnskmEV_DRV_COM_WLK.omx,boards,EV -DRV_EXP_WLK_WAIT,trnskmEV_DRV_EXP_WLK.omx,wait,EV -DRV_EXP_WLK_TOTIVT,trnskmEV_DRV_EXP_WLK.omx,ivt,EV -DRV_EXP_WLK_KEYIVT,trnskmEV_DRV_EXP_WLK.omx,ivtEXP,EV -DRV_EXP_WLK_FAR,trnskmEV_DRV_EXP_WLK.omx,fare,EV -DRV_EXP_WLK_DTIM,trnskmEV_DRV_EXP_WLK.omx,dtime,EV -DRV_EXP_WLK_WAUX,trnskmEV_DRV_EXP_WLK.omx,waux,EV -DRV_EXP_WLK_IWAIT,trnskmEV_DRV_EXP_WLK.omx,iwait,EV -DRV_EXP_WLK_XWAIT,trnskmEV_DRV_EXP_WLK.omx,xwait,EV -DRV_EXP_WLK_BOARDS,trnskmEV_DRV_EXP_WLK.omx,boards,EV -DRV_EXP_WLK_DDIST,trnskmEV_DRV_EXP_WLK.omx,ddist,EV -DRV_HVY_WLK_WAIT,trnskmEV_DRV_HVY_WLK.omx,wait,EV -DRV_HVY_WLK_TOTIVT,trnskmEV_DRV_HVY_WLK.omx,ivt,EV -DRV_HVY_WLK_KEYIVT,trnskmEV_DRV_HVY_WLK.omx,ivtHVY,EV -DRV_HVY_WLK_FAR,trnskmEV_DRV_HVY_WLK.omx,fare,EV -DRV_HVY_WLK_DTIM,trnskmEV_DRV_HVY_WLK.omx,dtime,EV -DRV_HVY_WLK_DDIST,trnskmEV_DRV_HVY_WLK.omx,ddist,EV -DRV_HVY_WLK_WAUX,trnskmEV_DRV_HVY_WLK.omx,waux,EV -DRV_HVY_WLK_IWAIT,trnskmEV_DRV_HVY_WLK.omx,iwait,EV -DRV_HVY_WLK_XWAIT,trnskmEV_DRV_HVY_WLK.omx,xwait,EV -DRV_HVY_WLK_BOARDS,trnskmEV_DRV_HVY_WLK.omx,boards,EV -DRV_LOC_WLK_WAIT,trnskmEV_DRV_LOC_WLK.omx,wait,EV -DRV_LOC_WLK_TOTIVT,trnskmEV_DRV_LOC_WLK.omx,ivt,EV -DRV_LOC_WLK_FAR,trnskmEV_DRV_LOC_WLK.omx,fare,EV -DRV_LOC_WLK_DTIM,trnskmEV_DRV_LOC_WLK.omx,dtime,EV -DRV_LOC_WLK_DDIST,trnskmEV_DRV_LOC_WLK.omx,ddist,EV -DRV_LOC_WLK_WAUX,trnskmEV_DRV_LOC_WLK.omx,waux,EV -DRV_LOC_WLK_IWAIT,trnskmEV_DRV_LOC_WLK.omx,iwait,EV -DRV_LOC_WLK_XWAIT,trnskmEV_DRV_LOC_WLK.omx,xwait,EV -DRV_LOC_WLK_BOARDS,trnskmEV_DRV_LOC_WLK.omx,boards,EV -DRV_LRF_WLK_WAIT,trnskmEV_DRV_LRF_WLK.omx,wait,EV -DRV_LRF_WLK_TOTIVT,trnskmEV_DRV_LRF_WLK.omx,ivt,EV -DRV_LRF_WLK_KEYIVT,trnskmEV_DRV_LRF_WLK.omx,ivtLRF,EV -DRV_LRF_WLK_FERRYIVT,trnskmEV_DRV_LRF_WLK.omx,ivtFerry,EV -DRV_LRF_WLK_FAR,trnskmEV_DRV_LRF_WLK.omx,fare,EV -DRV_LRF_WLK_DTIM,trnskmEV_DRV_LRF_WLK.omx,dtime,EV -DRV_LRF_WLK_DDIST,trnskmEV_DRV_LRF_WLK.omx,ddist,EV -DRV_LRF_WLK_WAUX,trnskmEV_DRV_LRF_WLK.omx,waux,EV -DRV_LRF_WLK_IWAIT,trnskmEV_DRV_LRF_WLK.omx,iwait,EV -DRV_LRF_WLK_XWAIT,trnskmEV_DRV_LRF_WLK.omx,xwait,EV -DRV_LRF_WLK_BOARDS,trnskmEV_DRV_LRF_WLK.omx,boards,EV -WLK_COM_DRV_WAIT,trnskmEV_WLK_COM_DRV.omx,wait,EV -WLK_COM_DRV_TOTIVT,trnskmEV_WLK_COM_DRV.omx,ivt,EV -WLK_COM_DRV_KEYIVT,trnskmEV_WLK_COM_DRV.omx,ivtCOM,EV -WLK_COM_DRV_FAR,trnskmEV_WLK_COM_DRV.omx,fare,EV -WLK_COM_DRV_DTIM,trnskmEV_WLK_COM_DRV.omx,dtime,EV -WLK_COM_DRV_DDIST,trnskmEV_WLK_COM_DRV.omx,ddist,EV -WLK_COM_DRV_WAUX,trnskmEV_WLK_COM_DRV.omx,waux,EV -WLK_COM_DRV_IWAIT,trnskmEV_WLK_COM_DRV.omx,iwait,EV -WLK_COM_DRV_XWAIT,trnskmEV_WLK_COM_DRV.omx,xwait,EV -WLK_COM_DRV_BOARDS,trnskmEV_WLK_COM_DRV.omx,boards,EV -WLK_COM_WLK_WAIT,trnskmEV_WLK_COM_WLK.omx,wait,EV -WLK_COM_WLK_TOTIVT,trnskmEV_WLK_COM_WLK.omx,ivt,EV -WLK_COM_WLK_KEYIVT,trnskmEV_WLK_COM_WLK.omx,ivtCOM,EV -WLK_COM_WLK_FAR,trnskmEV_WLK_COM_WLK.omx,fare,EV -WLK_COM_WLK_WAUX,trnskmEV_WLK_COM_WLK.omx,waux,EV -WLK_COM_WLK_IWAIT,trnskmEV_WLK_COM_WLK.omx,iwait,EV -WLK_COM_WLK_XWAIT,trnskmEV_WLK_COM_WLK.omx,xwait,EV -WLK_COM_WLK_BOARDS,trnskmEV_WLK_COM_WLK.omx,boards,EV -WLK_EXP_DRV_WAIT,trnskmEV_WLK_EXP_DRV.omx,wait,EV -WLK_EXP_DRV_TOTIVT,trnskmEV_WLK_EXP_DRV.omx,ivt,EV -WLK_EXP_DRV_KEYIVT,trnskmEV_WLK_EXP_DRV.omx,ivtEXP,EV -WLK_EXP_DRV_FAR,trnskmEV_WLK_EXP_DRV.omx,fare,EV -WLK_EXP_DRV_DTIM,trnskmEV_WLK_EXP_DRV.omx,dtime,EV -WLK_EXP_DRV_WAUX,trnskmEV_WLK_EXP_DRV.omx,waux,EV -WLK_EXP_DRV_IWAIT,trnskmEV_WLK_EXP_DRV.omx,iwait,EV -WLK_EXP_DRV_XWAIT,trnskmEV_WLK_EXP_DRV.omx,xwait,EV -WLK_EXP_DRV_BOARDS,trnskmEV_WLK_EXP_DRV.omx,boards,EV -WLK_EXP_DRV_DDIST,trnskmEV_WLK_EXP_DRV.omx,ddist,EV -WLK_EXP_WLK_WAIT,trnskmEV_WLK_EXP_WLK.omx,wait,EV -WLK_EXP_WLK_TOTIVT,trnskmEV_WLK_EXP_WLK.omx,ivt,EV -WLK_EXP_WLK_KEYIVT,trnskmEV_WLK_EXP_WLK.omx,ivtEXP,EV -WLK_EXP_WLK_FAR,trnskmEV_WLK_EXP_WLK.omx,fare,EV -WLK_EXP_WLK_WAUX,trnskmEV_WLK_EXP_WLK.omx,waux,EV -WLK_EXP_WLK_IWAIT,trnskmEV_WLK_EXP_WLK.omx,iwait,EV -WLK_EXP_WLK_XWAIT,trnskmEV_WLK_EXP_WLK.omx,xwait,EV -WLK_EXP_WLK_BOARDS,trnskmEV_WLK_EXP_WLK.omx,boards,EV -WLK_HVY_DRV_WAIT,trnskmEV_WLK_HVY_DRV.omx,wait,EV -WLK_HVY_DRV_TOTIVT,trnskmEV_WLK_HVY_DRV.omx,ivt,EV -WLK_HVY_DRV_KEYIVT,trnskmEV_WLK_HVY_DRV.omx,ivtHVY,EV -WLK_HVY_DRV_FAR,trnskmEV_WLK_HVY_DRV.omx,fare,EV -WLK_HVY_DRV_DTIM,trnskmEV_WLK_HVY_DRV.omx,dtime,EV -WLK_HVY_DRV_DDIST,trnskmEV_WLK_HVY_DRV.omx,ddist,EV -WLK_HVY_DRV_WAUX,trnskmEV_WLK_HVY_DRV.omx,waux,EV -WLK_HVY_DRV_IWAIT,trnskmEV_WLK_HVY_DRV.omx,iwait,EV -WLK_HVY_DRV_XWAIT,trnskmEV_WLK_HVY_DRV.omx,xwait,EV -WLK_HVY_DRV_BOARDS,trnskmEV_WLK_HVY_DRV.omx,boards,EV -WLK_HVY_WLK_WAIT,trnskmEV_WLK_HVY_WLK.omx,wait,EV -WLK_HVY_WLK_TOTIVT,trnskmEV_WLK_HVY_WLK.omx,ivt,EV -WLK_HVY_WLK_KEYIVT,trnskmEV_WLK_HVY_WLK.omx,ivtHVY,EV -WLK_HVY_WLK_FAR,trnskmEV_WLK_HVY_WLK.omx,fare,EV -WLK_HVY_WLK_WAUX,trnskmEV_WLK_HVY_WLK.omx,waux,EV -WLK_HVY_WLK_IWAIT,trnskmEV_WLK_HVY_WLK.omx,iwait,EV -WLK_HVY_WLK_XWAIT,trnskmEV_WLK_HVY_WLK.omx,xwait,EV -WLK_HVY_WLK_BOARDS,trnskmEV_WLK_HVY_WLK.omx,boards,EV -WLK_LOC_DRV_WAIT,trnskmEV_WLK_LOC_DRV.omx,wait,EV -WLK_LOC_DRV_TOTIVT,trnskmEV_WLK_LOC_DRV.omx,ivt,EV -WLK_LOC_DRV_FAR,trnskmEV_WLK_LOC_DRV.omx,fare,EV -WLK_LOC_DRV_DTIM,trnskmEV_WLK_LOC_DRV.omx,dtime,EV -WLK_LOC_DRV_DDIST,trnskmEV_WLK_LOC_DRV.omx,ddist,EV -WLK_LOC_DRV_WAUX,trnskmEV_WLK_LOC_DRV.omx,waux,EV -WLK_LOC_DRV_IWAIT,trnskmEV_WLK_LOC_DRV.omx,iwait,EV -WLK_LOC_DRV_XWAIT,trnskmEV_WLK_LOC_DRV.omx,xwait,EV -WLK_LOC_DRV_BOARDS,trnskmEV_WLK_LOC_DRV.omx,boards,EV -WLK_LOC_WLK_WAIT,trnskmEV_WLK_LOC_WLK.omx,wait,EV -WLK_LOC_WLK_TOTIVT,trnskmEV_WLK_LOC_WLK.omx,ivt,EV -WLK_LOC_WLK_FAR,trnskmEV_WLK_LOC_WLK.omx,fare,EV -WLK_LOC_WLK_WAUX,trnskmEV_WLK_LOC_WLK.omx,waux,EV -WLK_LOC_WLK_IWAIT,trnskmEV_WLK_LOC_WLK.omx,iwait,EV -WLK_LOC_WLK_XWAIT,trnskmEV_WLK_LOC_WLK.omx,xwait,EV -WLK_LOC_WLK_BOARDS,trnskmEV_WLK_LOC_WLK.omx,boards,EV -WLK_LRF_DRV_WAIT,trnskmEV_WLK_LRF_DRV.omx,wait,EV -WLK_LRF_DRV_TOTIVT,trnskmEV_WLK_LRF_DRV.omx,ivt,EV -WLK_LRF_DRV_KEYIVT,trnskmEV_WLK_LRF_DRV.omx,ivtLRF,EV -WLK_LRF_DRV_FERRYIVT,trnskmEV_WLK_LRF_DRV.omx,ivtFerry,EV -WLK_LRF_DRV_FAR,trnskmEV_WLK_LRF_DRV.omx,fare,EV -WLK_LRF_DRV_DTIM,trnskmEV_WLK_LRF_DRV.omx,dtime,EV -WLK_LRF_DRV_DDIST,trnskmEV_WLK_LRF_DRV.omx,ddist,EV -WLK_LRF_DRV_WAUX,trnskmEV_WLK_LRF_DRV.omx,waux,EV -WLK_LRF_DRV_IWAIT,trnskmEV_WLK_LRF_DRV.omx,iwait,EV -WLK_LRF_DRV_XWAIT,trnskmEV_WLK_LRF_DRV.omx,xwait,EV -WLK_LRF_DRV_BOARDS,trnskmEV_WLK_LRF_DRV.omx,boards,EV -WLK_LRF_WLK_WAIT,trnskmEV_WLK_LRF_WLK.omx,wait,EV -WLK_LRF_WLK_TOTIVT,trnskmEV_WLK_LRF_WLK.omx,ivt,EV -WLK_LRF_WLK_KEYIVT,trnskmEV_WLK_LRF_WLK.omx,ivtLRF,EV -WLK_LRF_WLK_FERRYIVT,trnskmEV_WLK_LRF_WLK.omx,ivtFerry,EV -WLK_LRF_WLK_FAR,trnskmEV_WLK_LRF_WLK.omx,fare,EV -WLK_LRF_WLK_WAUX,trnskmEV_WLK_LRF_WLK.omx,waux,EV -WLK_LRF_WLK_IWAIT,trnskmEV_WLK_LRF_WLK.omx,iwait,EV -WLK_LRF_WLK_XWAIT,trnskmEV_WLK_LRF_WLK.omx,xwait,EV -WLK_LRF_WLK_BOARDS,trnskmEV_WLK_LRF_WLK.omx,boards,EV -DRV_COM_WLK_WAIT,trnskmMD_DRV_COM_WLK.omx,wait,MD -DRV_COM_WLK_TOTIVT,trnskmMD_DRV_COM_WLK.omx,ivt,MD -DRV_COM_WLK_KEYIVT,trnskmMD_DRV_COM_WLK.omx,ivtCOM,MD -DRV_COM_WLK_FAR,trnskmMD_DRV_COM_WLK.omx,fare,MD -DRV_COM_WLK_DTIM,trnskmMD_DRV_COM_WLK.omx,dtime,MD -DRV_COM_WLK_DDIST,trnskmMD_DRV_COM_WLK.omx,ddist,MD -DRV_COM_WLK_WAUX,trnskmMD_DRV_COM_WLK.omx,waux,MD -DRV_COM_WLK_IWAIT,trnskmMD_DRV_COM_WLK.omx,iwait,MD -DRV_COM_WLK_XWAIT,trnskmMD_DRV_COM_WLK.omx,xwait,MD -DRV_COM_WLK_BOARDS,trnskmMD_DRV_COM_WLK.omx,boards,MD -DRV_EXP_WLK_WAIT,trnskmMD_DRV_EXP_WLK.omx,wait,MD -DRV_EXP_WLK_TOTIVT,trnskmMD_DRV_EXP_WLK.omx,ivt,MD -DRV_EXP_WLK_KEYIVT,trnskmMD_DRV_EXP_WLK.omx,ivtEXP,MD -DRV_EXP_WLK_FAR,trnskmMD_DRV_EXP_WLK.omx,fare,MD -DRV_EXP_WLK_DTIM,trnskmMD_DRV_EXP_WLK.omx,dtime,MD -DRV_EXP_WLK_WAUX,trnskmMD_DRV_EXP_WLK.omx,waux,MD -DRV_EXP_WLK_IWAIT,trnskmMD_DRV_EXP_WLK.omx,iwait,MD -DRV_EXP_WLK_XWAIT,trnskmMD_DRV_EXP_WLK.omx,xwait,MD -DRV_EXP_WLK_BOARDS,trnskmMD_DRV_EXP_WLK.omx,boards,MD -DRV_EXP_WLK_DDIST,trnskmMD_DRV_EXP_WLK.omx,ddist,MD -DRV_HVY_WLK_WAIT,trnskmMD_DRV_HVY_WLK.omx,wait,MD -DRV_HVY_WLK_TOTIVT,trnskmMD_DRV_HVY_WLK.omx,ivt,MD -DRV_HVY_WLK_KEYIVT,trnskmMD_DRV_HVY_WLK.omx,ivtHVY,MD -DRV_HVY_WLK_FAR,trnskmMD_DRV_HVY_WLK.omx,fare,MD -DRV_HVY_WLK_DTIM,trnskmMD_DRV_HVY_WLK.omx,dtime,MD -DRV_HVY_WLK_DDIST,trnskmMD_DRV_HVY_WLK.omx,ddist,MD -DRV_HVY_WLK_WAUX,trnskmMD_DRV_HVY_WLK.omx,waux,MD -DRV_HVY_WLK_IWAIT,trnskmMD_DRV_HVY_WLK.omx,iwait,MD -DRV_HVY_WLK_XWAIT,trnskmMD_DRV_HVY_WLK.omx,xwait,MD -DRV_HVY_WLK_BOARDS,trnskmMD_DRV_HVY_WLK.omx,boards,MD -DRV_LOC_WLK_WAIT,trnskmMD_DRV_LOC_WLK.omx,wait,MD -DRV_LOC_WLK_TOTIVT,trnskmMD_DRV_LOC_WLK.omx,ivt,MD -DRV_LOC_WLK_FAR,trnskmMD_DRV_LOC_WLK.omx,fare,MD -DRV_LOC_WLK_DTIM,trnskmMD_DRV_LOC_WLK.omx,dtime,MD -DRV_LOC_WLK_DDIST,trnskmMD_DRV_LOC_WLK.omx,ddist,MD -DRV_LOC_WLK_WAUX,trnskmMD_DRV_LOC_WLK.omx,waux,MD -DRV_LOC_WLK_IWAIT,trnskmMD_DRV_LOC_WLK.omx,iwait,MD -DRV_LOC_WLK_XWAIT,trnskmMD_DRV_LOC_WLK.omx,xwait,MD -DRV_LOC_WLK_BOARDS,trnskmMD_DRV_LOC_WLK.omx,boards,MD -DRV_LRF_WLK_WAIT,trnskmMD_DRV_LRF_WLK.omx,wait,MD -DRV_LRF_WLK_TOTIVT,trnskmMD_DRV_LRF_WLK.omx,ivt,MD -DRV_LRF_WLK_KEYIVT,trnskmMD_DRV_LRF_WLK.omx,ivtLRF,MD -DRV_LRF_WLK_FERRYIVT,trnskmMD_DRV_LRF_WLK.omx,ivtFerry,MD -DRV_LRF_WLK_FAR,trnskmMD_DRV_LRF_WLK.omx,fare,MD -DRV_LRF_WLK_DTIM,trnskmMD_DRV_LRF_WLK.omx,dtime,MD -DRV_LRF_WLK_DDIST,trnskmMD_DRV_LRF_WLK.omx,ddist,MD -DRV_LRF_WLK_WAUX,trnskmMD_DRV_LRF_WLK.omx,waux,MD -DRV_LRF_WLK_IWAIT,trnskmMD_DRV_LRF_WLK.omx,iwait,MD -DRV_LRF_WLK_XWAIT,trnskmMD_DRV_LRF_WLK.omx,xwait,MD -DRV_LRF_WLK_BOARDS,trnskmMD_DRV_LRF_WLK.omx,boards,MD -WLK_COM_DRV_WAIT,trnskmMD_WLK_COM_DRV.omx,wait,MD -WLK_COM_DRV_TOTIVT,trnskmMD_WLK_COM_DRV.omx,ivt,MD -WLK_COM_DRV_KEYIVT,trnskmMD_WLK_COM_DRV.omx,ivtCOM,MD -WLK_COM_DRV_FAR,trnskmMD_WLK_COM_DRV.omx,fare,MD -WLK_COM_DRV_DTIM,trnskmMD_WLK_COM_DRV.omx,dtime,MD -WLK_COM_DRV_DDIST,trnskmMD_WLK_COM_DRV.omx,ddist,MD -WLK_COM_DRV_WAUX,trnskmMD_WLK_COM_DRV.omx,waux,MD -WLK_COM_DRV_IWAIT,trnskmMD_WLK_COM_DRV.omx,iwait,MD -WLK_COM_DRV_XWAIT,trnskmMD_WLK_COM_DRV.omx,xwait,MD -WLK_COM_DRV_BOARDS,trnskmMD_WLK_COM_DRV.omx,boards,MD -WLK_COM_WLK_WAIT,trnskmMD_WLK_COM_WLK.omx,wait,MD -WLK_COM_WLK_TOTIVT,trnskmMD_WLK_COM_WLK.omx,ivt,MD -WLK_COM_WLK_KEYIVT,trnskmMD_WLK_COM_WLK.omx,ivtCOM,MD -WLK_COM_WLK_FAR,trnskmMD_WLK_COM_WLK.omx,fare,MD -WLK_COM_WLK_WAUX,trnskmMD_WLK_COM_WLK.omx,waux,MD -WLK_COM_WLK_IWAIT,trnskmMD_WLK_COM_WLK.omx,iwait,MD -WLK_COM_WLK_XWAIT,trnskmMD_WLK_COM_WLK.omx,xwait,MD -WLK_COM_WLK_BOARDS,trnskmMD_WLK_COM_WLK.omx,boards,MD -WLK_EXP_DRV_WAIT,trnskmMD_WLK_EXP_DRV.omx,wait,MD -WLK_EXP_DRV_TOTIVT,trnskmMD_WLK_EXP_DRV.omx,ivt,MD -WLK_EXP_DRV_KEYIVT,trnskmMD_WLK_EXP_DRV.omx,ivtEXP,MD -WLK_EXP_DRV_FAR,trnskmMD_WLK_EXP_DRV.omx,fare,MD -WLK_EXP_DRV_DTIM,trnskmMD_WLK_EXP_DRV.omx,dtime,MD -WLK_EXP_DRV_WAUX,trnskmMD_WLK_EXP_DRV.omx,waux,MD -WLK_EXP_DRV_IWAIT,trnskmMD_WLK_EXP_DRV.omx,iwait,MD -WLK_EXP_DRV_XWAIT,trnskmMD_WLK_EXP_DRV.omx,xwait,MD -WLK_EXP_DRV_BOARDS,trnskmMD_WLK_EXP_DRV.omx,boards,MD -WLK_EXP_DRV_DDIST,trnskmMD_WLK_EXP_DRV.omx,ddist,MD -WLK_EXP_WLK_WAIT,trnskmMD_WLK_EXP_WLK.omx,wait,MD -WLK_EXP_WLK_TOTIVT,trnskmMD_WLK_EXP_WLK.omx,ivt,MD -WLK_EXP_WLK_KEYIVT,trnskmMD_WLK_EXP_WLK.omx,ivtEXP,MD -WLK_EXP_WLK_FAR,trnskmMD_WLK_EXP_WLK.omx,fare,MD -WLK_EXP_WLK_WAUX,trnskmMD_WLK_EXP_WLK.omx,waux,MD -WLK_EXP_WLK_IWAIT,trnskmMD_WLK_EXP_WLK.omx,iwait,MD -WLK_EXP_WLK_XWAIT,trnskmMD_WLK_EXP_WLK.omx,xwait,MD -WLK_EXP_WLK_BOARDS,trnskmMD_WLK_EXP_WLK.omx,boards,MD -WLK_HVY_DRV_WAIT,trnskmMD_WLK_HVY_DRV.omx,wait,MD -WLK_HVY_DRV_TOTIVT,trnskmMD_WLK_HVY_DRV.omx,ivt,MD -WLK_HVY_DRV_KEYIVT,trnskmMD_WLK_HVY_DRV.omx,ivtHVY,MD -WLK_HVY_DRV_FAR,trnskmMD_WLK_HVY_DRV.omx,fare,MD -WLK_HVY_DRV_DTIM,trnskmMD_WLK_HVY_DRV.omx,dtime,MD -WLK_HVY_DRV_DDIST,trnskmMD_WLK_HVY_DRV.omx,ddist,MD -WLK_HVY_DRV_WAUX,trnskmMD_WLK_HVY_DRV.omx,waux,MD -WLK_HVY_DRV_IWAIT,trnskmMD_WLK_HVY_DRV.omx,iwait,MD -WLK_HVY_DRV_XWAIT,trnskmMD_WLK_HVY_DRV.omx,xwait,MD -WLK_HVY_DRV_BOARDS,trnskmMD_WLK_HVY_DRV.omx,boards,MD -WLK_HVY_WLK_WAIT,trnskmMD_WLK_HVY_WLK.omx,wait,MD -WLK_HVY_WLK_TOTIVT,trnskmMD_WLK_HVY_WLK.omx,ivt,MD -WLK_HVY_WLK_KEYIVT,trnskmMD_WLK_HVY_WLK.omx,ivtHVY,MD -WLK_HVY_WLK_FAR,trnskmMD_WLK_HVY_WLK.omx,fare,MD -WLK_HVY_WLK_WAUX,trnskmMD_WLK_HVY_WLK.omx,waux,MD -WLK_HVY_WLK_IWAIT,trnskmMD_WLK_HVY_WLK.omx,iwait,MD -WLK_HVY_WLK_XWAIT,trnskmMD_WLK_HVY_WLK.omx,xwait,MD -WLK_HVY_WLK_BOARDS,trnskmMD_WLK_HVY_WLK.omx,boards,MD -WLK_LOC_DRV_WAIT,trnskmMD_WLK_LOC_DRV.omx,wait,MD -WLK_LOC_DRV_TOTIVT,trnskmMD_WLK_LOC_DRV.omx,ivt,MD -WLK_LOC_DRV_FAR,trnskmMD_WLK_LOC_DRV.omx,fare,MD -WLK_LOC_DRV_DTIM,trnskmMD_WLK_LOC_DRV.omx,dtime,MD -WLK_LOC_DRV_DDIST,trnskmMD_WLK_LOC_DRV.omx,ddist,MD -WLK_LOC_DRV_WAUX,trnskmMD_WLK_LOC_DRV.omx,waux,MD -WLK_LOC_DRV_IWAIT,trnskmMD_WLK_LOC_DRV.omx,iwait,MD -WLK_LOC_DRV_XWAIT,trnskmMD_WLK_LOC_DRV.omx,xwait,MD -WLK_LOC_DRV_BOARDS,trnskmMD_WLK_LOC_DRV.omx,boards,MD -WLK_LOC_WLK_WAIT,trnskmMD_WLK_LOC_WLK.omx,wait,MD -WLK_LOC_WLK_TOTIVT,trnskmMD_WLK_LOC_WLK.omx,ivt,MD -WLK_LOC_WLK_FAR,trnskmMD_WLK_LOC_WLK.omx,fare,MD -WLK_LOC_WLK_WAUX,trnskmMD_WLK_LOC_WLK.omx,waux,MD -WLK_LOC_WLK_IWAIT,trnskmMD_WLK_LOC_WLK.omx,iwait,MD -WLK_LOC_WLK_XWAIT,trnskmMD_WLK_LOC_WLK.omx,xwait,MD -WLK_LOC_WLK_BOARDS,trnskmMD_WLK_LOC_WLK.omx,boards,MD -WLK_LRF_DRV_WAIT,trnskmMD_WLK_LRF_DRV.omx,wait,MD -WLK_LRF_DRV_TOTIVT,trnskmMD_WLK_LRF_DRV.omx,ivt,MD -WLK_LRF_DRV_KEYIVT,trnskmMD_WLK_LRF_DRV.omx,ivtLRF,MD -WLK_LRF_DRV_FERRYIVT,trnskmMD_WLK_LRF_DRV.omx,ivtFerry,MD -WLK_LRF_DRV_FAR,trnskmMD_WLK_LRF_DRV.omx,fare,MD -WLK_LRF_DRV_DTIM,trnskmMD_WLK_LRF_DRV.omx,dtime,MD -WLK_LRF_DRV_DDIST,trnskmMD_WLK_LRF_DRV.omx,ddist,MD -WLK_LRF_DRV_WAUX,trnskmMD_WLK_LRF_DRV.omx,waux,MD -WLK_LRF_DRV_IWAIT,trnskmMD_WLK_LRF_DRV.omx,iwait,MD -WLK_LRF_DRV_XWAIT,trnskmMD_WLK_LRF_DRV.omx,xwait,MD -WLK_LRF_DRV_BOARDS,trnskmMD_WLK_LRF_DRV.omx,boards,MD -WLK_LRF_WLK_WAIT,trnskmMD_WLK_LRF_WLK.omx,wait,MD -WLK_LRF_WLK_TOTIVT,trnskmMD_WLK_LRF_WLK.omx,ivt,MD -WLK_LRF_WLK_KEYIVT,trnskmMD_WLK_LRF_WLK.omx,ivtLRF,MD -WLK_LRF_WLK_FERRYIVT,trnskmMD_WLK_LRF_WLK.omx,ivtFerry,MD -WLK_LRF_WLK_FAR,trnskmMD_WLK_LRF_WLK.omx,fare,MD -WLK_LRF_WLK_WAUX,trnskmMD_WLK_LRF_WLK.omx,waux,MD -WLK_LRF_WLK_IWAIT,trnskmMD_WLK_LRF_WLK.omx,iwait,MD -WLK_LRF_WLK_XWAIT,trnskmMD_WLK_LRF_WLK.omx,xwait,MD -WLK_LRF_WLK_BOARDS,trnskmMD_WLK_LRF_WLK.omx,boards,MD -DRV_COM_WLK_WAIT,trnskmPM_DRV_COM_WLK.omx,wait,PM -DRV_COM_WLK_TOTIVT,trnskmPM_DRV_COM_WLK.omx,ivt,PM -DRV_COM_WLK_KEYIVT,trnskmPM_DRV_COM_WLK.omx,ivtCOM,PM -DRV_COM_WLK_FAR,trnskmPM_DRV_COM_WLK.omx,fare,PM -DRV_COM_WLK_DTIM,trnskmPM_DRV_COM_WLK.omx,dtime,PM -DRV_COM_WLK_DDIST,trnskmPM_DRV_COM_WLK.omx,ddist,PM -DRV_COM_WLK_WAUX,trnskmPM_DRV_COM_WLK.omx,waux,PM -DRV_COM_WLK_IWAIT,trnskmPM_DRV_COM_WLK.omx,iwait,PM -DRV_COM_WLK_XWAIT,trnskmPM_DRV_COM_WLK.omx,xwait,PM -DRV_COM_WLK_BOARDS,trnskmPM_DRV_COM_WLK.omx,boards,PM -DRV_EXP_WLK_WAIT,trnskmPM_DRV_EXP_WLK.omx,wait,PM -DRV_EXP_WLK_TOTIVT,trnskmPM_DRV_EXP_WLK.omx,ivt,PM -DRV_EXP_WLK_KEYIVT,trnskmPM_DRV_EXP_WLK.omx,ivtEXP,PM -DRV_EXP_WLK_FAR,trnskmPM_DRV_EXP_WLK.omx,fare,PM -DRV_EXP_WLK_DTIM,trnskmPM_DRV_EXP_WLK.omx,dtime,PM -DRV_EXP_WLK_WAUX,trnskmPM_DRV_EXP_WLK.omx,waux,PM -DRV_EXP_WLK_IWAIT,trnskmPM_DRV_EXP_WLK.omx,iwait,PM -DRV_EXP_WLK_XWAIT,trnskmPM_DRV_EXP_WLK.omx,xwait,PM -DRV_EXP_WLK_BOARDS,trnskmPM_DRV_EXP_WLK.omx,boards,PM -DRV_EXP_WLK_DDIST,trnskmPM_DRV_EXP_WLK.omx,ddist,PM -DRV_HVY_WLK_WAIT,trnskmPM_DRV_HVY_WLK.omx,wait,PM -DRV_HVY_WLK_TOTIVT,trnskmPM_DRV_HVY_WLK.omx,ivt,PM -DRV_HVY_WLK_KEYIVT,trnskmPM_DRV_HVY_WLK.omx,ivtHVY,PM -DRV_HVY_WLK_FAR,trnskmPM_DRV_HVY_WLK.omx,fare,PM -DRV_HVY_WLK_DTIM,trnskmPM_DRV_HVY_WLK.omx,dtime,PM -DRV_HVY_WLK_DDIST,trnskmPM_DRV_HVY_WLK.omx,ddist,PM -DRV_HVY_WLK_WAUX,trnskmPM_DRV_HVY_WLK.omx,waux,PM -DRV_HVY_WLK_IWAIT,trnskmPM_DRV_HVY_WLK.omx,iwait,PM -DRV_HVY_WLK_XWAIT,trnskmPM_DRV_HVY_WLK.omx,xwait,PM -DRV_HVY_WLK_BOARDS,trnskmPM_DRV_HVY_WLK.omx,boards,PM -DRV_LOC_WLK_WAIT,trnskmPM_DRV_LOC_WLK.omx,wait,PM -DRV_LOC_WLK_TOTIVT,trnskmPM_DRV_LOC_WLK.omx,ivt,PM -DRV_LOC_WLK_FAR,trnskmPM_DRV_LOC_WLK.omx,fare,PM -DRV_LOC_WLK_DTIM,trnskmPM_DRV_LOC_WLK.omx,dtime,PM -DRV_LOC_WLK_DDIST,trnskmPM_DRV_LOC_WLK.omx,ddist,PM -DRV_LOC_WLK_WAUX,trnskmPM_DRV_LOC_WLK.omx,waux,PM -DRV_LOC_WLK_IWAIT,trnskmPM_DRV_LOC_WLK.omx,iwait,PM -DRV_LOC_WLK_XWAIT,trnskmPM_DRV_LOC_WLK.omx,xwait,PM -DRV_LOC_WLK_BOARDS,trnskmPM_DRV_LOC_WLK.omx,boards,PM -DRV_LRF_WLK_WAIT,trnskmPM_DRV_LRF_WLK.omx,wait,PM -DRV_LRF_WLK_TOTIVT,trnskmPM_DRV_LRF_WLK.omx,ivt,PM -DRV_LRF_WLK_KEYIVT,trnskmPM_DRV_LRF_WLK.omx,ivtLRF,PM -DRV_LRF_WLK_FERRYIVT,trnskmPM_DRV_LRF_WLK.omx,ivtFerry,PM -DRV_LRF_WLK_FAR,trnskmPM_DRV_LRF_WLK.omx,fare,PM -DRV_LRF_WLK_DTIM,trnskmPM_DRV_LRF_WLK.omx,dtime,PM -DRV_LRF_WLK_DDIST,trnskmPM_DRV_LRF_WLK.omx,ddist,PM -DRV_LRF_WLK_WAUX,trnskmPM_DRV_LRF_WLK.omx,waux,PM -DRV_LRF_WLK_IWAIT,trnskmPM_DRV_LRF_WLK.omx,iwait,PM -DRV_LRF_WLK_XWAIT,trnskmPM_DRV_LRF_WLK.omx,xwait,PM -DRV_LRF_WLK_BOARDS,trnskmPM_DRV_LRF_WLK.omx,boards,PM -WLK_COM_DRV_WAIT,trnskmPM_WLK_COM_DRV.omx,wait,PM -WLK_COM_DRV_TOTIVT,trnskmPM_WLK_COM_DRV.omx,ivt,PM -WLK_COM_DRV_KEYIVT,trnskmPM_WLK_COM_DRV.omx,ivtCOM,PM -WLK_COM_DRV_FAR,trnskmPM_WLK_COM_DRV.omx,fare,PM -WLK_COM_DRV_DTIM,trnskmPM_WLK_COM_DRV.omx,dtime,PM -WLK_COM_DRV_DDIST,trnskmPM_WLK_COM_DRV.omx,ddist,PM -WLK_COM_DRV_WAUX,trnskmPM_WLK_COM_DRV.omx,waux,PM -WLK_COM_DRV_IWAIT,trnskmPM_WLK_COM_DRV.omx,iwait,PM -WLK_COM_DRV_XWAIT,trnskmPM_WLK_COM_DRV.omx,xwait,PM -WLK_COM_DRV_BOARDS,trnskmPM_WLK_COM_DRV.omx,boards,PM -WLK_COM_WLK_WAIT,trnskmPM_WLK_COM_WLK.omx,wait,PM -WLK_COM_WLK_TOTIVT,trnskmPM_WLK_COM_WLK.omx,ivt,PM -WLK_COM_WLK_KEYIVT,trnskmPM_WLK_COM_WLK.omx,ivtCOM,PM -WLK_COM_WLK_FAR,trnskmPM_WLK_COM_WLK.omx,fare,PM -WLK_COM_WLK_WAUX,trnskmPM_WLK_COM_WLK.omx,waux,PM -WLK_COM_WLK_IWAIT,trnskmPM_WLK_COM_WLK.omx,iwait,PM -WLK_COM_WLK_XWAIT,trnskmPM_WLK_COM_WLK.omx,xwait,PM -WLK_COM_WLK_BOARDS,trnskmPM_WLK_COM_WLK.omx,boards,PM -WLK_EXP_DRV_WAIT,trnskmPM_WLK_EXP_DRV.omx,wait,PM -WLK_EXP_DRV_TOTIVT,trnskmPM_WLK_EXP_DRV.omx,ivt,PM -WLK_EXP_DRV_KEYIVT,trnskmPM_WLK_EXP_DRV.omx,ivtEXP,PM -WLK_EXP_DRV_FAR,trnskmPM_WLK_EXP_DRV.omx,fare,PM -WLK_EXP_DRV_DTIM,trnskmPM_WLK_EXP_DRV.omx,dtime,PM -WLK_EXP_DRV_WAUX,trnskmPM_WLK_EXP_DRV.omx,waux,PM -WLK_EXP_DRV_IWAIT,trnskmPM_WLK_EXP_DRV.omx,iwait,PM -WLK_EXP_DRV_XWAIT,trnskmPM_WLK_EXP_DRV.omx,xwait,PM -WLK_EXP_DRV_BOARDS,trnskmPM_WLK_EXP_DRV.omx,boards,PM -WLK_EXP_DRV_DDIST,trnskmPM_WLK_EXP_DRV.omx,ddist,PM -WLK_EXP_WLK_WAIT,trnskmPM_WLK_EXP_WLK.omx,wait,PM -WLK_EXP_WLK_TOTIVT,trnskmPM_WLK_EXP_WLK.omx,ivt,PM -WLK_EXP_WLK_KEYIVT,trnskmPM_WLK_EXP_WLK.omx,ivtEXP,PM -WLK_EXP_WLK_FAR,trnskmPM_WLK_EXP_WLK.omx,fare,PM -WLK_EXP_WLK_WAUX,trnskmPM_WLK_EXP_WLK.omx,waux,PM -WLK_EXP_WLK_IWAIT,trnskmPM_WLK_EXP_WLK.omx,iwait,PM -WLK_EXP_WLK_XWAIT,trnskmPM_WLK_EXP_WLK.omx,xwait,PM -WLK_EXP_WLK_BOARDS,trnskmPM_WLK_EXP_WLK.omx,boards,PM -WLK_HVY_DRV_WAIT,trnskmPM_WLK_HVY_DRV.omx,wait,PM -WLK_HVY_DRV_TOTIVT,trnskmPM_WLK_HVY_DRV.omx,ivt,PM -WLK_HVY_DRV_KEYIVT,trnskmPM_WLK_HVY_DRV.omx,ivtHVY,PM -WLK_HVY_DRV_FAR,trnskmPM_WLK_HVY_DRV.omx,fare,PM -WLK_HVY_DRV_DTIM,trnskmPM_WLK_HVY_DRV.omx,dtime,PM -WLK_HVY_DRV_DDIST,trnskmPM_WLK_HVY_DRV.omx,ddist,PM -WLK_HVY_DRV_WAUX,trnskmPM_WLK_HVY_DRV.omx,waux,PM -WLK_HVY_DRV_IWAIT,trnskmPM_WLK_HVY_DRV.omx,iwait,PM -WLK_HVY_DRV_XWAIT,trnskmPM_WLK_HVY_DRV.omx,xwait,PM -WLK_HVY_DRV_BOARDS,trnskmPM_WLK_HVY_DRV.omx,boards,PM -WLK_HVY_WLK_WAIT,trnskmPM_WLK_HVY_WLK.omx,wait,PM -WLK_HVY_WLK_TOTIVT,trnskmPM_WLK_HVY_WLK.omx,ivt,PM -WLK_HVY_WLK_KEYIVT,trnskmPM_WLK_HVY_WLK.omx,ivtHVY,PM -WLK_HVY_WLK_FAR,trnskmPM_WLK_HVY_WLK.omx,fare,PM -WLK_HVY_WLK_WAUX,trnskmPM_WLK_HVY_WLK.omx,waux,PM -WLK_HVY_WLK_IWAIT,trnskmPM_WLK_HVY_WLK.omx,iwait,PM -WLK_HVY_WLK_XWAIT,trnskmPM_WLK_HVY_WLK.omx,xwait,PM -WLK_HVY_WLK_BOARDS,trnskmPM_WLK_HVY_WLK.omx,boards,PM -WLK_LOC_DRV_WAIT,trnskmPM_WLK_LOC_DRV.omx,wait,PM -WLK_LOC_DRV_TOTIVT,trnskmPM_WLK_LOC_DRV.omx,ivt,PM -WLK_LOC_DRV_FAR,trnskmPM_WLK_LOC_DRV.omx,fare,PM -WLK_LOC_DRV_DTIM,trnskmPM_WLK_LOC_DRV.omx,dtime,PM -WLK_LOC_DRV_DDIST,trnskmPM_WLK_LOC_DRV.omx,ddist,PM -WLK_LOC_DRV_WAUX,trnskmPM_WLK_LOC_DRV.omx,waux,PM -WLK_LOC_DRV_IWAIT,trnskmPM_WLK_LOC_DRV.omx,iwait,PM -WLK_LOC_DRV_XWAIT,trnskmPM_WLK_LOC_DRV.omx,xwait,PM -WLK_LOC_DRV_BOARDS,trnskmPM_WLK_LOC_DRV.omx,boards,PM -WLK_LOC_WLK_WAIT,trnskmPM_WLK_LOC_WLK.omx,wait,PM -WLK_LOC_WLK_TOTIVT,trnskmPM_WLK_LOC_WLK.omx,ivt,PM -WLK_LOC_WLK_FAR,trnskmPM_WLK_LOC_WLK.omx,fare,PM -WLK_LOC_WLK_WAUX,trnskmPM_WLK_LOC_WLK.omx,waux,PM -WLK_LOC_WLK_IWAIT,trnskmPM_WLK_LOC_WLK.omx,iwait,PM -WLK_LOC_WLK_XWAIT,trnskmPM_WLK_LOC_WLK.omx,xwait,PM -WLK_LOC_WLK_BOARDS,trnskmPM_WLK_LOC_WLK.omx,boards,PM -WLK_LRF_DRV_WAIT,trnskmPM_WLK_LRF_DRV.omx,wait,PM -WLK_LRF_DRV_TOTIVT,trnskmPM_WLK_LRF_DRV.omx,ivt,PM -WLK_LRF_DRV_KEYIVT,trnskmPM_WLK_LRF_DRV.omx,ivtLRF,PM -WLK_LRF_DRV_FERRYIVT,trnskmPM_WLK_LRF_DRV.omx,ivtFerry,PM -WLK_LRF_DRV_FAR,trnskmPM_WLK_LRF_DRV.omx,fare,PM -WLK_LRF_DRV_DTIM,trnskmPM_WLK_LRF_DRV.omx,dtime,PM -WLK_LRF_DRV_DDIST,trnskmPM_WLK_LRF_DRV.omx,ddist,PM -WLK_LRF_DRV_WAUX,trnskmPM_WLK_LRF_DRV.omx,waux,PM -WLK_LRF_DRV_IWAIT,trnskmPM_WLK_LRF_DRV.omx,iwait,PM -WLK_LRF_DRV_XWAIT,trnskmPM_WLK_LRF_DRV.omx,xwait,PM -WLK_LRF_DRV_BOARDS,trnskmPM_WLK_LRF_DRV.omx,boards,PM -WLK_LRF_WLK_WAIT,trnskmPM_WLK_LRF_WLK.omx,wait,PM -WLK_LRF_WLK_TOTIVT,trnskmPM_WLK_LRF_WLK.omx,ivt,PM -WLK_LRF_WLK_KEYIVT,trnskmPM_WLK_LRF_WLK.omx,ivtLRF,PM -WLK_LRF_WLK_FERRYIVT,trnskmPM_WLK_LRF_WLK.omx,ivtFerry,PM -WLK_LRF_WLK_FAR,trnskmPM_WLK_LRF_WLK.omx,fare,PM -WLK_LRF_WLK_WAUX,trnskmPM_WLK_LRF_WLK.omx,waux,PM -WLK_LRF_WLK_IWAIT,trnskmPM_WLK_LRF_WLK.omx,iwait,PM -WLK_LRF_WLK_XWAIT,trnskmPM_WLK_LRF_WLK.omx,xwait,PM -WLK_LRF_WLK_BOARDS,trnskmPM_WLK_LRF_WLK.omx,boards,PM -WLK_TRN_WLK_IVT,trnskmAM_wlk_trn_wlk.omx,ivt,AM -WLK_TRN_WLK_IWAIT,trnskmAM_wlk_trn_wlk.omx,iwait,AM -WLK_TRN_WLK_XWAIT,trnskmAM_wlk_trn_wlk.omx,xwait,AM -WLK_TRN_WLK_WACC,trnskmAM_wlk_trn_wlk.omx,wacc,AM -WLK_TRN_WLK_WAUX,trnskmAM_wlk_trn_wlk.omx,waux,AM -WLK_TRN_WLK_WEGR,trnskmAM_wlk_trn_wlk.omx,wegr,AM -WLK_TRN_WLK_IVT,trnskmMD_wlk_trn_wlk.omx,ivt,MD -WLK_TRN_WLK_IWAIT,trnskmMD_wlk_trn_wlk.omx,iwait,MD -WLK_TRN_WLK_XWAIT,trnskmMD_wlk_trn_wlk.omx,xwait,MD -WLK_TRN_WLK_WACC,trnskmMD_wlk_trn_wlk.omx,wacc,MD -WLK_TRN_WLK_WAUX,trnskmMD_wlk_trn_wlk.omx,waux,MD -WLK_TRN_WLK_WEGR,trnskmMD_wlk_trn_wlk.omx,wegr,MD -WLK_TRN_WLK_IVT,trnskmPM_wlk_trn_wlk.omx,ivt,PM -WLK_TRN_WLK_IWAIT,trnskmPM_wlk_trn_wlk.omx,iwait,PM -WLK_TRN_WLK_XWAIT,trnskmPM_wlk_trn_wlk.omx,xwait,PM -WLK_TRN_WLK_WACC,trnskmPM_wlk_trn_wlk.omx,wacc,PM -WLK_TRN_WLK_WAUX,trnskmPM_wlk_trn_wlk.omx,waux,PM -WLK_TRN_WLK_WEGR,trnskmPM_wlk_trn_wlk.omx,wegr,PM +SOV_TIME,hwyskmAM.tpp.omx,TIMEDA,AM +SOV_DIST,hwyskmAM.tpp.omx,DISTDA,AM +SOV_BTOLL,hwyskmAM.tpp.omx,BTOLLDA,AM +HOV2_TIME,hwyskmAM.tpp.omx,TIMES2,AM +HOV2_DIST,hwyskmAM.tpp.omx,DISTS2,AM +HOV2_BTOLL,hwyskmAM.tpp.omx,BTOLLS2,AM +HOV3_TIME,hwyskmAM.tpp.omx,TIMES3,AM +HOV3_DIST,hwyskmAM.tpp.omx,DISTS3,AM +HOV3_BTOLL,hwyskmAM.tpp.omx,BTOLLS3,AM +SOVTOLL_TIME,hwyskmAM.tpp.omx,TOLLTIMEDA,AM +SOVTOLL_DIST,hwyskmAM.tpp.omx,TOLLDISTDA,AM +SOVTOLL_BTOLL,hwyskmAM.tpp.omx,TOLLBTOLLDA,AM +SOVTOLL_VTOLL,hwyskmAM.tpp.omx,TOLLVTOLLDA,AM +HOV2TOLL_TIME,hwyskmAM.tpp.omx,TOLLTIMES2,AM +HOV2TOLL_DIST,hwyskmAM.tpp.omx,TOLLDISTS2,AM +HOV2TOLL_BTOLL,hwyskmAM.tpp.omx,TOLLBTOLLS2,AM +HOV2TOLL_VTOLL,hwyskmAM.tpp.omx,TOLLVTOLLS2,AM +HOV3TOLL_TIME,hwyskmAM.tpp.omx,TOLLTIMES3,AM +HOV3TOLL_DIST,hwyskmAM.tpp.omx,TOLLDISTS3,AM +HOV3TOLL_BTOLL,hwyskmAM.tpp.omx,TOLLBTOLLS3,AM +HOV3TOLL_VTOLL,hwyskmAM.tpp.omx,TOLLVTOLLS3,AM +SOV_TIME,hwyskmEA.tpp.omx,TIMEDA,EA +SOV_DIST,hwyskmEA.tpp.omx,DISTDA,EA +SOV_BTOLL,hwyskmEA.tpp.omx,BTOLLDA,EA +HOV2_TIME,hwyskmEA.tpp.omx,TIMES2,EA +HOV2_DIST,hwyskmEA.tpp.omx,DISTS2,EA +HOV2_BTOLL,hwyskmEA.tpp.omx,BTOLLS2,EA +HOV3_TIME,hwyskmEA.tpp.omx,TIMES3,EA +HOV3_DIST,hwyskmEA.tpp.omx,DISTS3,EA +HOV3_BTOLL,hwyskmEA.tpp.omx,BTOLLS3,EA +SOVTOLL_TIME,hwyskmEA.tpp.omx,TOLLTIMEDA,EA +SOVTOLL_DIST,hwyskmEA.tpp.omx,TOLLDISTDA,EA +SOVTOLL_BTOLL,hwyskmEA.tpp.omx,TOLLBTOLLDA,EA +SOVTOLL_VTOLL,hwyskmEA.tpp.omx,TOLLVTOLLDA,EA +HOV2TOLL_TIME,hwyskmEA.tpp.omx,TOLLTIMES2,EA +HOV2TOLL_DIST,hwyskmEA.tpp.omx,TOLLDISTS2,EA +HOV2TOLL_BTOLL,hwyskmEA.tpp.omx,TOLLBTOLLS2,EA +HOV2TOLL_VTOLL,hwyskmEA.tpp.omx,TOLLVTOLLS2,EA +HOV3TOLL_TIME,hwyskmEA.tpp.omx,TOLLTIMES3,EA +HOV3TOLL_DIST,hwyskmEA.tpp.omx,TOLLDISTS3,EA +HOV3TOLL_BTOLL,hwyskmEA.tpp.omx,TOLLBTOLLS3,EA +HOV3TOLL_VTOLL,hwyskmEA.tpp.omx,TOLLVTOLLS3,EA +SOV_TIME,hwyskmEV.tpp.omx,TIMEDA,EV +SOV_DIST,hwyskmEV.tpp.omx,DISTDA,EV +SOV_BTOLL,hwyskmEV.tpp.omx,BTOLLDA,EV +HOV2_TIME,hwyskmEV.tpp.omx,TIMES2,EV +HOV2_DIST,hwyskmEV.tpp.omx,DISTS2,EV +HOV2_BTOLL,hwyskmEV.tpp.omx,BTOLLS2,EV +HOV3_TIME,hwyskmEV.tpp.omx,TIMES3,EV +HOV3_DIST,hwyskmEV.tpp.omx,DISTS3,EV +HOV3_BTOLL,hwyskmEV.tpp.omx,BTOLLS3,EV +SOVTOLL_TIME,hwyskmEV.tpp.omx,TOLLTIMEDA,EV +SOVTOLL_DIST,hwyskmEV.tpp.omx,TOLLDISTDA,EV +SOVTOLL_BTOLL,hwyskmEV.tpp.omx,TOLLBTOLLDA,EV +SOVTOLL_VTOLL,hwyskmEV.tpp.omx,TOLLVTOLLDA,EV +HOV2TOLL_TIME,hwyskmEV.tpp.omx,TOLLTIMES2,EV +HOV2TOLL_DIST,hwyskmEV.tpp.omx,TOLLDISTS2,EV +HOV2TOLL_BTOLL,hwyskmEV.tpp.omx,TOLLBTOLLS2,EV +HOV2TOLL_VTOLL,hwyskmEV.tpp.omx,TOLLVTOLLS2,EV +HOV3TOLL_TIME,hwyskmEV.tpp.omx,TOLLTIMES3,EV +HOV3TOLL_DIST,hwyskmEV.tpp.omx,TOLLDISTS3,EV +HOV3TOLL_BTOLL,hwyskmEV.tpp.omx,TOLLBTOLLS3,EV +HOV3TOLL_VTOLL,hwyskmEV.tpp.omx,TOLLVTOLLS3,EV +SOV_TIME,hwyskmMD.tpp.omx,TIMEDA,MD +SOV_DIST,hwyskmMD.tpp.omx,DISTDA,MD +SOV_BTOLL,hwyskmMD.tpp.omx,BTOLLDA,MD +HOV2_TIME,hwyskmMD.tpp.omx,TIMES2,MD +HOV2_DIST,hwyskmMD.tpp.omx,DISTS2,MD +HOV2_BTOLL,hwyskmMD.tpp.omx,BTOLLS2,MD +HOV3_TIME,hwyskmMD.tpp.omx,TIMES3,MD +HOV3_DIST,hwyskmMD.tpp.omx,DISTS3,MD +HOV3_BTOLL,hwyskmMD.tpp.omx,BTOLLS3,MD +SOVTOLL_TIME,hwyskmMD.tpp.omx,TOLLTIMEDA,MD +SOVTOLL_DIST,hwyskmMD.tpp.omx,TOLLDISTDA,MD +SOVTOLL_BTOLL,hwyskmMD.tpp.omx,TOLLBTOLLDA,MD +SOVTOLL_VTOLL,hwyskmMD.tpp.omx,TOLLVTOLLDA,MD +HOV2TOLL_TIME,hwyskmMD.tpp.omx,TOLLTIMES2,MD +HOV2TOLL_DIST,hwyskmMD.tpp.omx,TOLLDISTS2,MD +HOV2TOLL_BTOLL,hwyskmMD.tpp.omx,TOLLBTOLLS2,MD +HOV2TOLL_VTOLL,hwyskmMD.tpp.omx,TOLLVTOLLS2,MD +HOV3TOLL_TIME,hwyskmMD.tpp.omx,TOLLTIMES3,MD +HOV3TOLL_DIST,hwyskmMD.tpp.omx,TOLLDISTS3,MD +HOV3TOLL_BTOLL,hwyskmMD.tpp.omx,TOLLBTOLLS3,MD +HOV3TOLL_VTOLL,hwyskmMD.tpp.omx,TOLLVTOLLS3,MD +SOV_TIME,hwyskmPM.tpp.omx,TIMEDA,PM +SOV_DIST,hwyskmPM.tpp.omx,DISTDA,PM +SOV_BTOLL,hwyskmPM.tpp.omx,BTOLLDA,PM +HOV2_TIME,hwyskmPM.tpp.omx,TIMES2,PM +HOV2_DIST,hwyskmPM.tpp.omx,DISTS2,PM +HOV2_BTOLL,hwyskmPM.tpp.omx,BTOLLS2,PM +HOV3_TIME,hwyskmPM.tpp.omx,TIMES3,PM +HOV3_DIST,hwyskmPM.tpp.omx,DISTS3,PM +HOV3_BTOLL,hwyskmPM.tpp.omx,BTOLLS3,PM +SOVTOLL_TIME,hwyskmPM.tpp.omx,TOLLTIMEDA,PM +SOVTOLL_DIST,hwyskmPM.tpp.omx,TOLLDISTDA,PM +SOVTOLL_BTOLL,hwyskmPM.tpp.omx,TOLLBTOLLDA,PM +SOVTOLL_VTOLL,hwyskmPM.tpp.omx,TOLLVTOLLDA,PM +HOV2TOLL_TIME,hwyskmPM.tpp.omx,TOLLTIMES2,PM +HOV2TOLL_DIST,hwyskmPM.tpp.omx,TOLLDISTS2,PM +HOV2TOLL_BTOLL,hwyskmPM.tpp.omx,TOLLBTOLLS2,PM +HOV2TOLL_VTOLL,hwyskmPM.tpp.omx,TOLLVTOLLS2,PM +HOV3TOLL_TIME,hwyskmPM.tpp.omx,TOLLTIMES3,PM +HOV3TOLL_DIST,hwyskmPM.tpp.omx,TOLLDISTS3,PM +HOV3TOLL_BTOLL,hwyskmPM.tpp.omx,TOLLBTOLLS3,PM +HOV3TOLL_VTOLL,hwyskmPM.tpp.omx,TOLLVTOLLS3,PM +DIST,nonmotskm.tpp.omx,DIST, +DISTWALK,nonmotskm.tpp.omx,DISTWALK, +DISTBIKE,nonmotskm.tpp.omx,DISTBIKE, +DRV_COM_WLK_WAIT,trnskmAM_DRV_COM_WLK.tpp.omx,wait,AM +DRV_COM_WLK_TOTIVT,trnskmAM_DRV_COM_WLK.tpp.omx,ivt,AM +DRV_COM_WLK_KEYIVT,trnskmAM_DRV_COM_WLK.tpp.omx,ivtCOM,AM +DRV_COM_WLK_FAR,trnskmAM_DRV_COM_WLK.tpp.omx,fare,AM +DRV_COM_WLK_DTIM,trnskmAM_DRV_COM_WLK.tpp.omx,dtime,AM +DRV_COM_WLK_DDIST,trnskmAM_DRV_COM_WLK.tpp.omx,ddist,AM +DRV_COM_WLK_WAUX,trnskmAM_DRV_COM_WLK.tpp.omx,waux,AM +DRV_COM_WLK_IWAIT,trnskmAM_DRV_COM_WLK.tpp.omx,iwait,AM +DRV_COM_WLK_XWAIT,trnskmAM_DRV_COM_WLK.tpp.omx,xwait,AM +DRV_COM_WLK_BOARDS,trnskmAM_DRV_COM_WLK.tpp.omx,boards,AM +DRV_EXP_WLK_WAIT,trnskmAM_DRV_EXP_WLK.tpp.omx,wait,AM +DRV_EXP_WLK_TOTIVT,trnskmAM_DRV_EXP_WLK.tpp.omx,ivt,AM +DRV_EXP_WLK_KEYIVT,trnskmAM_DRV_EXP_WLK.tpp.omx,ivtEXP,AM +DRV_EXP_WLK_FAR,trnskmAM_DRV_EXP_WLK.tpp.omx,fare,AM +DRV_EXP_WLK_DTIM,trnskmAM_DRV_EXP_WLK.tpp.omx,dtime,AM +DRV_EXP_WLK_WAUX,trnskmAM_DRV_EXP_WLK.tpp.omx,waux,AM +DRV_EXP_WLK_IWAIT,trnskmAM_DRV_EXP_WLK.tpp.omx,iwait,AM +DRV_EXP_WLK_XWAIT,trnskmAM_DRV_EXP_WLK.tpp.omx,xwait,AM +DRV_EXP_WLK_BOARDS,trnskmAM_DRV_EXP_WLK.tpp.omx,boards,AM +DRV_EXP_WLK_DDIST,trnskmAM_DRV_EXP_WLK.tpp.omx,ddist,AM +DRV_HVY_WLK_WAIT,trnskmAM_DRV_HVY_WLK.tpp.omx,wait,AM +DRV_HVY_WLK_TOTIVT,trnskmAM_DRV_HVY_WLK.tpp.omx,ivt,AM +DRV_HVY_WLK_KEYIVT,trnskmAM_DRV_HVY_WLK.tpp.omx,ivtHVY,AM +DRV_HVY_WLK_FAR,trnskmAM_DRV_HVY_WLK.tpp.omx,fare,AM +DRV_HVY_WLK_DTIM,trnskmAM_DRV_HVY_WLK.tpp.omx,dtime,AM +DRV_HVY_WLK_DDIST,trnskmAM_DRV_HVY_WLK.tpp.omx,ddist,AM +DRV_HVY_WLK_WAUX,trnskmAM_DRV_HVY_WLK.tpp.omx,waux,AM +DRV_HVY_WLK_IWAIT,trnskmAM_DRV_HVY_WLK.tpp.omx,iwait,AM +DRV_HVY_WLK_XWAIT,trnskmAM_DRV_HVY_WLK.tpp.omx,xwait,AM +DRV_HVY_WLK_BOARDS,trnskmAM_DRV_HVY_WLK.tpp.omx,boards,AM +DRV_LOC_WLK_WAIT,trnskmAM_DRV_LOC_WLK.tpp.omx,wait,AM +DRV_LOC_WLK_TOTIVT,trnskmAM_DRV_LOC_WLK.tpp.omx,ivt,AM +DRV_LOC_WLK_FAR,trnskmAM_DRV_LOC_WLK.tpp.omx,fare,AM +DRV_LOC_WLK_DTIM,trnskmAM_DRV_LOC_WLK.tpp.omx,dtime,AM +DRV_LOC_WLK_DDIST,trnskmAM_DRV_LOC_WLK.tpp.omx,ddist,AM +DRV_LOC_WLK_WAUX,trnskmAM_DRV_LOC_WLK.tpp.omx,waux,AM +DRV_LOC_WLK_IWAIT,trnskmAM_DRV_LOC_WLK.tpp.omx,iwait,AM +DRV_LOC_WLK_XWAIT,trnskmAM_DRV_LOC_WLK.tpp.omx,xwait,AM +DRV_LOC_WLK_BOARDS,trnskmAM_DRV_LOC_WLK.tpp.omx,boards,AM +DRV_LRF_WLK_WAIT,trnskmAM_DRV_LRF_WLK.tpp.omx,wait,AM +DRV_LRF_WLK_TOTIVT,trnskmAM_DRV_LRF_WLK.tpp.omx,ivt,AM +DRV_LRF_WLK_KEYIVT,trnskmAM_DRV_LRF_WLK.tpp.omx,ivtLRF,AM +DRV_LRF_WLK_FERRYIVT,trnskmAM_DRV_LRF_WLK.tpp.omx,ivtFerry,AM +DRV_LRF_WLK_FAR,trnskmAM_DRV_LRF_WLK.tpp.omx,fare,AM +DRV_LRF_WLK_DTIM,trnskmAM_DRV_LRF_WLK.tpp.omx,dtime,AM +DRV_LRF_WLK_DDIST,trnskmAM_DRV_LRF_WLK.tpp.omx,ddist,AM +DRV_LRF_WLK_WAUX,trnskmAM_DRV_LRF_WLK.tpp.omx,waux,AM +DRV_LRF_WLK_IWAIT,trnskmAM_DRV_LRF_WLK.tpp.omx,iwait,AM +DRV_LRF_WLK_XWAIT,trnskmAM_DRV_LRF_WLK.tpp.omx,xwait,AM +DRV_LRF_WLK_BOARDS,trnskmAM_DRV_LRF_WLK.tpp.omx,boards,AM +WLK_COM_DRV_WAIT,trnskmAM_WLK_COM_DRV.tpp.omx,wait,AM +WLK_COM_DRV_TOTIVT,trnskmAM_WLK_COM_DRV.tpp.omx,ivt,AM +WLK_COM_DRV_KEYIVT,trnskmAM_WLK_COM_DRV.tpp.omx,ivtCOM,AM +WLK_COM_DRV_FAR,trnskmAM_WLK_COM_DRV.tpp.omx,fare,AM +WLK_COM_DRV_DTIM,trnskmAM_WLK_COM_DRV.tpp.omx,dtime,AM +WLK_COM_DRV_DDIST,trnskmAM_WLK_COM_DRV.tpp.omx,ddist,AM +WLK_COM_DRV_WAUX,trnskmAM_WLK_COM_DRV.tpp.omx,waux,AM +WLK_COM_DRV_IWAIT,trnskmAM_WLK_COM_DRV.tpp.omx,iwait,AM +WLK_COM_DRV_XWAIT,trnskmAM_WLK_COM_DRV.tpp.omx,xwait,AM +WLK_COM_DRV_BOARDS,trnskmAM_WLK_COM_DRV.tpp.omx,boards,AM +WLK_COM_WLK_WAIT,trnskmAM_WLK_COM_WLK.tpp.omx,wait,AM +WLK_COM_WLK_TOTIVT,trnskmAM_WLK_COM_WLK.tpp.omx,ivt,AM +WLK_COM_WLK_KEYIVT,trnskmAM_WLK_COM_WLK.tpp.omx,ivtCOM,AM +WLK_COM_WLK_FAR,trnskmAM_WLK_COM_WLK.tpp.omx,fare,AM +WLK_COM_WLK_WAUX,trnskmAM_WLK_COM_WLK.tpp.omx,waux,AM +WLK_COM_WLK_IWAIT,trnskmAM_WLK_COM_WLK.tpp.omx,iwait,AM +WLK_COM_WLK_XWAIT,trnskmAM_WLK_COM_WLK.tpp.omx,xwait,AM +WLK_COM_WLK_BOARDS,trnskmAM_WLK_COM_WLK.tpp.omx,boards,AM +WLK_EXP_DRV_WAIT,trnskmAM_WLK_EXP_DRV.tpp.omx,wait,AM +WLK_EXP_DRV_TOTIVT,trnskmAM_WLK_EXP_DRV.tpp.omx,ivt,AM +WLK_EXP_DRV_KEYIVT,trnskmAM_WLK_EXP_DRV.tpp.omx,ivtEXP,AM +WLK_EXP_DRV_FAR,trnskmAM_WLK_EXP_DRV.tpp.omx,fare,AM +WLK_EXP_DRV_DTIM,trnskmAM_WLK_EXP_DRV.tpp.omx,dtime,AM +WLK_EXP_DRV_WAUX,trnskmAM_WLK_EXP_DRV.tpp.omx,waux,AM +WLK_EXP_DRV_IWAIT,trnskmAM_WLK_EXP_DRV.tpp.omx,iwait,AM +WLK_EXP_DRV_XWAIT,trnskmAM_WLK_EXP_DRV.tpp.omx,xwait,AM +WLK_EXP_DRV_BOARDS,trnskmAM_WLK_EXP_DRV.tpp.omx,boards,AM +WLK_EXP_DRV_DDIST,trnskmAM_WLK_EXP_DRV.tpp.omx,ddist,AM +WLK_EXP_WLK_WAIT,trnskmAM_WLK_EXP_WLK.tpp.omx,wait,AM +WLK_EXP_WLK_TOTIVT,trnskmAM_WLK_EXP_WLK.tpp.omx,ivt,AM +WLK_EXP_WLK_KEYIVT,trnskmAM_WLK_EXP_WLK.tpp.omx,ivtEXP,AM +WLK_EXP_WLK_FAR,trnskmAM_WLK_EXP_WLK.tpp.omx,fare,AM +WLK_EXP_WLK_WAUX,trnskmAM_WLK_EXP_WLK.tpp.omx,waux,AM +WLK_EXP_WLK_IWAIT,trnskmAM_WLK_EXP_WLK.tpp.omx,iwait,AM +WLK_EXP_WLK_XWAIT,trnskmAM_WLK_EXP_WLK.tpp.omx,xwait,AM +WLK_EXP_WLK_BOARDS,trnskmAM_WLK_EXP_WLK.tpp.omx,boards,AM +WLK_HVY_DRV_WAIT,trnskmAM_WLK_HVY_DRV.tpp.omx,wait,AM +WLK_HVY_DRV_TOTIVT,trnskmAM_WLK_HVY_DRV.tpp.omx,ivt,AM +WLK_HVY_DRV_KEYIVT,trnskmAM_WLK_HVY_DRV.tpp.omx,ivtHVY,AM +WLK_HVY_DRV_FAR,trnskmAM_WLK_HVY_DRV.tpp.omx,fare,AM +WLK_HVY_DRV_DTIM,trnskmAM_WLK_HVY_DRV.tpp.omx,dtime,AM +WLK_HVY_DRV_DDIST,trnskmAM_WLK_HVY_DRV.tpp.omx,ddist,AM +WLK_HVY_DRV_WAUX,trnskmAM_WLK_HVY_DRV.tpp.omx,waux,AM +WLK_HVY_DRV_IWAIT,trnskmAM_WLK_HVY_DRV.tpp.omx,iwait,AM +WLK_HVY_DRV_XWAIT,trnskmAM_WLK_HVY_DRV.tpp.omx,xwait,AM +WLK_HVY_DRV_BOARDS,trnskmAM_WLK_HVY_DRV.tpp.omx,boards,AM +WLK_HVY_WLK_WAIT,trnskmAM_WLK_HVY_WLK.tpp.omx,wait,AM +WLK_HVY_WLK_TOTIVT,trnskmAM_WLK_HVY_WLK.tpp.omx,ivt,AM +WLK_HVY_WLK_KEYIVT,trnskmAM_WLK_HVY_WLK.tpp.omx,ivtHVY,AM +WLK_HVY_WLK_FAR,trnskmAM_WLK_HVY_WLK.tpp.omx,fare,AM +WLK_HVY_WLK_WAUX,trnskmAM_WLK_HVY_WLK.tpp.omx,waux,AM +WLK_HVY_WLK_IWAIT,trnskmAM_WLK_HVY_WLK.tpp.omx,iwait,AM +WLK_HVY_WLK_XWAIT,trnskmAM_WLK_HVY_WLK.tpp.omx,xwait,AM +WLK_HVY_WLK_BOARDS,trnskmAM_WLK_HVY_WLK.tpp.omx,boards,AM +WLK_LOC_DRV_WAIT,trnskmAM_WLK_LOC_DRV.tpp.omx,wait,AM +WLK_LOC_DRV_TOTIVT,trnskmAM_WLK_LOC_DRV.tpp.omx,ivt,AM +WLK_LOC_DRV_FAR,trnskmAM_WLK_LOC_DRV.tpp.omx,fare,AM +WLK_LOC_DRV_DTIM,trnskmAM_WLK_LOC_DRV.tpp.omx,dtime,AM +WLK_LOC_DRV_DDIST,trnskmAM_WLK_LOC_DRV.tpp.omx,ddist,AM +WLK_LOC_DRV_WAUX,trnskmAM_WLK_LOC_DRV.tpp.omx,waux,AM +WLK_LOC_DRV_IWAIT,trnskmAM_WLK_LOC_DRV.tpp.omx,iwait,AM +WLK_LOC_DRV_XWAIT,trnskmAM_WLK_LOC_DRV.tpp.omx,xwait,AM +WLK_LOC_DRV_BOARDS,trnskmAM_WLK_LOC_DRV.tpp.omx,boards,AM +WLK_LOC_WLK_WAIT,trnskmAM_WLK_LOC_WLK.tpp.omx,wait,AM +WLK_LOC_WLK_TOTIVT,trnskmAM_WLK_LOC_WLK.tpp.omx,ivt,AM +WLK_LOC_WLK_FAR,trnskmAM_WLK_LOC_WLK.tpp.omx,fare,AM +WLK_LOC_WLK_WAUX,trnskmAM_WLK_LOC_WLK.tpp.omx,waux,AM +WLK_LOC_WLK_IWAIT,trnskmAM_WLK_LOC_WLK.tpp.omx,iwait,AM +WLK_LOC_WLK_XWAIT,trnskmAM_WLK_LOC_WLK.tpp.omx,xwait,AM +WLK_LOC_WLK_BOARDS,trnskmAM_WLK_LOC_WLK.tpp.omx,boards,AM +WLK_LRF_DRV_WAIT,trnskmAM_WLK_LRF_DRV.tpp.omx,wait,AM +WLK_LRF_DRV_TOTIVT,trnskmAM_WLK_LRF_DRV.tpp.omx,ivt,AM +WLK_LRF_DRV_KEYIVT,trnskmAM_WLK_LRF_DRV.tpp.omx,ivtLRF,AM +WLK_LRF_DRV_FERRYIVT,trnskmAM_WLK_LRF_DRV.tpp.omx,ivtFerry,AM +WLK_LRF_DRV_FAR,trnskmAM_WLK_LRF_DRV.tpp.omx,fare,AM +WLK_LRF_DRV_DTIM,trnskmAM_WLK_LRF_DRV.tpp.omx,dtime,AM +WLK_LRF_DRV_DDIST,trnskmAM_WLK_LRF_DRV.tpp.omx,ddist,AM +WLK_LRF_DRV_WAUX,trnskmAM_WLK_LRF_DRV.tpp.omx,waux,AM +WLK_LRF_DRV_IWAIT,trnskmAM_WLK_LRF_DRV.tpp.omx,iwait,AM +WLK_LRF_DRV_XWAIT,trnskmAM_WLK_LRF_DRV.tpp.omx,xwait,AM +WLK_LRF_DRV_BOARDS,trnskmAM_WLK_LRF_DRV.tpp.omx,boards,AM +WLK_LRF_WLK_WAIT,trnskmAM_WLK_LRF_WLK.tpp.omx,wait,AM +WLK_LRF_WLK_TOTIVT,trnskmAM_WLK_LRF_WLK.tpp.omx,ivt,AM +WLK_LRF_WLK_KEYIVT,trnskmAM_WLK_LRF_WLK.tpp.omx,ivtLRF,AM +WLK_LRF_WLK_FERRYIVT,trnskmAM_WLK_LRF_WLK.tpp.omx,ivtFerry,AM +WLK_LRF_WLK_FAR,trnskmAM_WLK_LRF_WLK.tpp.omx,fare,AM +WLK_LRF_WLK_WAUX,trnskmAM_WLK_LRF_WLK.tpp.omx,waux,AM +WLK_LRF_WLK_IWAIT,trnskmAM_WLK_LRF_WLK.tpp.omx,iwait,AM +WLK_LRF_WLK_XWAIT,trnskmAM_WLK_LRF_WLK.tpp.omx,xwait,AM +WLK_LRF_WLK_BOARDS,trnskmAM_WLK_LRF_WLK.tpp.omx,boards,AM +DRV_COM_WLK_WAIT,trnskmEA_DRV_COM_WLK.tpp.omx,wait,EA +DRV_COM_WLK_TOTIVT,trnskmEA_DRV_COM_WLK.tpp.omx,ivt,EA +DRV_COM_WLK_KEYIVT,trnskmEA_DRV_COM_WLK.tpp.omx,ivtCOM,EA +DRV_COM_WLK_FAR,trnskmEA_DRV_COM_WLK.tpp.omx,fare,EA +DRV_COM_WLK_DTIM,trnskmEA_DRV_COM_WLK.tpp.omx,dtime,EA +DRV_COM_WLK_DDIST,trnskmEA_DRV_COM_WLK.tpp.omx,ddist,EA +DRV_COM_WLK_WAUX,trnskmEA_DRV_COM_WLK.tpp.omx,waux,EA +DRV_COM_WLK_IWAIT,trnskmEA_DRV_COM_WLK.tpp.omx,iwait,EA +DRV_COM_WLK_XWAIT,trnskmEA_DRV_COM_WLK.tpp.omx,xwait,EA +DRV_COM_WLK_BOARDS,trnskmEA_DRV_COM_WLK.tpp.omx,boards,EA +DRV_EXP_WLK_WAIT,trnskmEA_DRV_EXP_WLK.tpp.omx,wait,EA +DRV_EXP_WLK_TOTIVT,trnskmEA_DRV_EXP_WLK.tpp.omx,ivt,EA +DRV_EXP_WLK_KEYIVT,trnskmEA_DRV_EXP_WLK.tpp.omx,ivtEXP,EA +DRV_EXP_WLK_FAR,trnskmEA_DRV_EXP_WLK.tpp.omx,fare,EA +DRV_EXP_WLK_DTIM,trnskmEA_DRV_EXP_WLK.tpp.omx,dtime,EA +DRV_EXP_WLK_WAUX,trnskmEA_DRV_EXP_WLK.tpp.omx,waux,EA +DRV_EXP_WLK_IWAIT,trnskmEA_DRV_EXP_WLK.tpp.omx,iwait,EA +DRV_EXP_WLK_XWAIT,trnskmEA_DRV_EXP_WLK.tpp.omx,xwait,EA +DRV_EXP_WLK_BOARDS,trnskmEA_DRV_EXP_WLK.tpp.omx,boards,EA +DRV_EXP_WLK_DDIST,trnskmEA_DRV_EXP_WLK.tpp.omx,ddist,EA +DRV_HVY_WLK_WAIT,trnskmEA_DRV_HVY_WLK.tpp.omx,wait,EA +DRV_HVY_WLK_TOTIVT,trnskmEA_DRV_HVY_WLK.tpp.omx,ivt,EA +DRV_HVY_WLK_KEYIVT,trnskmEA_DRV_HVY_WLK.tpp.omx,ivtHVY,EA +DRV_HVY_WLK_FAR,trnskmEA_DRV_HVY_WLK.tpp.omx,fare,EA +DRV_HVY_WLK_DTIM,trnskmEA_DRV_HVY_WLK.tpp.omx,dtime,EA +DRV_HVY_WLK_DDIST,trnskmEA_DRV_HVY_WLK.tpp.omx,ddist,EA +DRV_HVY_WLK_WAUX,trnskmEA_DRV_HVY_WLK.tpp.omx,waux,EA +DRV_HVY_WLK_IWAIT,trnskmEA_DRV_HVY_WLK.tpp.omx,iwait,EA +DRV_HVY_WLK_XWAIT,trnskmEA_DRV_HVY_WLK.tpp.omx,xwait,EA +DRV_HVY_WLK_BOARDS,trnskmEA_DRV_HVY_WLK.tpp.omx,boards,EA +DRV_LOC_WLK_WAIT,trnskmEA_DRV_LOC_WLK.tpp.omx,wait,EA +DRV_LOC_WLK_TOTIVT,trnskmEA_DRV_LOC_WLK.tpp.omx,ivt,EA +DRV_LOC_WLK_FAR,trnskmEA_DRV_LOC_WLK.tpp.omx,fare,EA +DRV_LOC_WLK_DTIM,trnskmEA_DRV_LOC_WLK.tpp.omx,dtime,EA +DRV_LOC_WLK_DDIST,trnskmEA_DRV_LOC_WLK.tpp.omx,ddist,EA +DRV_LOC_WLK_WAUX,trnskmEA_DRV_LOC_WLK.tpp.omx,waux,EA +DRV_LOC_WLK_IWAIT,trnskmEA_DRV_LOC_WLK.tpp.omx,iwait,EA +DRV_LOC_WLK_XWAIT,trnskmEA_DRV_LOC_WLK.tpp.omx,xwait,EA +DRV_LOC_WLK_BOARDS,trnskmEA_DRV_LOC_WLK.tpp.omx,boards,EA +DRV_LRF_WLK_WAIT,trnskmEA_DRV_LRF_WLK.tpp.omx,wait,EA +DRV_LRF_WLK_TOTIVT,trnskmEA_DRV_LRF_WLK.tpp.omx,ivt,EA +DRV_LRF_WLK_KEYIVT,trnskmEA_DRV_LRF_WLK.tpp.omx,ivtLRF,EA +DRV_LRF_WLK_FERRYIVT,trnskmEA_DRV_LRF_WLK.tpp.omx,ivtFerry,EA +DRV_LRF_WLK_FAR,trnskmEA_DRV_LRF_WLK.tpp.omx,fare,EA +DRV_LRF_WLK_DTIM,trnskmEA_DRV_LRF_WLK.tpp.omx,dtime,EA +DRV_LRF_WLK_DDIST,trnskmEA_DRV_LRF_WLK.tpp.omx,ddist,EA +DRV_LRF_WLK_WAUX,trnskmEA_DRV_LRF_WLK.tpp.omx,waux,EA +DRV_LRF_WLK_IWAIT,trnskmEA_DRV_LRF_WLK.tpp.omx,iwait,EA +DRV_LRF_WLK_XWAIT,trnskmEA_DRV_LRF_WLK.tpp.omx,xwait,EA +DRV_LRF_WLK_BOARDS,trnskmEA_DRV_LRF_WLK.tpp.omx,boards,EA +WLK_COM_DRV_WAIT,trnskmEA_WLK_COM_DRV.tpp.omx,wait,EA +WLK_COM_DRV_TOTIVT,trnskmEA_WLK_COM_DRV.tpp.omx,ivt,EA +WLK_COM_DRV_KEYIVT,trnskmEA_WLK_COM_DRV.tpp.omx,ivtCOM,EA +WLK_COM_DRV_FAR,trnskmEA_WLK_COM_DRV.tpp.omx,fare,EA +WLK_COM_DRV_DTIM,trnskmEA_WLK_COM_DRV.tpp.omx,dtime,EA +WLK_COM_DRV_DDIST,trnskmEA_WLK_COM_DRV.tpp.omx,ddist,EA +WLK_COM_DRV_WAUX,trnskmEA_WLK_COM_DRV.tpp.omx,waux,EA +WLK_COM_DRV_IWAIT,trnskmEA_WLK_COM_DRV.tpp.omx,iwait,EA +WLK_COM_DRV_XWAIT,trnskmEA_WLK_COM_DRV.tpp.omx,xwait,EA +WLK_COM_DRV_BOARDS,trnskmEA_WLK_COM_DRV.tpp.omx,boards,EA +WLK_COM_WLK_WAIT,trnskmEA_WLK_COM_WLK.tpp.omx,wait,EA +WLK_COM_WLK_TOTIVT,trnskmEA_WLK_COM_WLK.tpp.omx,ivt,EA +WLK_COM_WLK_KEYIVT,trnskmEA_WLK_COM_WLK.tpp.omx,ivtCOM,EA +WLK_COM_WLK_FAR,trnskmEA_WLK_COM_WLK.tpp.omx,fare,EA +WLK_COM_WLK_WAUX,trnskmEA_WLK_COM_WLK.tpp.omx,waux,EA +WLK_COM_WLK_IWAIT,trnskmEA_WLK_COM_WLK.tpp.omx,iwait,EA +WLK_COM_WLK_XWAIT,trnskmEA_WLK_COM_WLK.tpp.omx,xwait,EA +WLK_COM_WLK_BOARDS,trnskmEA_WLK_COM_WLK.tpp.omx,boards,EA +WLK_EXP_DRV_WAIT,trnskmEA_WLK_EXP_DRV.tpp.omx,wait,EA +WLK_EXP_DRV_TOTIVT,trnskmEA_WLK_EXP_DRV.tpp.omx,ivt,EA +WLK_EXP_DRV_KEYIVT,trnskmEA_WLK_EXP_DRV.tpp.omx,ivtEXP,EA +WLK_EXP_DRV_FAR,trnskmEA_WLK_EXP_DRV.tpp.omx,fare,EA +WLK_EXP_DRV_DTIM,trnskmEA_WLK_EXP_DRV.tpp.omx,dtime,EA +WLK_EXP_DRV_DDIST,trnskmEA_WLK_EXP_DRV.tpp.omx,ddist,EA +WLK_EXP_DRV_WAUX,trnskmEA_WLK_EXP_DRV.tpp.omx,waux,EA +WLK_EXP_DRV_IWAIT,trnskmEA_WLK_EXP_DRV.tpp.omx,iwait,EA +WLK_EXP_DRV_XWAIT,trnskmEA_WLK_EXP_DRV.tpp.omx,xwait,EA +WLK_EXP_DRV_BOARDS,trnskmEA_WLK_EXP_DRV.tpp.omx,boards,EA +WLK_EXP_WLK_WAIT,trnskmEA_WLK_EXP_WLK.tpp.omx,wait,EA +WLK_EXP_WLK_TOTIVT,trnskmEA_WLK_EXP_WLK.tpp.omx,ivt,EA +WLK_EXP_WLK_KEYIVT,trnskmEA_WLK_EXP_WLK.tpp.omx,ivtEXP,EA +WLK_EXP_WLK_FAR,trnskmEA_WLK_EXP_WLK.tpp.omx,fare,EA +WLK_EXP_WLK_WAUX,trnskmEA_WLK_EXP_WLK.tpp.omx,waux,EA +WLK_EXP_WLK_IWAIT,trnskmEA_WLK_EXP_WLK.tpp.omx,iwait,EA +WLK_EXP_WLK_XWAIT,trnskmEA_WLK_EXP_WLK.tpp.omx,xwait,EA +WLK_EXP_WLK_BOARDS,trnskmEA_WLK_EXP_WLK.tpp.omx,boards,EA +WLK_HVY_DRV_WAIT,trnskmEA_WLK_HVY_DRV.tpp.omx,wait,EA +WLK_HVY_DRV_TOTIVT,trnskmEA_WLK_HVY_DRV.tpp.omx,ivt,EA +WLK_HVY_DRV_KEYIVT,trnskmEA_WLK_HVY_DRV.tpp.omx,ivtHVY,EA +WLK_HVY_DRV_FAR,trnskmEA_WLK_HVY_DRV.tpp.omx,fare,EA +WLK_HVY_DRV_DTIM,trnskmEA_WLK_HVY_DRV.tpp.omx,dtime,EA +WLK_HVY_DRV_DDIST,trnskmEA_WLK_HVY_DRV.tpp.omx,ddist,EA +WLK_HVY_DRV_WAUX,trnskmEA_WLK_HVY_DRV.tpp.omx,waux,EA +WLK_HVY_DRV_IWAIT,trnskmEA_WLK_HVY_DRV.tpp.omx,iwait,EA +WLK_HVY_DRV_XWAIT,trnskmEA_WLK_HVY_DRV.tpp.omx,xwait,EA +WLK_HVY_DRV_BOARDS,trnskmEA_WLK_HVY_DRV.tpp.omx,boards,EA +WLK_HVY_WLK_WAIT,trnskmEA_WLK_HVY_WLK.tpp.omx,wait,EA +WLK_HVY_WLK_TOTIVT,trnskmEA_WLK_HVY_WLK.tpp.omx,ivt,EA +WLK_HVY_WLK_KEYIVT,trnskmEA_WLK_HVY_WLK.tpp.omx,ivtHVY,EA +WLK_HVY_WLK_FAR,trnskmEA_WLK_HVY_WLK.tpp.omx,fare,EA +WLK_HVY_WLK_WAUX,trnskmEA_WLK_HVY_WLK.tpp.omx,waux,EA +WLK_HVY_WLK_IWAIT,trnskmEA_WLK_HVY_WLK.tpp.omx,iwait,EA +WLK_HVY_WLK_XWAIT,trnskmEA_WLK_HVY_WLK.tpp.omx,xwait,EA +WLK_HVY_WLK_BOARDS,trnskmEA_WLK_HVY_WLK.tpp.omx,boards,EA +WLK_LOC_DRV_WAIT,trnskmEA_WLK_LOC_DRV.tpp.omx,wait,EA +WLK_LOC_DRV_TOTIVT,trnskmEA_WLK_LOC_DRV.tpp.omx,ivt,EA +WLK_LOC_DRV_FAR,trnskmEA_WLK_LOC_DRV.tpp.omx,fare,EA +WLK_LOC_DRV_DTIM,trnskmEA_WLK_LOC_DRV.tpp.omx,dtime,EA +WLK_LOC_DRV_DDIST,trnskmEA_WLK_LOC_DRV.tpp.omx,ddist,EA +WLK_LOC_DRV_WAUX,trnskmEA_WLK_LOC_DRV.tpp.omx,waux,EA +WLK_LOC_DRV_IWAIT,trnskmEA_WLK_LOC_DRV.tpp.omx,iwait,EA +WLK_LOC_DRV_XWAIT,trnskmEA_WLK_LOC_DRV.tpp.omx,xwait,EA +WLK_LOC_DRV_BOARDS,trnskmEA_WLK_LOC_DRV.tpp.omx,boards,EA +WLK_LOC_WLK_WAIT,trnskmEA_WLK_LOC_WLK.tpp.omx,wait,EA +WLK_LOC_WLK_TOTIVT,trnskmEA_WLK_LOC_WLK.tpp.omx,ivt,EA +WLK_LOC_WLK_FAR,trnskmEA_WLK_LOC_WLK.tpp.omx,fare,EA +WLK_LOC_WLK_WAUX,trnskmEA_WLK_LOC_WLK.tpp.omx,waux,EA +WLK_LOC_WLK_IWAIT,trnskmEA_WLK_LOC_WLK.tpp.omx,iwait,EA +WLK_LOC_WLK_XWAIT,trnskmEA_WLK_LOC_WLK.tpp.omx,xwait,EA +WLK_LOC_WLK_BOARDS,trnskmEA_WLK_LOC_WLK.tpp.omx,boards,EA +WLK_LRF_DRV_WAIT,trnskmEA_WLK_LRF_DRV.tpp.omx,wait,EA +WLK_LRF_DRV_TOTIVT,trnskmEA_WLK_LRF_DRV.tpp.omx,ivt,EA +WLK_LRF_DRV_KEYIVT,trnskmEA_WLK_LRF_DRV.tpp.omx,ivtLRF,EA +WLK_LRF_DRV_FERRYIVT,trnskmEA_WLK_LRF_DRV.tpp.omx,ivtFerry,EA +WLK_LRF_DRV_FAR,trnskmEA_WLK_LRF_DRV.tpp.omx,fare,EA +WLK_LRF_DRV_DTIM,trnskmEA_WLK_LRF_DRV.tpp.omx,dtime,EA +WLK_LRF_DRV_DDIST,trnskmEA_WLK_LRF_DRV.tpp.omx,ddist,EA +WLK_LRF_DRV_WAUX,trnskmEA_WLK_LRF_DRV.tpp.omx,waux,EA +WLK_LRF_DRV_IWAIT,trnskmEA_WLK_LRF_DRV.tpp.omx,iwait,EA +WLK_LRF_DRV_XWAIT,trnskmEA_WLK_LRF_DRV.tpp.omx,xwait,EA +WLK_LRF_DRV_BOARDS,trnskmEA_WLK_LRF_DRV.tpp.omx,boards,EA +WLK_LRF_WLK_WAIT,trnskmEA_WLK_LRF_WLK.tpp.omx,wait,EA +WLK_LRF_WLK_TOTIVT,trnskmEA_WLK_LRF_WLK.tpp.omx,ivt,EA +WLK_LRF_WLK_KEYIVT,trnskmEA_WLK_LRF_WLK.tpp.omx,ivtLRF,EA +WLK_LRF_WLK_FERRYIVT,trnskmEA_WLK_LRF_WLK.tpp.omx,ivtFerry,EA +WLK_LRF_WLK_FAR,trnskmEA_WLK_LRF_WLK.tpp.omx,fare,EA +WLK_LRF_WLK_WAUX,trnskmEA_WLK_LRF_WLK.tpp.omx,waux,EA +WLK_LRF_WLK_IWAIT,trnskmEA_WLK_LRF_WLK.tpp.omx,iwait,EA +WLK_LRF_WLK_XWAIT,trnskmEA_WLK_LRF_WLK.tpp.omx,xwait,EA +WLK_LRF_WLK_BOARDS,trnskmEA_WLK_LRF_WLK.tpp.omx,boards,EA +DRV_COM_WLK_WAIT,trnskmEV_DRV_COM_WLK.tpp.omx,wait,EV +DRV_COM_WLK_TOTIVT,trnskmEV_DRV_COM_WLK.tpp.omx,ivt,EV +DRV_COM_WLK_KEYIVT,trnskmEV_DRV_COM_WLK.tpp.omx,ivtCOM,EV +DRV_COM_WLK_FAR,trnskmEV_DRV_COM_WLK.tpp.omx,fare,EV +DRV_COM_WLK_DTIM,trnskmEV_DRV_COM_WLK.tpp.omx,dtime,EV +DRV_COM_WLK_DDIST,trnskmEV_DRV_COM_WLK.tpp.omx,ddist,EV +DRV_COM_WLK_WAUX,trnskmEV_DRV_COM_WLK.tpp.omx,waux,EV +DRV_COM_WLK_IWAIT,trnskmEV_DRV_COM_WLK.tpp.omx,iwait,EV +DRV_COM_WLK_XWAIT,trnskmEV_DRV_COM_WLK.tpp.omx,xwait,EV +DRV_COM_WLK_BOARDS,trnskmEV_DRV_COM_WLK.tpp.omx,boards,EV +DRV_EXP_WLK_WAIT,trnskmEV_DRV_EXP_WLK.tpp.omx,wait,EV +DRV_EXP_WLK_TOTIVT,trnskmEV_DRV_EXP_WLK.tpp.omx,ivt,EV +DRV_EXP_WLK_KEYIVT,trnskmEV_DRV_EXP_WLK.tpp.omx,ivtEXP,EV +DRV_EXP_WLK_FAR,trnskmEV_DRV_EXP_WLK.tpp.omx,fare,EV +DRV_EXP_WLK_DTIM,trnskmEV_DRV_EXP_WLK.tpp.omx,dtime,EV +DRV_EXP_WLK_WAUX,trnskmEV_DRV_EXP_WLK.tpp.omx,waux,EV +DRV_EXP_WLK_IWAIT,trnskmEV_DRV_EXP_WLK.tpp.omx,iwait,EV +DRV_EXP_WLK_XWAIT,trnskmEV_DRV_EXP_WLK.tpp.omx,xwait,EV +DRV_EXP_WLK_BOARDS,trnskmEV_DRV_EXP_WLK.tpp.omx,boards,EV +DRV_EXP_WLK_DDIST,trnskmEV_DRV_EXP_WLK.tpp.omx,ddist,EV +DRV_HVY_WLK_WAIT,trnskmEV_DRV_HVY_WLK.tpp.omx,wait,EV +DRV_HVY_WLK_TOTIVT,trnskmEV_DRV_HVY_WLK.tpp.omx,ivt,EV +DRV_HVY_WLK_KEYIVT,trnskmEV_DRV_HVY_WLK.tpp.omx,ivtHVY,EV +DRV_HVY_WLK_FAR,trnskmEV_DRV_HVY_WLK.tpp.omx,fare,EV +DRV_HVY_WLK_DTIM,trnskmEV_DRV_HVY_WLK.tpp.omx,dtime,EV +DRV_HVY_WLK_DDIST,trnskmEV_DRV_HVY_WLK.tpp.omx,ddist,EV +DRV_HVY_WLK_WAUX,trnskmEV_DRV_HVY_WLK.tpp.omx,waux,EV +DRV_HVY_WLK_IWAIT,trnskmEV_DRV_HVY_WLK.tpp.omx,iwait,EV +DRV_HVY_WLK_XWAIT,trnskmEV_DRV_HVY_WLK.tpp.omx,xwait,EV +DRV_HVY_WLK_BOARDS,trnskmEV_DRV_HVY_WLK.tpp.omx,boards,EV +DRV_LOC_WLK_WAIT,trnskmEV_DRV_LOC_WLK.tpp.omx,wait,EV +DRV_LOC_WLK_TOTIVT,trnskmEV_DRV_LOC_WLK.tpp.omx,ivt,EV +DRV_LOC_WLK_FAR,trnskmEV_DRV_LOC_WLK.tpp.omx,fare,EV +DRV_LOC_WLK_DTIM,trnskmEV_DRV_LOC_WLK.tpp.omx,dtime,EV +DRV_LOC_WLK_DDIST,trnskmEV_DRV_LOC_WLK.tpp.omx,ddist,EV +DRV_LOC_WLK_WAUX,trnskmEV_DRV_LOC_WLK.tpp.omx,waux,EV +DRV_LOC_WLK_IWAIT,trnskmEV_DRV_LOC_WLK.tpp.omx,iwait,EV +DRV_LOC_WLK_XWAIT,trnskmEV_DRV_LOC_WLK.tpp.omx,xwait,EV +DRV_LOC_WLK_BOARDS,trnskmEV_DRV_LOC_WLK.tpp.omx,boards,EV +DRV_LRF_WLK_WAIT,trnskmEV_DRV_LRF_WLK.tpp.omx,wait,EV +DRV_LRF_WLK_TOTIVT,trnskmEV_DRV_LRF_WLK.tpp.omx,ivt,EV +DRV_LRF_WLK_KEYIVT,trnskmEV_DRV_LRF_WLK.tpp.omx,ivtLRF,EV +DRV_LRF_WLK_FERRYIVT,trnskmEV_DRV_LRF_WLK.tpp.omx,ivtFerry,EV +DRV_LRF_WLK_FAR,trnskmEV_DRV_LRF_WLK.tpp.omx,fare,EV +DRV_LRF_WLK_DTIM,trnskmEV_DRV_LRF_WLK.tpp.omx,dtime,EV +DRV_LRF_WLK_DDIST,trnskmEV_DRV_LRF_WLK.tpp.omx,ddist,EV +DRV_LRF_WLK_WAUX,trnskmEV_DRV_LRF_WLK.tpp.omx,waux,EV +DRV_LRF_WLK_IWAIT,trnskmEV_DRV_LRF_WLK.tpp.omx,iwait,EV +DRV_LRF_WLK_XWAIT,trnskmEV_DRV_LRF_WLK.tpp.omx,xwait,EV +DRV_LRF_WLK_BOARDS,trnskmEV_DRV_LRF_WLK.tpp.omx,boards,EV +WLK_COM_DRV_WAIT,trnskmEV_WLK_COM_DRV.tpp.omx,wait,EV +WLK_COM_DRV_TOTIVT,trnskmEV_WLK_COM_DRV.tpp.omx,ivt,EV +WLK_COM_DRV_KEYIVT,trnskmEV_WLK_COM_DRV.tpp.omx,ivtCOM,EV +WLK_COM_DRV_FAR,trnskmEV_WLK_COM_DRV.tpp.omx,fare,EV +WLK_COM_DRV_DTIM,trnskmEV_WLK_COM_DRV.tpp.omx,dtime,EV +WLK_COM_DRV_DDIST,trnskmEV_WLK_COM_DRV.tpp.omx,ddist,EV +WLK_COM_DRV_WAUX,trnskmEV_WLK_COM_DRV.tpp.omx,waux,EV +WLK_COM_DRV_IWAIT,trnskmEV_WLK_COM_DRV.tpp.omx,iwait,EV +WLK_COM_DRV_XWAIT,trnskmEV_WLK_COM_DRV.tpp.omx,xwait,EV +WLK_COM_DRV_BOARDS,trnskmEV_WLK_COM_DRV.tpp.omx,boards,EV +WLK_COM_WLK_WAIT,trnskmEV_WLK_COM_WLK.tpp.omx,wait,EV +WLK_COM_WLK_TOTIVT,trnskmEV_WLK_COM_WLK.tpp.omx,ivt,EV +WLK_COM_WLK_KEYIVT,trnskmEV_WLK_COM_WLK.tpp.omx,ivtCOM,EV +WLK_COM_WLK_FAR,trnskmEV_WLK_COM_WLK.tpp.omx,fare,EV +WLK_COM_WLK_WAUX,trnskmEV_WLK_COM_WLK.tpp.omx,waux,EV +WLK_COM_WLK_IWAIT,trnskmEV_WLK_COM_WLK.tpp.omx,iwait,EV +WLK_COM_WLK_XWAIT,trnskmEV_WLK_COM_WLK.tpp.omx,xwait,EV +WLK_COM_WLK_BOARDS,trnskmEV_WLK_COM_WLK.tpp.omx,boards,EV +WLK_EXP_DRV_WAIT,trnskmEV_WLK_EXP_DRV.tpp.omx,wait,EV +WLK_EXP_DRV_TOTIVT,trnskmEV_WLK_EXP_DRV.tpp.omx,ivt,EV +WLK_EXP_DRV_KEYIVT,trnskmEV_WLK_EXP_DRV.tpp.omx,ivtEXP,EV +WLK_EXP_DRV_FAR,trnskmEV_WLK_EXP_DRV.tpp.omx,fare,EV +WLK_EXP_DRV_DTIM,trnskmEV_WLK_EXP_DRV.tpp.omx,dtime,EV +WLK_EXP_DRV_WAUX,trnskmEV_WLK_EXP_DRV.tpp.omx,waux,EV +WLK_EXP_DRV_IWAIT,trnskmEV_WLK_EXP_DRV.tpp.omx,iwait,EV +WLK_EXP_DRV_XWAIT,trnskmEV_WLK_EXP_DRV.tpp.omx,xwait,EV +WLK_EXP_DRV_BOARDS,trnskmEV_WLK_EXP_DRV.tpp.omx,boards,EV +WLK_EXP_DRV_DDIST,trnskmEV_WLK_EXP_DRV.tpp.omx,ddist,EV +WLK_EXP_WLK_WAIT,trnskmEV_WLK_EXP_WLK.tpp.omx,wait,EV +WLK_EXP_WLK_TOTIVT,trnskmEV_WLK_EXP_WLK.tpp.omx,ivt,EV +WLK_EXP_WLK_KEYIVT,trnskmEV_WLK_EXP_WLK.tpp.omx,ivtEXP,EV +WLK_EXP_WLK_FAR,trnskmEV_WLK_EXP_WLK.tpp.omx,fare,EV +WLK_EXP_WLK_WAUX,trnskmEV_WLK_EXP_WLK.tpp.omx,waux,EV +WLK_EXP_WLK_IWAIT,trnskmEV_WLK_EXP_WLK.tpp.omx,iwait,EV +WLK_EXP_WLK_XWAIT,trnskmEV_WLK_EXP_WLK.tpp.omx,xwait,EV +WLK_EXP_WLK_BOARDS,trnskmEV_WLK_EXP_WLK.tpp.omx,boards,EV +WLK_HVY_DRV_WAIT,trnskmEV_WLK_HVY_DRV.tpp.omx,wait,EV +WLK_HVY_DRV_TOTIVT,trnskmEV_WLK_HVY_DRV.tpp.omx,ivt,EV +WLK_HVY_DRV_KEYIVT,trnskmEV_WLK_HVY_DRV.tpp.omx,ivtHVY,EV +WLK_HVY_DRV_FAR,trnskmEV_WLK_HVY_DRV.tpp.omx,fare,EV +WLK_HVY_DRV_DTIM,trnskmEV_WLK_HVY_DRV.tpp.omx,dtime,EV +WLK_HVY_DRV_DDIST,trnskmEV_WLK_HVY_DRV.tpp.omx,ddist,EV +WLK_HVY_DRV_WAUX,trnskmEV_WLK_HVY_DRV.tpp.omx,waux,EV +WLK_HVY_DRV_IWAIT,trnskmEV_WLK_HVY_DRV.tpp.omx,iwait,EV +WLK_HVY_DRV_XWAIT,trnskmEV_WLK_HVY_DRV.tpp.omx,xwait,EV +WLK_HVY_DRV_BOARDS,trnskmEV_WLK_HVY_DRV.tpp.omx,boards,EV +WLK_HVY_WLK_WAIT,trnskmEV_WLK_HVY_WLK.tpp.omx,wait,EV +WLK_HVY_WLK_TOTIVT,trnskmEV_WLK_HVY_WLK.tpp.omx,ivt,EV +WLK_HVY_WLK_KEYIVT,trnskmEV_WLK_HVY_WLK.tpp.omx,ivtHVY,EV +WLK_HVY_WLK_FAR,trnskmEV_WLK_HVY_WLK.tpp.omx,fare,EV +WLK_HVY_WLK_WAUX,trnskmEV_WLK_HVY_WLK.tpp.omx,waux,EV +WLK_HVY_WLK_IWAIT,trnskmEV_WLK_HVY_WLK.tpp.omx,iwait,EV +WLK_HVY_WLK_XWAIT,trnskmEV_WLK_HVY_WLK.tpp.omx,xwait,EV +WLK_HVY_WLK_BOARDS,trnskmEV_WLK_HVY_WLK.tpp.omx,boards,EV +WLK_LOC_DRV_WAIT,trnskmEV_WLK_LOC_DRV.tpp.omx,wait,EV +WLK_LOC_DRV_TOTIVT,trnskmEV_WLK_LOC_DRV.tpp.omx,ivt,EV +WLK_LOC_DRV_FAR,trnskmEV_WLK_LOC_DRV.tpp.omx,fare,EV +WLK_LOC_DRV_DTIM,trnskmEV_WLK_LOC_DRV.tpp.omx,dtime,EV +WLK_LOC_DRV_DDIST,trnskmEV_WLK_LOC_DRV.tpp.omx,ddist,EV +WLK_LOC_DRV_WAUX,trnskmEV_WLK_LOC_DRV.tpp.omx,waux,EV +WLK_LOC_DRV_IWAIT,trnskmEV_WLK_LOC_DRV.tpp.omx,iwait,EV +WLK_LOC_DRV_XWAIT,trnskmEV_WLK_LOC_DRV.tpp.omx,xwait,EV +WLK_LOC_DRV_BOARDS,trnskmEV_WLK_LOC_DRV.tpp.omx,boards,EV +WLK_LOC_WLK_WAIT,trnskmEV_WLK_LOC_WLK.tpp.omx,wait,EV +WLK_LOC_WLK_TOTIVT,trnskmEV_WLK_LOC_WLK.tpp.omx,ivt,EV +WLK_LOC_WLK_FAR,trnskmEV_WLK_LOC_WLK.tpp.omx,fare,EV +WLK_LOC_WLK_WAUX,trnskmEV_WLK_LOC_WLK.tpp.omx,waux,EV +WLK_LOC_WLK_IWAIT,trnskmEV_WLK_LOC_WLK.tpp.omx,iwait,EV +WLK_LOC_WLK_XWAIT,trnskmEV_WLK_LOC_WLK.tpp.omx,xwait,EV +WLK_LOC_WLK_BOARDS,trnskmEV_WLK_LOC_WLK.tpp.omx,boards,EV +WLK_LRF_DRV_WAIT,trnskmEV_WLK_LRF_DRV.tpp.omx,wait,EV +WLK_LRF_DRV_TOTIVT,trnskmEV_WLK_LRF_DRV.tpp.omx,ivt,EV +WLK_LRF_DRV_KEYIVT,trnskmEV_WLK_LRF_DRV.tpp.omx,ivtLRF,EV +WLK_LRF_DRV_FERRYIVT,trnskmEV_WLK_LRF_DRV.tpp.omx,ivtFerry,EV +WLK_LRF_DRV_FAR,trnskmEV_WLK_LRF_DRV.tpp.omx,fare,EV +WLK_LRF_DRV_DTIM,trnskmEV_WLK_LRF_DRV.tpp.omx,dtime,EV +WLK_LRF_DRV_DDIST,trnskmEV_WLK_LRF_DRV.tpp.omx,ddist,EV +WLK_LRF_DRV_WAUX,trnskmEV_WLK_LRF_DRV.tpp.omx,waux,EV +WLK_LRF_DRV_IWAIT,trnskmEV_WLK_LRF_DRV.tpp.omx,iwait,EV +WLK_LRF_DRV_XWAIT,trnskmEV_WLK_LRF_DRV.tpp.omx,xwait,EV +WLK_LRF_DRV_BOARDS,trnskmEV_WLK_LRF_DRV.tpp.omx,boards,EV +WLK_LRF_WLK_WAIT,trnskmEV_WLK_LRF_WLK.tpp.omx,wait,EV +WLK_LRF_WLK_TOTIVT,trnskmEV_WLK_LRF_WLK.tpp.omx,ivt,EV +WLK_LRF_WLK_KEYIVT,trnskmEV_WLK_LRF_WLK.tpp.omx,ivtLRF,EV +WLK_LRF_WLK_FERRYIVT,trnskmEV_WLK_LRF_WLK.tpp.omx,ivtFerry,EV +WLK_LRF_WLK_FAR,trnskmEV_WLK_LRF_WLK.tpp.omx,fare,EV +WLK_LRF_WLK_WAUX,trnskmEV_WLK_LRF_WLK.tpp.omx,waux,EV +WLK_LRF_WLK_IWAIT,trnskmEV_WLK_LRF_WLK.tpp.omx,iwait,EV +WLK_LRF_WLK_XWAIT,trnskmEV_WLK_LRF_WLK.tpp.omx,xwait,EV +WLK_LRF_WLK_BOARDS,trnskmEV_WLK_LRF_WLK.tpp.omx,boards,EV +DRV_COM_WLK_WAIT,trnskmMD_DRV_COM_WLK.tpp.omx,wait,MD +DRV_COM_WLK_TOTIVT,trnskmMD_DRV_COM_WLK.tpp.omx,ivt,MD +DRV_COM_WLK_KEYIVT,trnskmMD_DRV_COM_WLK.tpp.omx,ivtCOM,MD +DRV_COM_WLK_FAR,trnskmMD_DRV_COM_WLK.tpp.omx,fare,MD +DRV_COM_WLK_DTIM,trnskmMD_DRV_COM_WLK.tpp.omx,dtime,MD +DRV_COM_WLK_DDIST,trnskmMD_DRV_COM_WLK.tpp.omx,ddist,MD +DRV_COM_WLK_WAUX,trnskmMD_DRV_COM_WLK.tpp.omx,waux,MD +DRV_COM_WLK_IWAIT,trnskmMD_DRV_COM_WLK.tpp.omx,iwait,MD +DRV_COM_WLK_XWAIT,trnskmMD_DRV_COM_WLK.tpp.omx,xwait,MD +DRV_COM_WLK_BOARDS,trnskmMD_DRV_COM_WLK.tpp.omx,boards,MD +DRV_EXP_WLK_WAIT,trnskmMD_DRV_EXP_WLK.tpp.omx,wait,MD +DRV_EXP_WLK_TOTIVT,trnskmMD_DRV_EXP_WLK.tpp.omx,ivt,MD +DRV_EXP_WLK_KEYIVT,trnskmMD_DRV_EXP_WLK.tpp.omx,ivtEXP,MD +DRV_EXP_WLK_FAR,trnskmMD_DRV_EXP_WLK.tpp.omx,fare,MD +DRV_EXP_WLK_DTIM,trnskmMD_DRV_EXP_WLK.tpp.omx,dtime,MD +DRV_EXP_WLK_WAUX,trnskmMD_DRV_EXP_WLK.tpp.omx,waux,MD +DRV_EXP_WLK_IWAIT,trnskmMD_DRV_EXP_WLK.tpp.omx,iwait,MD +DRV_EXP_WLK_XWAIT,trnskmMD_DRV_EXP_WLK.tpp.omx,xwait,MD +DRV_EXP_WLK_BOARDS,trnskmMD_DRV_EXP_WLK.tpp.omx,boards,MD +DRV_EXP_WLK_DDIST,trnskmMD_DRV_EXP_WLK.tpp.omx,ddist,MD +DRV_HVY_WLK_WAIT,trnskmMD_DRV_HVY_WLK.tpp.omx,wait,MD +DRV_HVY_WLK_TOTIVT,trnskmMD_DRV_HVY_WLK.tpp.omx,ivt,MD +DRV_HVY_WLK_KEYIVT,trnskmMD_DRV_HVY_WLK.tpp.omx,ivtHVY,MD +DRV_HVY_WLK_FAR,trnskmMD_DRV_HVY_WLK.tpp.omx,fare,MD +DRV_HVY_WLK_DTIM,trnskmMD_DRV_HVY_WLK.tpp.omx,dtime,MD +DRV_HVY_WLK_DDIST,trnskmMD_DRV_HVY_WLK.tpp.omx,ddist,MD +DRV_HVY_WLK_WAUX,trnskmMD_DRV_HVY_WLK.tpp.omx,waux,MD +DRV_HVY_WLK_IWAIT,trnskmMD_DRV_HVY_WLK.tpp.omx,iwait,MD +DRV_HVY_WLK_XWAIT,trnskmMD_DRV_HVY_WLK.tpp.omx,xwait,MD +DRV_HVY_WLK_BOARDS,trnskmMD_DRV_HVY_WLK.tpp.omx,boards,MD +DRV_LOC_WLK_WAIT,trnskmMD_DRV_LOC_WLK.tpp.omx,wait,MD +DRV_LOC_WLK_TOTIVT,trnskmMD_DRV_LOC_WLK.tpp.omx,ivt,MD +DRV_LOC_WLK_FAR,trnskmMD_DRV_LOC_WLK.tpp.omx,fare,MD +DRV_LOC_WLK_DTIM,trnskmMD_DRV_LOC_WLK.tpp.omx,dtime,MD +DRV_LOC_WLK_DDIST,trnskmMD_DRV_LOC_WLK.tpp.omx,ddist,MD +DRV_LOC_WLK_WAUX,trnskmMD_DRV_LOC_WLK.tpp.omx,waux,MD +DRV_LOC_WLK_IWAIT,trnskmMD_DRV_LOC_WLK.tpp.omx,iwait,MD +DRV_LOC_WLK_XWAIT,trnskmMD_DRV_LOC_WLK.tpp.omx,xwait,MD +DRV_LOC_WLK_BOARDS,trnskmMD_DRV_LOC_WLK.tpp.omx,boards,MD +DRV_LRF_WLK_WAIT,trnskmMD_DRV_LRF_WLK.tpp.omx,wait,MD +DRV_LRF_WLK_TOTIVT,trnskmMD_DRV_LRF_WLK.tpp.omx,ivt,MD +DRV_LRF_WLK_KEYIVT,trnskmMD_DRV_LRF_WLK.tpp.omx,ivtLRF,MD +DRV_LRF_WLK_FERRYIVT,trnskmMD_DRV_LRF_WLK.tpp.omx,ivtFerry,MD +DRV_LRF_WLK_FAR,trnskmMD_DRV_LRF_WLK.tpp.omx,fare,MD +DRV_LRF_WLK_DTIM,trnskmMD_DRV_LRF_WLK.tpp.omx,dtime,MD +DRV_LRF_WLK_DDIST,trnskmMD_DRV_LRF_WLK.tpp.omx,ddist,MD +DRV_LRF_WLK_WAUX,trnskmMD_DRV_LRF_WLK.tpp.omx,waux,MD +DRV_LRF_WLK_IWAIT,trnskmMD_DRV_LRF_WLK.tpp.omx,iwait,MD +DRV_LRF_WLK_XWAIT,trnskmMD_DRV_LRF_WLK.tpp.omx,xwait,MD +DRV_LRF_WLK_BOARDS,trnskmMD_DRV_LRF_WLK.tpp.omx,boards,MD +WLK_COM_DRV_WAIT,trnskmMD_WLK_COM_DRV.tpp.omx,wait,MD +WLK_COM_DRV_TOTIVT,trnskmMD_WLK_COM_DRV.tpp.omx,ivt,MD +WLK_COM_DRV_KEYIVT,trnskmMD_WLK_COM_DRV.tpp.omx,ivtCOM,MD +WLK_COM_DRV_FAR,trnskmMD_WLK_COM_DRV.tpp.omx,fare,MD +WLK_COM_DRV_DTIM,trnskmMD_WLK_COM_DRV.tpp.omx,dtime,MD +WLK_COM_DRV_DDIST,trnskmMD_WLK_COM_DRV.tpp.omx,ddist,MD +WLK_COM_DRV_WAUX,trnskmMD_WLK_COM_DRV.tpp.omx,waux,MD +WLK_COM_DRV_IWAIT,trnskmMD_WLK_COM_DRV.tpp.omx,iwait,MD +WLK_COM_DRV_XWAIT,trnskmMD_WLK_COM_DRV.tpp.omx,xwait,MD +WLK_COM_DRV_BOARDS,trnskmMD_WLK_COM_DRV.tpp.omx,boards,MD +WLK_COM_WLK_WAIT,trnskmMD_WLK_COM_WLK.tpp.omx,wait,MD +WLK_COM_WLK_TOTIVT,trnskmMD_WLK_COM_WLK.tpp.omx,ivt,MD +WLK_COM_WLK_KEYIVT,trnskmMD_WLK_COM_WLK.tpp.omx,ivtCOM,MD +WLK_COM_WLK_FAR,trnskmMD_WLK_COM_WLK.tpp.omx,fare,MD +WLK_COM_WLK_WAUX,trnskmMD_WLK_COM_WLK.tpp.omx,waux,MD +WLK_COM_WLK_IWAIT,trnskmMD_WLK_COM_WLK.tpp.omx,iwait,MD +WLK_COM_WLK_XWAIT,trnskmMD_WLK_COM_WLK.tpp.omx,xwait,MD +WLK_COM_WLK_BOARDS,trnskmMD_WLK_COM_WLK.tpp.omx,boards,MD +WLK_EXP_DRV_WAIT,trnskmMD_WLK_EXP_DRV.tpp.omx,wait,MD +WLK_EXP_DRV_TOTIVT,trnskmMD_WLK_EXP_DRV.tpp.omx,ivt,MD +WLK_EXP_DRV_KEYIVT,trnskmMD_WLK_EXP_DRV.tpp.omx,ivtEXP,MD +WLK_EXP_DRV_FAR,trnskmMD_WLK_EXP_DRV.tpp.omx,fare,MD +WLK_EXP_DRV_DTIM,trnskmMD_WLK_EXP_DRV.tpp.omx,dtime,MD +WLK_EXP_DRV_WAUX,trnskmMD_WLK_EXP_DRV.tpp.omx,waux,MD +WLK_EXP_DRV_IWAIT,trnskmMD_WLK_EXP_DRV.tpp.omx,iwait,MD +WLK_EXP_DRV_XWAIT,trnskmMD_WLK_EXP_DRV.tpp.omx,xwait,MD +WLK_EXP_DRV_BOARDS,trnskmMD_WLK_EXP_DRV.tpp.omx,boards,MD +WLK_EXP_DRV_DDIST,trnskmMD_WLK_EXP_DRV.tpp.omx,ddist,MD +WLK_EXP_WLK_WAIT,trnskmMD_WLK_EXP_WLK.tpp.omx,wait,MD +WLK_EXP_WLK_TOTIVT,trnskmMD_WLK_EXP_WLK.tpp.omx,ivt,MD +WLK_EXP_WLK_KEYIVT,trnskmMD_WLK_EXP_WLK.tpp.omx,ivtEXP,MD +WLK_EXP_WLK_FAR,trnskmMD_WLK_EXP_WLK.tpp.omx,fare,MD +WLK_EXP_WLK_WAUX,trnskmMD_WLK_EXP_WLK.tpp.omx,waux,MD +WLK_EXP_WLK_IWAIT,trnskmMD_WLK_EXP_WLK.tpp.omx,iwait,MD +WLK_EXP_WLK_XWAIT,trnskmMD_WLK_EXP_WLK.tpp.omx,xwait,MD +WLK_EXP_WLK_BOARDS,trnskmMD_WLK_EXP_WLK.tpp.omx,boards,MD +WLK_HVY_DRV_WAIT,trnskmMD_WLK_HVY_DRV.tpp.omx,wait,MD +WLK_HVY_DRV_TOTIVT,trnskmMD_WLK_HVY_DRV.tpp.omx,ivt,MD +WLK_HVY_DRV_KEYIVT,trnskmMD_WLK_HVY_DRV.tpp.omx,ivtHVY,MD +WLK_HVY_DRV_FAR,trnskmMD_WLK_HVY_DRV.tpp.omx,fare,MD +WLK_HVY_DRV_DTIM,trnskmMD_WLK_HVY_DRV.tpp.omx,dtime,MD +WLK_HVY_DRV_DDIST,trnskmMD_WLK_HVY_DRV.tpp.omx,ddist,MD +WLK_HVY_DRV_WAUX,trnskmMD_WLK_HVY_DRV.tpp.omx,waux,MD +WLK_HVY_DRV_IWAIT,trnskmMD_WLK_HVY_DRV.tpp.omx,iwait,MD +WLK_HVY_DRV_XWAIT,trnskmMD_WLK_HVY_DRV.tpp.omx,xwait,MD +WLK_HVY_DRV_BOARDS,trnskmMD_WLK_HVY_DRV.tpp.omx,boards,MD +WLK_HVY_WLK_WAIT,trnskmMD_WLK_HVY_WLK.tpp.omx,wait,MD +WLK_HVY_WLK_TOTIVT,trnskmMD_WLK_HVY_WLK.tpp.omx,ivt,MD +WLK_HVY_WLK_KEYIVT,trnskmMD_WLK_HVY_WLK.tpp.omx,ivtHVY,MD +WLK_HVY_WLK_FAR,trnskmMD_WLK_HVY_WLK.tpp.omx,fare,MD +WLK_HVY_WLK_WAUX,trnskmMD_WLK_HVY_WLK.tpp.omx,waux,MD +WLK_HVY_WLK_IWAIT,trnskmMD_WLK_HVY_WLK.tpp.omx,iwait,MD +WLK_HVY_WLK_XWAIT,trnskmMD_WLK_HVY_WLK.tpp.omx,xwait,MD +WLK_HVY_WLK_BOARDS,trnskmMD_WLK_HVY_WLK.tpp.omx,boards,MD +WLK_LOC_DRV_WAIT,trnskmMD_WLK_LOC_DRV.tpp.omx,wait,MD +WLK_LOC_DRV_TOTIVT,trnskmMD_WLK_LOC_DRV.tpp.omx,ivt,MD +WLK_LOC_DRV_FAR,trnskmMD_WLK_LOC_DRV.tpp.omx,fare,MD +WLK_LOC_DRV_DTIM,trnskmMD_WLK_LOC_DRV.tpp.omx,dtime,MD +WLK_LOC_DRV_DDIST,trnskmMD_WLK_LOC_DRV.tpp.omx,ddist,MD +WLK_LOC_DRV_WAUX,trnskmMD_WLK_LOC_DRV.tpp.omx,waux,MD +WLK_LOC_DRV_IWAIT,trnskmMD_WLK_LOC_DRV.tpp.omx,iwait,MD +WLK_LOC_DRV_XWAIT,trnskmMD_WLK_LOC_DRV.tpp.omx,xwait,MD +WLK_LOC_DRV_BOARDS,trnskmMD_WLK_LOC_DRV.tpp.omx,boards,MD +WLK_LOC_WLK_WAIT,trnskmMD_WLK_LOC_WLK.tpp.omx,wait,MD +WLK_LOC_WLK_TOTIVT,trnskmMD_WLK_LOC_WLK.tpp.omx,ivt,MD +WLK_LOC_WLK_FAR,trnskmMD_WLK_LOC_WLK.tpp.omx,fare,MD +WLK_LOC_WLK_WAUX,trnskmMD_WLK_LOC_WLK.tpp.omx,waux,MD +WLK_LOC_WLK_IWAIT,trnskmMD_WLK_LOC_WLK.tpp.omx,iwait,MD +WLK_LOC_WLK_XWAIT,trnskmMD_WLK_LOC_WLK.tpp.omx,xwait,MD +WLK_LOC_WLK_BOARDS,trnskmMD_WLK_LOC_WLK.tpp.omx,boards,MD +WLK_LRF_DRV_WAIT,trnskmMD_WLK_LRF_DRV.tpp.omx,wait,MD +WLK_LRF_DRV_TOTIVT,trnskmMD_WLK_LRF_DRV.tpp.omx,ivt,MD +WLK_LRF_DRV_KEYIVT,trnskmMD_WLK_LRF_DRV.tpp.omx,ivtLRF,MD +WLK_LRF_DRV_FERRYIVT,trnskmMD_WLK_LRF_DRV.tpp.omx,ivtFerry,MD +WLK_LRF_DRV_FAR,trnskmMD_WLK_LRF_DRV.tpp.omx,fare,MD +WLK_LRF_DRV_DTIM,trnskmMD_WLK_LRF_DRV.tpp.omx,dtime,MD +WLK_LRF_DRV_DDIST,trnskmMD_WLK_LRF_DRV.tpp.omx,ddist,MD +WLK_LRF_DRV_WAUX,trnskmMD_WLK_LRF_DRV.tpp.omx,waux,MD +WLK_LRF_DRV_IWAIT,trnskmMD_WLK_LRF_DRV.tpp.omx,iwait,MD +WLK_LRF_DRV_XWAIT,trnskmMD_WLK_LRF_DRV.tpp.omx,xwait,MD +WLK_LRF_DRV_BOARDS,trnskmMD_WLK_LRF_DRV.tpp.omx,boards,MD +WLK_LRF_WLK_WAIT,trnskmMD_WLK_LRF_WLK.tpp.omx,wait,MD +WLK_LRF_WLK_TOTIVT,trnskmMD_WLK_LRF_WLK.tpp.omx,ivt,MD +WLK_LRF_WLK_KEYIVT,trnskmMD_WLK_LRF_WLK.tpp.omx,ivtLRF,MD +WLK_LRF_WLK_FERRYIVT,trnskmMD_WLK_LRF_WLK.tpp.omx,ivtFerry,MD +WLK_LRF_WLK_FAR,trnskmMD_WLK_LRF_WLK.tpp.omx,fare,MD +WLK_LRF_WLK_WAUX,trnskmMD_WLK_LRF_WLK.tpp.omx,waux,MD +WLK_LRF_WLK_IWAIT,trnskmMD_WLK_LRF_WLK.tpp.omx,iwait,MD +WLK_LRF_WLK_XWAIT,trnskmMD_WLK_LRF_WLK.tpp.omx,xwait,MD +WLK_LRF_WLK_BOARDS,trnskmMD_WLK_LRF_WLK.tpp.omx,boards,MD +DRV_COM_WLK_WAIT,trnskmPM_DRV_COM_WLK.tpp.omx,wait,PM +DRV_COM_WLK_TOTIVT,trnskmPM_DRV_COM_WLK.tpp.omx,ivt,PM +DRV_COM_WLK_KEYIVT,trnskmPM_DRV_COM_WLK.tpp.omx,ivtCOM,PM +DRV_COM_WLK_FAR,trnskmPM_DRV_COM_WLK.tpp.omx,fare,PM +DRV_COM_WLK_DTIM,trnskmPM_DRV_COM_WLK.tpp.omx,dtime,PM +DRV_COM_WLK_DDIST,trnskmPM_DRV_COM_WLK.tpp.omx,ddist,PM +DRV_COM_WLK_WAUX,trnskmPM_DRV_COM_WLK.tpp.omx,waux,PM +DRV_COM_WLK_IWAIT,trnskmPM_DRV_COM_WLK.tpp.omx,iwait,PM +DRV_COM_WLK_XWAIT,trnskmPM_DRV_COM_WLK.tpp.omx,xwait,PM +DRV_COM_WLK_BOARDS,trnskmPM_DRV_COM_WLK.tpp.omx,boards,PM +DRV_EXP_WLK_WAIT,trnskmPM_DRV_EXP_WLK.tpp.omx,wait,PM +DRV_EXP_WLK_TOTIVT,trnskmPM_DRV_EXP_WLK.tpp.omx,ivt,PM +DRV_EXP_WLK_KEYIVT,trnskmPM_DRV_EXP_WLK.tpp.omx,ivtEXP,PM +DRV_EXP_WLK_FAR,trnskmPM_DRV_EXP_WLK.tpp.omx,fare,PM +DRV_EXP_WLK_DTIM,trnskmPM_DRV_EXP_WLK.tpp.omx,dtime,PM +DRV_EXP_WLK_WAUX,trnskmPM_DRV_EXP_WLK.tpp.omx,waux,PM +DRV_EXP_WLK_IWAIT,trnskmPM_DRV_EXP_WLK.tpp.omx,iwait,PM +DRV_EXP_WLK_XWAIT,trnskmPM_DRV_EXP_WLK.tpp.omx,xwait,PM +DRV_EXP_WLK_BOARDS,trnskmPM_DRV_EXP_WLK.tpp.omx,boards,PM +DRV_EXP_WLK_DDIST,trnskmPM_DRV_EXP_WLK.tpp.omx,ddist,PM +DRV_HVY_WLK_WAIT,trnskmPM_DRV_HVY_WLK.tpp.omx,wait,PM +DRV_HVY_WLK_TOTIVT,trnskmPM_DRV_HVY_WLK.tpp.omx,ivt,PM +DRV_HVY_WLK_KEYIVT,trnskmPM_DRV_HVY_WLK.tpp.omx,ivtHVY,PM +DRV_HVY_WLK_FAR,trnskmPM_DRV_HVY_WLK.tpp.omx,fare,PM +DRV_HVY_WLK_DTIM,trnskmPM_DRV_HVY_WLK.tpp.omx,dtime,PM +DRV_HVY_WLK_DDIST,trnskmPM_DRV_HVY_WLK.tpp.omx,ddist,PM +DRV_HVY_WLK_WAUX,trnskmPM_DRV_HVY_WLK.tpp.omx,waux,PM +DRV_HVY_WLK_IWAIT,trnskmPM_DRV_HVY_WLK.tpp.omx,iwait,PM +DRV_HVY_WLK_XWAIT,trnskmPM_DRV_HVY_WLK.tpp.omx,xwait,PM +DRV_HVY_WLK_BOARDS,trnskmPM_DRV_HVY_WLK.tpp.omx,boards,PM +DRV_LOC_WLK_WAIT,trnskmPM_DRV_LOC_WLK.tpp.omx,wait,PM +DRV_LOC_WLK_TOTIVT,trnskmPM_DRV_LOC_WLK.tpp.omx,ivt,PM +DRV_LOC_WLK_FAR,trnskmPM_DRV_LOC_WLK.tpp.omx,fare,PM +DRV_LOC_WLK_DTIM,trnskmPM_DRV_LOC_WLK.tpp.omx,dtime,PM +DRV_LOC_WLK_DDIST,trnskmPM_DRV_LOC_WLK.tpp.omx,ddist,PM +DRV_LOC_WLK_WAUX,trnskmPM_DRV_LOC_WLK.tpp.omx,waux,PM +DRV_LOC_WLK_IWAIT,trnskmPM_DRV_LOC_WLK.tpp.omx,iwait,PM +DRV_LOC_WLK_XWAIT,trnskmPM_DRV_LOC_WLK.tpp.omx,xwait,PM +DRV_LOC_WLK_BOARDS,trnskmPM_DRV_LOC_WLK.tpp.omx,boards,PM +DRV_LRF_WLK_WAIT,trnskmPM_DRV_LRF_WLK.tpp.omx,wait,PM +DRV_LRF_WLK_TOTIVT,trnskmPM_DRV_LRF_WLK.tpp.omx,ivt,PM +DRV_LRF_WLK_KEYIVT,trnskmPM_DRV_LRF_WLK.tpp.omx,ivtLRF,PM +DRV_LRF_WLK_FERRYIVT,trnskmPM_DRV_LRF_WLK.tpp.omx,ivtFerry,PM +DRV_LRF_WLK_FAR,trnskmPM_DRV_LRF_WLK.tpp.omx,fare,PM +DRV_LRF_WLK_DTIM,trnskmPM_DRV_LRF_WLK.tpp.omx,dtime,PM +DRV_LRF_WLK_DDIST,trnskmPM_DRV_LRF_WLK.tpp.omx,ddist,PM +DRV_LRF_WLK_WAUX,trnskmPM_DRV_LRF_WLK.tpp.omx,waux,PM +DRV_LRF_WLK_IWAIT,trnskmPM_DRV_LRF_WLK.tpp.omx,iwait,PM +DRV_LRF_WLK_XWAIT,trnskmPM_DRV_LRF_WLK.tpp.omx,xwait,PM +DRV_LRF_WLK_BOARDS,trnskmPM_DRV_LRF_WLK.tpp.omx,boards,PM +WLK_COM_DRV_WAIT,trnskmPM_WLK_COM_DRV.tpp.omx,wait,PM +WLK_COM_DRV_TOTIVT,trnskmPM_WLK_COM_DRV.tpp.omx,ivt,PM +WLK_COM_DRV_KEYIVT,trnskmPM_WLK_COM_DRV.tpp.omx,ivtCOM,PM +WLK_COM_DRV_FAR,trnskmPM_WLK_COM_DRV.tpp.omx,fare,PM +WLK_COM_DRV_DTIM,trnskmPM_WLK_COM_DRV.tpp.omx,dtime,PM +WLK_COM_DRV_DDIST,trnskmPM_WLK_COM_DRV.tpp.omx,ddist,PM +WLK_COM_DRV_WAUX,trnskmPM_WLK_COM_DRV.tpp.omx,waux,PM +WLK_COM_DRV_IWAIT,trnskmPM_WLK_COM_DRV.tpp.omx,iwait,PM +WLK_COM_DRV_XWAIT,trnskmPM_WLK_COM_DRV.tpp.omx,xwait,PM +WLK_COM_DRV_BOARDS,trnskmPM_WLK_COM_DRV.tpp.omx,boards,PM +WLK_COM_WLK_WAIT,trnskmPM_WLK_COM_WLK.tpp.omx,wait,PM +WLK_COM_WLK_TOTIVT,trnskmPM_WLK_COM_WLK.tpp.omx,ivt,PM +WLK_COM_WLK_KEYIVT,trnskmPM_WLK_COM_WLK.tpp.omx,ivtCOM,PM +WLK_COM_WLK_FAR,trnskmPM_WLK_COM_WLK.tpp.omx,fare,PM +WLK_COM_WLK_WAUX,trnskmPM_WLK_COM_WLK.tpp.omx,waux,PM +WLK_COM_WLK_IWAIT,trnskmPM_WLK_COM_WLK.tpp.omx,iwait,PM +WLK_COM_WLK_XWAIT,trnskmPM_WLK_COM_WLK.tpp.omx,xwait,PM +WLK_COM_WLK_BOARDS,trnskmPM_WLK_COM_WLK.tpp.omx,boards,PM +WLK_EXP_DRV_WAIT,trnskmPM_WLK_EXP_DRV.tpp.omx,wait,PM +WLK_EXP_DRV_TOTIVT,trnskmPM_WLK_EXP_DRV.tpp.omx,ivt,PM +WLK_EXP_DRV_KEYIVT,trnskmPM_WLK_EXP_DRV.tpp.omx,ivtEXP,PM +WLK_EXP_DRV_FAR,trnskmPM_WLK_EXP_DRV.tpp.omx,fare,PM +WLK_EXP_DRV_DTIM,trnskmPM_WLK_EXP_DRV.tpp.omx,dtime,PM +WLK_EXP_DRV_WAUX,trnskmPM_WLK_EXP_DRV.tpp.omx,waux,PM +WLK_EXP_DRV_IWAIT,trnskmPM_WLK_EXP_DRV.tpp.omx,iwait,PM +WLK_EXP_DRV_XWAIT,trnskmPM_WLK_EXP_DRV.tpp.omx,xwait,PM +WLK_EXP_DRV_BOARDS,trnskmPM_WLK_EXP_DRV.tpp.omx,boards,PM +WLK_EXP_DRV_DDIST,trnskmPM_WLK_EXP_DRV.tpp.omx,ddist,PM +WLK_EXP_WLK_WAIT,trnskmPM_WLK_EXP_WLK.tpp.omx,wait,PM +WLK_EXP_WLK_TOTIVT,trnskmPM_WLK_EXP_WLK.tpp.omx,ivt,PM +WLK_EXP_WLK_KEYIVT,trnskmPM_WLK_EXP_WLK.tpp.omx,ivtEXP,PM +WLK_EXP_WLK_FAR,trnskmPM_WLK_EXP_WLK.tpp.omx,fare,PM +WLK_EXP_WLK_WAUX,trnskmPM_WLK_EXP_WLK.tpp.omx,waux,PM +WLK_EXP_WLK_IWAIT,trnskmPM_WLK_EXP_WLK.tpp.omx,iwait,PM +WLK_EXP_WLK_XWAIT,trnskmPM_WLK_EXP_WLK.tpp.omx,xwait,PM +WLK_EXP_WLK_BOARDS,trnskmPM_WLK_EXP_WLK.tpp.omx,boards,PM +WLK_HVY_DRV_WAIT,trnskmPM_WLK_HVY_DRV.tpp.omx,wait,PM +WLK_HVY_DRV_TOTIVT,trnskmPM_WLK_HVY_DRV.tpp.omx,ivt,PM +WLK_HVY_DRV_KEYIVT,trnskmPM_WLK_HVY_DRV.tpp.omx,ivtHVY,PM +WLK_HVY_DRV_FAR,trnskmPM_WLK_HVY_DRV.tpp.omx,fare,PM +WLK_HVY_DRV_DTIM,trnskmPM_WLK_HVY_DRV.tpp.omx,dtime,PM +WLK_HVY_DRV_DDIST,trnskmPM_WLK_HVY_DRV.tpp.omx,ddist,PM +WLK_HVY_DRV_WAUX,trnskmPM_WLK_HVY_DRV.tpp.omx,waux,PM +WLK_HVY_DRV_IWAIT,trnskmPM_WLK_HVY_DRV.tpp.omx,iwait,PM +WLK_HVY_DRV_XWAIT,trnskmPM_WLK_HVY_DRV.tpp.omx,xwait,PM +WLK_HVY_DRV_BOARDS,trnskmPM_WLK_HVY_DRV.tpp.omx,boards,PM +WLK_HVY_WLK_WAIT,trnskmPM_WLK_HVY_WLK.tpp.omx,wait,PM +WLK_HVY_WLK_TOTIVT,trnskmPM_WLK_HVY_WLK.tpp.omx,ivt,PM +WLK_HVY_WLK_KEYIVT,trnskmPM_WLK_HVY_WLK.tpp.omx,ivtHVY,PM +WLK_HVY_WLK_FAR,trnskmPM_WLK_HVY_WLK.tpp.omx,fare,PM +WLK_HVY_WLK_WAUX,trnskmPM_WLK_HVY_WLK.tpp.omx,waux,PM +WLK_HVY_WLK_IWAIT,trnskmPM_WLK_HVY_WLK.tpp.omx,iwait,PM +WLK_HVY_WLK_XWAIT,trnskmPM_WLK_HVY_WLK.tpp.omx,xwait,PM +WLK_HVY_WLK_BOARDS,trnskmPM_WLK_HVY_WLK.tpp.omx,boards,PM +WLK_LOC_DRV_WAIT,trnskmPM_WLK_LOC_DRV.tpp.omx,wait,PM +WLK_LOC_DRV_TOTIVT,trnskmPM_WLK_LOC_DRV.tpp.omx,ivt,PM +WLK_LOC_DRV_FAR,trnskmPM_WLK_LOC_DRV.tpp.omx,fare,PM +WLK_LOC_DRV_DTIM,trnskmPM_WLK_LOC_DRV.tpp.omx,dtime,PM +WLK_LOC_DRV_DDIST,trnskmPM_WLK_LOC_DRV.tpp.omx,ddist,PM +WLK_LOC_DRV_WAUX,trnskmPM_WLK_LOC_DRV.tpp.omx,waux,PM +WLK_LOC_DRV_IWAIT,trnskmPM_WLK_LOC_DRV.tpp.omx,iwait,PM +WLK_LOC_DRV_XWAIT,trnskmPM_WLK_LOC_DRV.tpp.omx,xwait,PM +WLK_LOC_DRV_BOARDS,trnskmPM_WLK_LOC_DRV.tpp.omx,boards,PM +WLK_LOC_WLK_WAIT,trnskmPM_WLK_LOC_WLK.tpp.omx,wait,PM +WLK_LOC_WLK_TOTIVT,trnskmPM_WLK_LOC_WLK.tpp.omx,ivt,PM +WLK_LOC_WLK_FAR,trnskmPM_WLK_LOC_WLK.tpp.omx,fare,PM +WLK_LOC_WLK_WAUX,trnskmPM_WLK_LOC_WLK.tpp.omx,waux,PM +WLK_LOC_WLK_IWAIT,trnskmPM_WLK_LOC_WLK.tpp.omx,iwait,PM +WLK_LOC_WLK_XWAIT,trnskmPM_WLK_LOC_WLK.tpp.omx,xwait,PM +WLK_LOC_WLK_BOARDS,trnskmPM_WLK_LOC_WLK.tpp.omx,boards,PM +WLK_LRF_DRV_WAIT,trnskmPM_WLK_LRF_DRV.tpp.omx,wait,PM +WLK_LRF_DRV_TOTIVT,trnskmPM_WLK_LRF_DRV.tpp.omx,ivt,PM +WLK_LRF_DRV_KEYIVT,trnskmPM_WLK_LRF_DRV.tpp.omx,ivtLRF,PM +WLK_LRF_DRV_FERRYIVT,trnskmPM_WLK_LRF_DRV.tpp.omx,ivtFerry,PM +WLK_LRF_DRV_FAR,trnskmPM_WLK_LRF_DRV.tpp.omx,fare,PM +WLK_LRF_DRV_DTIM,trnskmPM_WLK_LRF_DRV.tpp.omx,dtime,PM +WLK_LRF_DRV_DDIST,trnskmPM_WLK_LRF_DRV.tpp.omx,ddist,PM +WLK_LRF_DRV_WAUX,trnskmPM_WLK_LRF_DRV.tpp.omx,waux,PM +WLK_LRF_DRV_IWAIT,trnskmPM_WLK_LRF_DRV.tpp.omx,iwait,PM +WLK_LRF_DRV_XWAIT,trnskmPM_WLK_LRF_DRV.tpp.omx,xwait,PM +WLK_LRF_DRV_BOARDS,trnskmPM_WLK_LRF_DRV.tpp.omx,boards,PM +WLK_LRF_WLK_WAIT,trnskmPM_WLK_LRF_WLK.tpp.omx,wait,PM +WLK_LRF_WLK_TOTIVT,trnskmPM_WLK_LRF_WLK.tpp.omx,ivt,PM +WLK_LRF_WLK_KEYIVT,trnskmPM_WLK_LRF_WLK.tpp.omx,ivtLRF,PM +WLK_LRF_WLK_FERRYIVT,trnskmPM_WLK_LRF_WLK.tpp.omx,ivtFerry,PM +WLK_LRF_WLK_FAR,trnskmPM_WLK_LRF_WLK.tpp.omx,fare,PM +WLK_LRF_WLK_WAUX,trnskmPM_WLK_LRF_WLK.tpp.omx,waux,PM +WLK_LRF_WLK_IWAIT,trnskmPM_WLK_LRF_WLK.tpp.omx,iwait,PM +WLK_LRF_WLK_XWAIT,trnskmPM_WLK_LRF_WLK.tpp.omx,xwait,PM +WLK_LRF_WLK_BOARDS,trnskmPM_WLK_LRF_WLK.tpp.omx,boards,PM +WLK_TRN_WLK_IVT,trnskmAM_wlk_trn_wlk.tpp.omx,ivt,AM +WLK_TRN_WLK_IWAIT,trnskmAM_wlk_trn_wlk.tpp.omx,iwait,AM +WLK_TRN_WLK_XWAIT,trnskmAM_wlk_trn_wlk.tpp.omx,xwait,AM +WLK_TRN_WLK_WACC,trnskmAM_wlk_trn_wlk.tpp.omx,wacc,AM +WLK_TRN_WLK_WAUX,trnskmAM_wlk_trn_wlk.tpp.omx,waux,AM +WLK_TRN_WLK_WEGR,trnskmAM_wlk_trn_wlk.tpp.omx,wegr,AM +WLK_TRN_WLK_IVT,trnskmMD_wlk_trn_wlk.tpp.omx,ivt,MD +WLK_TRN_WLK_IWAIT,trnskmMD_wlk_trn_wlk.tpp.omx,iwait,MD +WLK_TRN_WLK_XWAIT,trnskmMD_wlk_trn_wlk.tpp.omx,xwait,MD +WLK_TRN_WLK_WACC,trnskmMD_wlk_trn_wlk.tpp.omx,wacc,MD +WLK_TRN_WLK_WAUX,trnskmMD_wlk_trn_wlk.tpp.omx,waux,MD +WLK_TRN_WLK_WEGR,trnskmMD_wlk_trn_wlk.tpp.omx,wegr,MD +WLK_TRN_WLK_IVT,trnskmPM_wlk_trn_wlk.tpp.omx,ivt,PM +WLK_TRN_WLK_IWAIT,trnskmPM_wlk_trn_wlk.tpp.omx,iwait,PM +WLK_TRN_WLK_XWAIT,trnskmPM_wlk_trn_wlk.tpp.omx,xwait,PM +WLK_TRN_WLK_WACC,trnskmPM_wlk_trn_wlk.tpp.omx,wacc,PM +WLK_TRN_WLK_WAUX,trnskmPM_wlk_trn_wlk.tpp.omx,waux,PM +WLK_TRN_WLK_WEGR,trnskmPM_wlk_trn_wlk.tpp.omx,wegr,PM From b8921597c5b2969fdeff53029ee8b46b3735a616 Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 1 Feb 2019 00:17:28 -0500 Subject: [PATCH 073/122] passing tests with mtc_tm1 verification dataset --- activitysim/abm/test/configs/settings.yaml | 3 +- activitysim/abm/test/data/mtc_asim.h5 | Bin 4354008 -> 3197440 bytes activitysim/abm/test/data/override_hh_ids.csv | 20 +-- activitysim/abm/test/data/skims.omx | Bin 3668684 -> 3674745 bytes activitysim/abm/test/run_mp.py | 14 +- activitysim/abm/test/test_pipeline.py | 130 +++++++++++++----- docs/abmexample.rst | 1 - scripts/create_sf_example.py | 49 +++---- scripts/mtc_inputs.py | 35 +++-- 9 files changed, 160 insertions(+), 92 deletions(-) diff --git a/activitysim/abm/test/configs/settings.yaml b/activitysim/abm/test/configs/settings.yaml index 9bfbe437e..fc5fd75ee 100644 --- a/activitysim/abm/test/configs/settings.yaml +++ b/activitysim/abm/test/configs/settings.yaml @@ -22,7 +22,8 @@ use_shadow_pricing: False # - tracing #trace household id; comment out or leave empty for no trace -#trace_hh_id: 1482966 +# households with all tour types +# [ 728370 1234067 1402924 1594625 1595333 1747572 1896849 1931818 2222690 2344951 2677154] trace_hh_id: # trace origin, destination in accessibility calculation; comment out or leave empty for no trace diff --git a/activitysim/abm/test/data/mtc_asim.h5 b/activitysim/abm/test/data/mtc_asim.h5 index d19cfb160a7d0beea2871eee7727a1d25688ba5b..c886000fa234d9b168a95ff70134fa5481362232 100644 GIT binary patch literal 3197440 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%0AX=j!6~8OkTB^V7PnCqF%P@&C)vk0+)h zu25eOQxCt=t@6D-by(^2^5%sVzq+2tA9#!QO>q1FnB$s~`CJ!L-}T5oRyU8lm>$Gm zHm+5_*XKB3H8&l8id_%>;N(TWx8I{F;d#DI{mO5B=7RDt^_CT{RlnEgIACwy%I|Al zckI-ve$_+T%GRH|zwK-1lTKo9-nn)9H&q{>VYQArKh5R+lS}_-m#NkD{jdi%CA?~X z^OZh6l!x-Lns-a{Yt`@d<#AMVx)0_j-s;=jE3div*;7h=pS!k|>GM?|%v>#>)uDNBDxAAckDtd; z&5g%Tx~>ObdvJSw>H8_`FKrBc_^IzasN>7i&pKY};bp#x@!?u|y?(mp?(ywoe&YN4 z%VU0{eKXnlxN==v0k3+#r0aTOE`2cPl*hlRaPB%izT<$^-2C7tU4Cu-|8$#kJs;I! z<+sOo`fY!IedDvHR`n>+0qZyI;hY$4k8`k5^&#B~Kk@y^77Pt{=)L>dX6Ye_y=UrGAV8s=4hye$wUF|9WnFC*^0WPpp2An!bMe`uVAj5AA<^d6;@)s85{Ot0%vTE1k@n z8&-P!;3pk^?|h%d|DWXek2XcRd`*4*^zr$uZasBz>ZxbGibwf)u+r=0^}~wqxbu4A z`-3m~+A&kBy5DEx3;$A|F2B|F^$;K8i+ecAuXt6yT9?ldFLlKH6uUq9%b%SywW?pz zK7QEz|G`xH?)Ugj7r)A@=e+teFRn2AlBW){Ud72bH>~>P`ANs;#VgzGGEuz0a`t|$ zgD&6bz~27WUm=~F3avY;-{Y&po-RN6&9B!!f8*5ZeC+b2ri9nKKgshU)<-8X=T*=A zXk7Kn!%@BD@fl`4V}4qf-yb=+ZD{pPM>dA*Ha62p4j^c^7Qo6Q?K&s_=(w9 z)#dD_fK3GQrG$7qdI+vJ5iH#a}&y1%&gxMxnS z>f67uDbYNRS=JF zsayH251LO5Q*T-ETJ?KMfvB#D5)WvjQl~;#& zP`*Muh?lW^h31Dn{G@xk)rP(Av2psBIvqT5Zyh{+Jsj1e+e7R1cv&CqGcQaXF+F~Yy??UZ zeK$^2_lw)?)jH|o(}VW0{hj&#Gp_nFuMcKj>~YOcbNamaLzg$_ zosSL2HATAk^dMbz=saYd9;5@Sym}9-etFo_sp|S2XT@`$z)yVpdz)Q%n_8WZ9beWM z_ANerar!B*&-Fd?=1#pTk5^&#B~Kk@y^77Pu0QkYiTuQOKX}0(H>UIPjn}uBKL1jW z-&xmZpNkVy&s>=wjk8~zSn1(a_1^sY>@$AS<<|)xIc}n=pVEHre)Hc;d3?}=^658y zXihPtvus?eey`7Qz-n&3@{_LoQD1$*)T+MmjHZOg*VNaaew!!vH$9ko%Zk^k-|KT+ zus1J1>AIf0<#p{}>U8j`SU%A_{H9Z3_OCUjKevCh&vC(O9mma2`o0gb?XSFOYW4a3 ziO+0Gcx&C?_PzP&!D`+u&97Cz*XKB3HK*%0Kk06Mz4O=mPOZ+ze%Cc6Jm)L*^Lm2M zXRov56H`w;^S!v&rw)61Rh`eU$K$7U`Muq?i{IDW_`;@ySNZKc^4I$2k*AZ057osz z9OdJ|o^DllT(IKtrSki%|LxXOt2&*WueVP2>9259k8XwZt9ma#+HYRi)8!|9pYOc? z&swMY5w~d!y7;N@JleFgZQw@t0#`~@f-(Cy>U$pLFP_->kf#Sz4=?khaZfk(Gw!WJA6EU2o1b|0_tF1)(6p5Lb*nG*@v7^$ z^Tl8D>gyps#25E)l%Kqed+Vt48OkSmT=Ua<-mg02^7i{|>Q}Bd@KJy2=KfZvpGZ$0 zA69vF=y*r@70*0$duZLnN}r$V_UEJSea^I$`Y9XR%6NRuc|2}%Zu7{~NlZQU%=hA6 zpE~U6RdvS&E1vzyPwVpg(VtoT{Bqg94`IE;oL4>bqjA+Q4@dQq$7h&zv3;0+%fJ3+ z?Rw((ATPUr>!fR+(lfX7r7jQkXDpw3RV)wbiMJF}FJt=VhVqGg;U_)c*F5C2?TX=i z{MYN70$u#n&AOQX9#`^-m0spYV{<}%)|H3qqo@y7eLnByC!YJ0KR9La{n%^Y))dXD zUmx`N;IF*<8Ryx2^u+j3UEIS_J|67pR(1Wb;yFHki@iVi&nGwM)c@)!O^Ggk>hm-G z)+aAcOucHIW$kLS4gX?=cOeEkz9s=7WrvGv3i>g(aCUbRliVI4qT}W#9e(}!?RTD9JfG^v6Zh7^(}yokRL?%~DAKL?S@4SuToJZCNp+{#+eC!>XSj{KRt}F4_Cv*Y6{n zKmDuX*C69-`9NV!_J$i&WFC7tG7<}>9259k8TgG)8l1*w9mXSbu#8B zegA*)tM+OC^8FN_@8E6e^~8MiV9vL!c&+-qKF0y8dHKapy51kW?W0eaTAh!JSAW=w zuIJm-ci-aiqfQ^vf%5pU%B%NKKg1V9`NUq`{7`?@$4~Y4D~~HH8+ScwYE?gfpT@v* zzUV`Fe&dThKgcK2f$HM9SoO=pxw`m#hVqH-Blu}upEn%-)#pwv>gT?=G5Cp}y1Bp2 zk$%hLi&IaZe6?=oQ!it>^7>(}mw9#lFnMCW@l)P);vIK-#3ZS{;}6;jm9F*aIv#m3 zee2PK_^`^WC(?yk7t@9Eb5TF6`qlYqKCgGqJY@0v5NF+@b?C!SeUBUd;(ML1u3sId zUX@Q?#=SmunEj)%epvN6-t^n+`QtzFn5k9$@((2~GtNhk)e`lZm3i0P6y}5nKpR2=fSoN#(6VK!Q_)qUM zjiPQYJTV<{h5CBf)6-wAb5rMYee=R3EeZ=DLpS=6DrexoyzJB)`$-{SD z!B3uf)y1&Nt3x~}KZ@o}e(K@D~A3)sruV=7#c#_8~v%`~32p-#Tq-QNKF=|I6z6vhtfgG@lseoXd{q zuG8Z?4%nNQpLF@P=N)%j@OvfRKd~=UUw_u=L-Qr3-m+usRl4R=&sZMPgB9QYOus#k zKKOyVOp@vse7>zf*Y)4JkZ$@dRyP+tF&(Hbu5gs6A0NukMg6eq_x#0A$Lo6XUw_zM z`S|<#?;O&U@c5Scz5Q)2NDro7l}}zTP8~7q^;dPr0V^I~_-S1}KH)xhom!obJN&FE z(S@n6-+opXd;I86#9tOVE;y=-539bp^YfFg&v)+j&##=8Q2%(l9pC(Wz4$pt<+r)` zu3sLe9$w~qu|Al5d05pmpRu`N*2UHR3BRvB{GJn)-~Qf}u6;@mx~}t8UZ1?AdAO}rbtd|aCIpQ} z3p+}Q9$HbM0tku*6ciDWD8vplD2>2Du|xt&2pW(^Y@%quE^3fakr=HIo5)pDV&p2p zG7>e$5JM454Y4IgE$rfbo^S5o$+O?bQFDH4FRYt;`RhGnjCZ_qeD62cTx+kr_BzW# z>y7Ho?ZdG#Jv{h80Wr>pLIpnRgoJwL7I^O`qY zb^g?%zSHXW+v8P!SLe4rb3u8Sb5{A}_2SeK!(M+?cO9_Dx0S-+lie zUUib8ISVx;Y0bk=z5_3)b+UMr@H6MJKdwbAaOrV*|#aBkE!o* zgO_!4Sr1P>`FQfM%4fae$wPf`G;eOGKhfi!pY-j|ue@vR=Y4#?eezP*edMFM*!`fB zh!54pJsjoZ!Jcka=QFH$&V%%u9~a*H)TvdS4&7?b%5VHcdeAzH;(B%F_Twk!{Cwpn zADk!OKV+M!Mg7~yHU+x)sayH2&psC?re3v9@N*VXSC1t zz-pc9^$EZ4^sa-ZR`=r_?d#e$eShqte!Gqet9cjATW48e11ued_!)r`L%`T(sTP zs(!`!O$m>Gsn1W>fp0y1@`NkYFZrW7t_!A4#{8u3ocYat&Y7s{C#`-E2+w^< zz4TjMUk~vizIYUSeOb?VbR9m!s*f-Hw4V2?KKq>WrdIU}U)hxK?9nhVe*3)70k^r&L{iBtn z%R_wFyI#D+?3)|Y%RYH%ZYZC~7k<+9^ZYqa{OzeledC9k0$u#n^|;$ zRr%zJ=|cHLy2;Bp`!c2j)l-Kr{8X1eY^U}w^_^bQl#)ju^0V^W{-%>i2dazbV%0AX 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
&(^d^|=mM>GPYPbouo=XY4+; zs?)&}_twGF*TYdgx)oZds`v7v{pN+0E&e5Mv&ttgWBM@bbk$+< zdNCbX@tqs|w4TrV-03lEf4}pd)eol7^*Bg<_mQva^dTK6j}NQ7dJpwOd@+C^N2J?rLipYZY2Q?JV7B_==XVmv6{Lv!Qlhw{mj=cl^;{jEP; z`~L}iuzlQW)BCZ`BRa76`ouaF(peT-XRh98AD>~hj`KJD_W#vA_<_xDp9k{y^K`3u z@;tG=eeHherw8$2l~+$3<>SHB8IAQreHq)I{B-?ZpImUz+RuMqxqEx$)8Q+9C{NFM z(eoRh4z!+l6zRe2qYJY>8tbptp~HvV-~2vw|8^3qpV|KVqwaa)aYNsF=E?cwAwI0~ z>J|3-vYv6Jn|ap*vo5C3PqF8_V}G#yM7@mzV6_pCPH!DNeLYM)d^)4p>r;oLy2&#y z%(~b<o=bm(pfexs^9B#9k80)KFv zYk%Kx>Df(*kEw4i=Xdh(-6#CynO9v5tGqhIgYu(j-sGnq9!$QNFX^}6r@i;PzcW$Q zuRNl4@+a4&>v4l8ZytI5Vs(6|F0L^9=~sNZu;TUd)`iKJw~zU0KKuTad$0Yz_0j+T za<1Rjg*oq{@p|<}`}qm0d8_^H=gU9)=N+e3_v6Uhwh8p*JmTv7wvXHoe0n*rdNqI6 zt5_aZdh&SY5ocYD2P=NHzvZ9!!YwCBexG?r;o7 zPA~7eV8!>m#ZT*cfABlUG=tTzZXa*!zW%QEH+^$Kd05T6-u$Bay*}3gt2ryb`E}XR zyH2hAKJW=m$@=ZxbG7x((qVNb8B^BGn=*UeAswpS%9D`%g0|EX2|^>=HEbnWBRcOU8T-TkqyIz4gf zsb_vPuKMNSs9y5)SM%fZfuHz%{N}mMA@w7_(mL_@M-TFozPy+|okaX)A^o{}qka5_ zy><9Wzx`HzW#z)tUN*I=)4>yuuA{Gqqk7g)q@Ve@@u-e@VYQArKk0k_`@oypFXa9H ze&1&`C4Kg3>iap!ex|Fh%HyS;zRXwgC_i}_SNfUP2eU4A&hpds@@x0oKYXIP9~&Rh zI+I5a(#`et$wPTa=a#~`>-6|}9o5`)_$l`K~-UUGG=@%70t?_m?;A-IVY=4pN^kU-6;2#EGdlH>NkYFZrW7 zt_P-$m|y9)=gSvt|Eh_q{;-d?PIFZI+s`xnww~C!bQ1BQy10jH7 zep=V_<-`AWx2aV<|2~axsZW=`>h#SePQ-`m;!*7NWj*83bzBdu`qcSpJwBfJv9-T% z$nR&x1~>M0MD!KkVsI#Jz^mG<%EO@B|qKYIG@->jR*;{qQ~J@qo5bupw1 zV*8ArbiAHB?aQy7sOq0@FUL3iyyJe+gVp|~Z!U2non;~Yxq730 zt_N1@(Br4reYpFl+s;!T{dwN~vTvw#Gp+n@Zj?&@*9 zvddrZJ5km1^|y0@9{fM;x9c!Zg?O;y_40g%6`wEsw662_0bf}A{s|pCac>Ja?TQ-*v!h zUcT^?F2B$GY4Awy*_o=)2r%yh853sr{B)+H#~Li>yxkT z+?33>=<}tyAU#;kyWaew`n^8a0joLfx61FQe{k*R?ep`Rz5ShabDImw!<@IuC$AT$ zju`g(tGere70=^7{q}uI&->7JQ>)KkUHS{{nUrodC!gIk1a9Y3}5`-Xj*5+5Ob zb2%^6<)OL6c=D--Cl9N9RZqSc>PyUe#^#3EFV@FTvHf}Du}4o-^*g`3b<*V%J(zyW z>vN8a6Y-aYu8UsA@`-pb>*CS*`N~ha&h>9?dEO+cKKl2rbn|}D@1g5h7Futv-e})? z{r2~lXUG3{WZs-3W4uJXQOy3iv32J5;g9x@`dyvh_UA`_^L5kG_V?#k8_eNcFdwYW z6Ma3Tvn;gUT)ol0QNQu{NuS@B{OQ`i=Qb~%n2xwYeLYM){7P4Tl-J|W)iE!u^wraE z=l7v!w|}YMba7KMPj7$YLF;*3iPfQbZz-I+PLH40QO%9VPr82Ix#+{cHnpnL!K?iC zelFfzv<}oS?%^mO5B7AcIv;yF&JBLz^ZSfXuYLby*Dp6^`u1tgLD%aOeD}xsZasBz z>Q(vViSeO)qB^YVna|k#FzaIbF#Y!a_s{O|z=^6p`u#rqw{J^(f=G9^9Rr%zJNBMYA|6FukuQ7$nO`&>)iW=w*7JPFPx?MT`1S9X{#ntA2Sns+T-I!>o&)qx`h4zbE8ulAi<)sMWQDe1$f@BZl*tDDE;O21egUmeP)t~~73@#sQ%Sk04p zG1Q;Bu9KhY{5<&$_n%tTcU^s9j5d^+x;n414SFlfLK6{f>J4)T;ja zZJH8Z<+s--xzF_Vus84Ay7g|UK0d>09dqzg%pdNMF!|<&c!|#0{CnfvWT zV|wx#%R{!Y!L z*IV`3pZvskp1ku*+f1$Q$2mW0O7!`f`p$p%Q(dfXJ$hn%s4ni|C?7A^&3JS@diWW8 zT=Ua9^1pY|FHWuMqyJxq{mb86U*39&_=_UFMf>%Q>hKlnPqaVzNnd`~6AqqQ)Hl4V zDbU5Mbor{Deb$o~<3n|E4@dcUu%}zq^}`;IpRU*Yu~&Rxo2gZOi5mi#ZUd7-});|y=tB0Rk41EH#gRonAeeYvGw>VcAk9m z)f*>JYDf9&h0ZVtor5oDYn1wbIP-)5xu{%@1bpCy7p;ahjWI%_)y+@^5WD} z&wR%EVb(KFU41b5RZIusrH;ojKh^p5iObr*+>c5p*TGA~OTVoP@$}1!dpOF+gFW4< zZeCdNoGbjauJh!>f3fS-s=ocxo02|!`eNq=U)9Yck55k>UmZX5;tJ`}NgiEw{N!bf zFRu^ECt8o6boh197ER$t5~~7u^m^;y>BAQ%s%M{g6zNv{tY=*HW!}6n>tc0&TF<%s z7YA;bsP4x}_h_AX_9Z>&_PC$&`g({D@x^no>X(Oeb@BNK-r0r^wp-sM?Wv6zVjbXUF^KEp1c?z_O1gjk#6S2bfG+~=23_G zp?o4=_(^}ORpP;iK5P1y{eOp#wgO$4y6!_>uh{^YfdZ zbnU-wc4?p7bU!w}yD8J>t92n={^C1F)UBs3rUTW*JsjoZ!Jcka*AFY6>*lBH<>MYd z*nMhMr-N6;@`?IJG5hDn)|uOfKibb{s6Wx;nxFLLxBK+@Q;Yf!&ut3!t;a#?x?gze z=CRMMhbP8|RbD-jUgk5V3)RI?zCzapt3JN)6R&-0aAoDlceeAyd9(EnZN;9h`;ga7 zAJR#zu4}z{>&(^Z^{K;3-#+FiAG{vC_4mbw9tp4p6 zwO;qdx$tj_?O*zHf15`ioy62r&wMZL^{K<2UR8Hpu*c)4b^W~k+{>E5>Zi4jM=b64 z#nwyAdDS!Di+g?Qu%}nm`3!qJep>ggs}2A4AKHQa=G7Z;=W82-E`I9g{>Hbjy)KeZ zOg;6?kH+SN`cgMzydLU<72o5XpXTv-wwkVz4LpoSFQc~G&;F{ZykO7cc17ark*;T9**+yU{ANIn-^BR>h+-gefc%* zofP}~z+;;t-D*zfg*wdhM4leZd8>T#s<`TxhgE+suiy1leEW=__+Ae_@9sxUt?I9c z|38g?soy(Kaz5+A)LU=7sP1T=>w(q0)p^3N{daAj_i?_{$vJxKWS{;DQxA_$4@ddQ z%ec3Wx_O~|B7J^}{r>V3Z#raZvA^H(!KUCGuJqk6JnP~+$E=5^jt|wv71FD8vX3sz z`e~I9o&B1v+4Ud_(mVnbscoAU*+-WKz-s-qzAK)F3kF9 ztRGf=p4ZZE-&gRp?;JL@s?*8&Gsa8A8^!FO8(U{?AO2`RpP~N5%5VGo_|5J#wWQy4 ztNl&aKDMs;6Y0t0&qe)kZa=b;Luje~{()D@!i@txr)T+Mq!8pR#&@&~?93KY!k#TOmF5)T{Ez zqm%h6PTgKy=~x$5Jb8Xv&-uOk58H-TKV#dbNVoFaxlRvX-g%RCvALjr@hJBC)ZwUZ z^7L2p0CPa1>1etOX32Gad$`E5S)RETFjSn;bo9a!<`^AnHX zKfeEirj+`f@7ox3p+0){Z~CpSFJt-C!^^z5huM#x`Hb-rlV@(2e6e$ypL9H5zT(Sk z|9@`Zf091Z({JC4VYR>M<-Gdj#n$VgzKW*~E1rC{zW%IdjF*0UfAD=@x#zT$`o4E; z&gg^Yfz|V6A;F#o!@+V?7>@4t^B^cJ$-Ka{7JRH({Jm$9&g(sPy1pLbuNSSesLp6VpJBBgJ${Pq`*WZA*r`?hoOZl) z&y#9@)9+z5@7%m~=Jw%__VXF)PqaVzN#ET)KJ-?Ihe#$52oa&jc z;!!>xtn_+$KEocLpVsyJw7c){vZ+=5+SP-CE`I9I-QVQ(vViOmJ&6D!>+pZ#Kd zn0c{%#!vJ4dH#EEY>xo-J>Jkdoy(9Oq?>i~cwC6_#B^YlSBK^wjp;&tiK&}){ZL&W zbe;T^U;SEwm5-mW?bPb?_?N%2Dd9Pf=)<{w(}Q$il~!&OZQukt(36MXkqpZU~b&RgY^HyYDR9PLk@>wsAoTaTY&?|=X9pY1SF z?f(m2-57ksrw6N_ADtuizkYhvyz2CONEffCJ6E4Bq@U}mL+kJpzx`HcW#v`RX@3uB z|8KEbWAya-Nk7-~`kYQ8K2#UaMb`!O(}nVLQ9rEuT{l0?!>?a@bqjA-r`HV;PT@S4K=;!|S_kiy@@wBPc{kUrPb|TPqzf#|MVt?aT=ZU&` zQm@JVE8TZtJ9rPY=2e{KnJAcX1*e zs9!u6^+Wx0V||Ie`IFC2>-%}H_ESgqdKk&R(kUMr0@KG^--^# zDC&2Ozjx&u9XPtbt(Qn=SxA4b-e@16VYQCuHGa~UKke1Dq-k*<8k@}tGiO=uXJ?H3&s(wR!eh@$P zJU;lUE><^ZBK|Ff)*IFD@zr5Zm!JIediXoX95=Pv|5vwPuWkBy-g%Vz{B%yJ(>E8C zhxp6JMfH1qt^-zc+i(1&>+@{4JK~O0tNN{fzw?uwh=26VWnR^UpgmlyU(MhR_~9~;h(s-4xYXq zrXD_>3P*YRD}I&NmuOz7o;rSxKk?_gPDynmRiwdR_8 zedl+deVTjjecylf;`dKJxK~rctNhOM+xq&f#=b@{^ZwZyj~l1LYI# z_H9adUQed}T)*kT)LV7jTc=mgdD8Ey-#+Fip7UhqhqP~^sDJvwZ6*5E{?7A+F4SMe zc|CZUuVVUDT|RZi5U+>UgH^wM$WOfXTWM=+*ZI4}-aCM~`YRmOqnk)K^P}pA=6i9kFY6ii^ws$c?{nA~VlJ%?e#Qm!L)?Z=Hi?sqZhyLbIgXeg1M^mM1Rk3bJByZW7W8%$MZjZ z^3-4E~Q=~nYp`x{?>h1I;B*b@!NB)#>1gN7vDZ59Oge{Z%3Txq7`m^TJBMI=}7vvoCwt)avIE zmp-^DTHiUJ`qln+-m9CB9!$Me#mnmV`dkOB=5_u2r0es+i%+}%)M|f^{{9X>JwI4q z>^_;#x)2{$dG%3D9Wfl$NuKL~Sr@x*ep=V>Oa17|r%qILeR$&DI>~dN=_IC}I-ODM z^<_Qd(RIuVt3KxjKdtBcKA*kQu2ZY;r=0V!wnOQ{)aR!=l=G2)&F|! zhN)Hk!WItu?Yy==A7IWYkH2bMR=?NhI$$-o$2ULedcU*f1HUr0s(*A{5Kn*V+uuFE z=_ID!+?d|nzT}VU@EN9#m|y&)&+i?7AA(aq9r+A@I(uy`{r2xm-S?~QCq3$P+?Ohr zPt*s^Cr-@%?TzWr)#>%Q4%pM@CtW`;xcu9^mX|Ci6*bs~Gcc*usZwDIdw$}}U+dHD?<2o;J@Li%GwKQT^{CqVwig3M0%t7eb&X7 z$4k8`k5?gmm^`{L>s6e3`eD_lp67|b@AGTdZa?3#|LIt-_x_TP*{9$7Vu-(LTvor= z=Q?0DH^2Bv*XM(8erh{N+z&c<`sJbX`{&JX^O;}V!ycc{u+no*^OLUpDR=q3sa5@o zPc#M}og1kyj|aUDbU&=8E>68FpFA-=afNgto*2qk$Y)sfxo&>q$zS-&_DP}ok?o%! z?S6jZe&u!3hdG~obzR$=Usk`@=Q?0Dr~SrHx}Gm@^vw49#Lpw{dR9}Ui=X;*)uFum zNhdM&^k;ri#UZlIM z&S<~ug4KG?+1%eguR3(Ewxar$YqS%BE=+y*%e;7euIhNHSLKsuJ^75~AzeJEK8mT6 zeRM0oeg6I9Lr$1l`AtW^*m~j$^+A1NC|~I;o2Ngwzt?A8Sm~?tldk8vUwzc$r&jf? zS2Tt`_bc`7V|q~D{-y)*Qm@J`DSvfi6N`TVr5{e9*C*fLSiTz|pw?e=xT z^8Gwr_rZOmYpyDtg3DKdonf ze)U0TOjPv`zPokeRr|X-Pt0RoF+FI$s|)9@)8o4iSj}yJ@{_Ljmv{Pp`@mcMto7e_ zrR#B!`tDbCp6E}se(J3%w%%O5UY|Ow^ws(4dhO3gKI=YHtIrSV;ECyoE7aG+)WffI zGe62tUdE|g#pZ<-&pza*^=`i2@ca|nBf$MwfBC)U9LfDF_P7ySpMD~}oEI)jDbnwKzb@24{F!k{1R5;4hpZtvRdT3r)@$Ex? zTF>`mzjDj=%;tW);%QCEeZo(Dj~hI7^T<~*KCISJPfWfT;)|g?l!x+(t^=xPpX=tQ zI=}w<2Tz?^)!%mg#-NK&AL^$sFZMjEpB}`Ac;Zp4^z_q(^03OQL+e2KMAy$x`aU20 z^Sj=8YEi%9rA-0PxtqEkH}uubqmND^K2#TvVy{mfj_M}Qb-}EQ)%j^%=l2PJdeB5w zf7?x3XZn?T)&8cRNY6a<@G{Q4I!qn;xtKZ`)02ntiQaec({=Fsdxzd(YEj>OY*V0{ z`+_>?ECi5y{J7ws(K}t(WUm@hBh9b-+ru%F}@rPoAIF^Lf<|4{8pn zANGmXfmfa1?jwJ#Bi3g=dJrF0dG%3D9Wfl$NuKL~Sr@y0ep=V-??3tS9VhCW*Ej4< zFKi6D`1Bw@>EkR6Q>%KVlk4Cm(#MDL zP(J&&H>N*Vr`Km**wg1HUHkL8uWnzcQvdN=ni8J#A@%j!&+6v!xS*4Wzbw-GS@pXv zSgp?&e&X5RH@N-c-)B4H8|@14dVc5n{CA#Yo$j(e_4=08@Ac(%RC9RUmVV2>{!Q(U z_-}2CU z`k?v55dZ4Jx$E@!t^-zcdtB%K_W8*h_Go^)ABTOctxVTBocesWkMZT*PkC5fmwK*K z#g$Ix>B35{m)8#~KEL^C9Q{Q>SZ@THXJRO*NRX%wc_xjXf z_K(K;p}vgmGk&^$e!cf|o2ORygASfJb>$QF^)U7DQ!nFDp1z7-<@F_+7phk}{8V=z zKK#4KO|9-nrIYL6CDQNVs9x$SGgZdG?(u;SUr>9_np-hKb6Ro%Z2Xg=q0>g!Lx^+EHAVd||q zp1V$u?>b;LulF_lr0efj-SvCVnp%AyXyfip39s7U^0~e~s4j-pysOI3U8l#->!{|m z5BW)#-|s)9?O^pQZrK<;eSXu!x8L1o`NV48W%K5MJ)Puboc-p%Szxm7|PCa=#>KV(!>`Q*Hj$dJ}Lr*@@_4AXi{r9drwtuieee)fg z0$u%dv%jk2L3%Lts#v~4`c=KkSAA99+_2(VkDqw<_l|!rx;exbekJl5W?k%YoqqfK z5T8B&sEMk+?}2S)`!?qgJ0I}OA$H#2%j20(U0$r-Lwyxb9acQ~YJL3>Khg6SKk4!N zhllMtwWwe5l%_xzKXsi)d{oybFHXdV)jH~P(R!(yG2JS!P7lf_R(|vAzPq+xAmO+E zT)($Y_UW&1RF7^Ct<&SF&*hsJR{b9L{KWHm@Xua%&uN6WtRDb-Kcr36({)bS$9Wyu zFNXNA%Bxq{>r;2#u+ppYbYPFiPwP2fH{W^V)ariF!4vn^!PD2n)WfG!;V4i4T)sZ2 z-@GvEV(;_#so&?{r@ruaC#w1dk7=EBtNrV7qfR&bGOunvb(nc^V)C;tPF}`%u&SFI z;wP%}lb-YY-H$naYEgghXB$HupRU;R0>9Pev)}zO4?aCQ>hgFHPdyP2I`^P_h1L4< zF#FAA9e$e6dGhT27ymuR(a#6-6}p16Fg}SLyeTpI=sv{l|Ge z!~dA|fBrqu^+Nu9XZ_bk)-F5en2F(jT)9WP)9$zPA@!XXxxdBsKb^$XQ_p-AkMi+g zrPs^z8CHCL^V7Os4<7NImrbqem+#(`@G8Ig$zSX1>mmNC(0X(AM*H{-t95#>Prh`w z#qXaSxO-EiTg~G>@Ec#={@1S#bKWYSJn<+W59*(b`eD^y?QgFKZ}zwC7k2FLOMba2 z@ri$_&rg1<>ob=)5g)3HdpOF+gFW4J?^R6=y#6 zdUf-|>=)DFr`Z1f&_B2Tv;Pl1sdeJn$Mj(JdeA)j=p@ov71E!pH`?dAV6~2Y$WQv- zU%u!yubW!ck86MKr~CJj`DuUCh2|0`=Dg~eui{ZY9<20wdDjCgzIFI1_ImISANAy^ zRsD)Xn-U&hQ@`5Z`phLxOg;6?_u^ikI_&9Hb=Lzco^|*scAoszDed!E=go0XY>$ZC zxAgTp*TvBN@w!N!Ue2qY`CLz&SoPtlL-`(>3r~OM)gfQ_iO-MYzqN6qx*r#}m*d^@ zL_Z&`M<3>#^7vO5&RwU+cO9^rn=kyNEC0CPYCGNiIPED-iEibmeXLG5`!cU?UUisx zabohbE>2#?c(AIQ8{#MG=cm|t@{Ug~{(hQ%Jif|9_lw{1`g(}Jy=a|fbw>O746F6L zU&;O5{*}75wSAAj`_!sVC+Fy`lYROt9Mz*+A^ob}%a8V(7goCJ{G{*w&W`V&H0Suz z^O2tw&v}r3dp-F654FGisJ?N(rbO4{BlUadiGJ6Saq4YvY#lhNhYzbh^TUcqho54v zpLf3D!=_g6_vzq?d+Xro!xty2XPhHZQDr_8~v5=l$hNPF?&w_UQc@ zgD!sRyI=WyDORVGn0nPZ$?L_|!J`Z16MJ>KF#E;)&i(E88*cpghfGx8$6srI4t)6a zcg`s`7nFzQTQx4L-|NfksOGlM_(|9Ikx$wB(uWwS{>lWv=y1Dd;tqbv2jmzry`dkOB=C00f`S*SOE>kPNZ+Tc# zqFc@3epP!$~+d6&&Et2^3<2dg=&{mt(~U%u1S%I_C{uPO1#K27~K70Ge8=Oa^4na{x-jRh^2zJPsnd&ldg`tN$|u^N{ItHmZ~ugE z9Y3|GU%Y-W<5lNL&u{(a6T_Ty)$!bQdVJRbt9jM=N!RP|Xa4zfr&h1ePk&syL;CPj z-+5$zGI!e)3yg zpLNBF_^U$tbM;31To0_)QRgRp?|0t#f)l4!^{qc_O6ut+z3TNKzUK|=;p;cAy1uOA z$>;r-$4kV=gLu{#XFcaqm)B<<=PWZ zb#V_z`FOCWTh;m8(^2QA*m?4*@7#7;Lfv|JmEZPj-e-M1v<}1<_i&Vt2Yb3zo!_wH z*@yhJu6_T|KVN)(a=|m35?%b%=eIgObRWctskf@wI&<}Ued?~a()YMdzi+YL@Sfw2 zo&HaKOZ)YyZU4S2UsK7IrU|}7x((qVNb8B^BGn=d49THemwJ{_LF+`^?yF2yT3hd z@;b~TFNW4P*Q(Jvb9H-t>af!1D?eSY_dB=y?|V+I>OcNZjlnNS-+Gnb_?|bcC$FEL zdgil^C!bi==~ngB5#vKTFnMC<0YCNg>(pyBC!VyPIQC|ZL6=YT^n2Z+E-y~M>A}=;)7lS+-H%Hy zXi9kQOX^qq+kV$)K6)_qs(kW#aq5U+ufM9h4p{MAH$ScG{rvBLsr`K3{kY!YO^L2^ zI`!%Dm#)4lkC%G-GM}-2nDvb5!mN+R`eD_l&QEjl`}AE-np)kD(Z3(-btYe}Prrxw z5MNy3C{KU#Gsf#7A7PKrPwP1kE_}nIr&jgNcWX*`_HpX#ulzO_oy62r&-`dy^~=Li zz2vzbn02vz$WQC~{^0X}viN#XAKtQl>#xu{bCKTMzU0r<;WMoI?Z5Qfd9vf*muh#S z^3Q$ZGnB6|{dOL_;8*sTTGaLD`lIXU>*1)Ld3tEQ9xv;oeddL!BldcbpY(lx@~c05 z_C!_B?+<$2NDsPS9zXK*%?0Hl{;F|V{a&BzfYsd2Uw+b+zvSf8rdD-2c;enVc=~#n zdiZoI9Odc9hw^jLys+xG|M+QL`|}fDId*DQKYjg)6mQx6t-r!*-nn`DbNlc|`}qv@ zC)&sSr0?hNzjd#NOfBkPx|he&YFl)&1VS+q8uGfj4aoKflK}2Yu)|@Wk$i{HWiq2U-`(Tc5sqBERvn zPhOw-GjCmpk0-6}p16Fg} zXZ)n=@&1yx95uD7?{vqeglB)GKHZ-;zs;TV_TpY2pJ7jrpLF@Sz z_Hp_xf5t!k_SCAr?~|L7eVcQLoioWZPv*ty=A|buR<95bR=m{h@vAx=Sgq$e`AN_D zeV;2HJhiG{`Glr~$Jf+%zv!!*M_x=1;;$N))$jGW4p_}iho554cNg7rx2e_VwHF@O z6#0cuU!3PRzWd{T;H&4n>WS8?Fm)gG<5jVI zqCRLoaboswZ%lu#POr~(z)D}8pLFfd9p8_woLkKEk$;QTt0MLE+vl+lz4w-htiJgH ztyiD(;VQ-Uf9{vZiMqa=PafjKDz9E)uP^HvSGt*ZJuvHH`ur60`^%3%c%nLQvQOMw z2ai5Jl!x;AR)zHE>h=1}3oCu^FZoH=>yvj}bo;5*{h)&iF8(l=~u?pI(V?+jm~c$@{_Lo7jCiZw1j_O&3brZ>xnDGb06p= z;?YqTS2)VkU-7HFzC`mv_0-`jKh^Eeo1fKQiMt<1uM_Ald!ASa(i59^dy($4I-~t~ zu(uvRU9a=xq=(&jYE|EI-KIzvKlR-&`xxIjqOM;Zre2j#UdFvXb(sC5v3^+fxn6#X zy}$gU6Bhq{sh!@@l;~Fb+w1A{Tc5i1|xEbu-4VkPgI4o$C40Idj>5 z51m@wk8{`G2+>VF>)5~imd8(|2l3}(>dx&?{^&e-Fm=Ry;iq|hzv`PC+Z|DV!vEJg z`DmZggYF}qIm8|p){__0f$HK4^;@sv$1cZawEXokaZYMY?l!=JvTRSoPay>9_Bny#0%>oJLhYp#AgF+umPRx|QGf z=BjY6Kcjr>!_hu-R(y3>@$5f-n#1}1m5&}bwW|O9;J=sQoS~O!E|~Rd-tEm>XRc1K zFR!E0ulBe7x#RDxSI#Zw`3)DVS4HafulwlplTRM9VIr#^dz;pa=N!IDvHhRNhsTe) zz8>O3eDPeY`sLwVU3@-5`P3EjQ*3`ipgOyIMgO^AjAId}d?BCv){#>12pLt=W z@AXCcy}pgtzH!!xQ>(uxcE)>}qV@HszWap-Js#A}rw&uE$|tWEr%o^K>8a;+$S30Q z)B4Vn2R*v|!lnC>-`DK<-8)aLZ(W#kt}0$uzt`tFU^TCEmY;OxuYA?xrdIW@t^a$` zbRm8H_OTd`|DLC_j+c6P@`+Ww%IjB$@}sEF++tYi@Dq>UC%yXDrWW-x-qIBC_?o)T z3;wE`tM_^lA6ie`!%;pS?CDl@*99vczxZige%}KF~}4=y^hy zj<~|)Wt@HLP(5S$3SFN*h%e?BKjl4Ne)_eKom$j4?ba0V>{q(beWWk15BBENw<@&m zT)keO>wuNM$2C9c`uybBw=ce*-)GmRgjc;jv44Nw{N{uC#Xaot`3x&P`!oIa`}2?3 zfAQx#YtLv7(uJw-KJpvSdg`u29j0EDPo8*`j|cV7Mg6eXpMKl7*Z=V^O|9y5_#_@( z2hV+=lZZ!OUEIS_J|67pR(12jif5nk)4I;zlV9@Msnzr8ZQDQhy6y8=zNLP3o|wlz z7bm9Pva##YpRs&}_^{%Q&d*nV()IJpQ+{dVw1oR{y$x+eJig@|?ql8$eLYOQ?TxKB zS7)?u)NlKcpY**x`IlGhIgO}(t@S5Tyq|Z!%{kX+{L%hVzn#xX=vRkyV3k)-9OdJ|)ESNS!>Z5c9Q<_s z{MhrQ51m@oH~e*D(Di(q`gG~x_x#3BJ^h*Q#l1dt*wd@(`m6c%Rr{OYFWUDd6P4c& zXusaQ?fFtaJt%K~o5Nh0SBG?9l~=E@*O&E-E8WbyE|~R<`Ds0VAN-ib^ZT?tn<8EO z)U)r^@q7ClFZHT?^2FAK@`-d|)<Q(t=dHU!=`MIbcR{fsW`04ujvE`5_POa*rufO&4 z+xqm)RU!VW(Ry=rdwuG#()YZ^PkuOmFa4j3zYqBSJ2pkS>8Evaf6Fg>e(Ud{`IA@W zlb5mU%zVapFzeRmYx-?}Uh8%*om&0A2_607Y8`c`4_}<9o_*rENI&~BpK1UQw>O+x-H*|~UsdgI`p|r0Sk1e=`DOKceXavmbMl*?biJQH<0#OQjzFLpZ5HEGa{1kgVc+JC~H?^u?Z$neU8}&Qa*Ka;C z#J{?5?m9ib>wwkVboeRudhk<6>^!xqAF}@bk}iJgdtUT>iVx-W%fr;e%Y4RoFzXrP zLG_H~D>OH(`uW07Jm>dsy!heM66$`x0Z)I`oJK662Nh`(xFR=?NhI$$+-_4>ql z^3m(OcxvVM)<11Z)}MQxn8)jWF+G^`R{7-h;?xntUVl}09kAk6e)~Sew>Lj(YUTG@ z`}k_jc>w8~o1a-XkH-Z*o_gwKKI>vg7s@Bn#e>PqIC*qn^2L?kUY~sIg-1_Re*672 zy87wC>O9exe#_@|&{G#zILgzH59Q~geyG2?9)7Cx@zxJ|-+1=1 z=LdOukPfW!>WOq=*2Q$8{9M!ztA2HUn$PQ#M;+LlQP026R-NDO7k{l!-@4*Ne5fuS z#a>_5Gag;X^}wo+K0n2No_5{eIAdy6r-LW%t%IkphpC58r-!5bh$s&QHUUZ3lL)!bgU@sqCSyD#7Fg$sTk`=6Tt?>yt2jZ>@d7aqM|o4_2^d6ItPThDo8K6RM$R{7+O#`K`RjOBai zI$*_j{rt3^*WaJ~L32j^ou{=9eI6gF?>^u`dH2J5>f+Q>&wR%Ed)KXxE|i~()`R-Z zBjy)B)xAFXtJ}BzZU5`T6X!beiTd!xiR${*#TAb7^jG{UuP@QOP(5{=zx-6c-Fm}^ zZn5X|e|{hKtUI&^=(51LO5>0Dhncby*Jb--$FzVMT-zwfhQ(}7c~`*Gl{ zn-bm*zndR?hLzv){N$ti@MkAAN7XO=$)Y|!9jM>_#uIxTDsN6m2jYoGvC`8|7s|sb zuMVvPJ$Ie*CcQ`)r>7Qn&J(eh*U*Kl8n~*O&E-d;02phVqI0 z;-~faea}>jdUPwaPF3&aNBhkSD_!R=Kk0ja`TK9!Ftw^*x_%(w zxnHU8emOtX_4N=R;)~~E)t~u{N7vyqtor!DPwVmfd$)Vp)T;iG^%shGmEU=um_uI= zb6)(+SMex6c^OywnRk6K>*C69?{_}E*CQq>zehih^|+yLJ=bF{aUwoc7ms4Euc}x1 zYF*a@@lr?3PqFjhcK^_RVMo2v$#w7&>GyC{FZDCFPUc7B(SGy7O5Z-@Cw=?-Ro}Vo z)T&MgPrp3$x~JOT`YWUZ@x(nG<>SGgZdK9LVcJcyxQ~ z;OXmO>fzI=aFnMXAIi@~^FsZpYk%@nz3u9?wflVV)T!0`&kZ+k&!lwiWBQP;{f+PW z)$OyMx|j}B7x!?Kj|Y3YRb4--Y*p3hK~pJMwk&l8_l{laJ4 z9dSSQS^xW0bW@M6`M;A{p*ocBVa_Rry?%aL*Za$_{Nux>R`mnh+n;TpSMfFV zy}se2I(>7A6Y-(CxWZAMetalD7hMn3pSt|ur#iphaOQ@oRsF(eG$p$D^kH?L=(Dak zke&IZcUQ`1Hlj3p{f`d3>>Y>fu543i05w zyzI}IuDPLn@+-gX?}J|2z7WgrN+;LBOSFCuQx8A&G9KmW!-w*7(Y#Q9>U!MsQ{DHG zzjVnxr&jg1{cF2Jc=j=U=sw!F$+xb2A|0qM9>rdtIvmwa9-m>>#r)u>b@_3~wc5$z z=l7?t|9j|k_0!Yu@u4oCeVJEJJv^x1!yaE9E~{s5Sm~(q)4E=NAAW~TQ>*(y2Tx2# zT%kUwPYmTNon`a%=l1vd%nK`hI{XxSzI^e&>^rrpZ@ouT!mG{`j}QCX`ufZzPQ-`m z;!*7NRrM-gt?POqUh43LpXxq8dDr6?e}CuLQyPOVK7HssQHSo2b>-IOoO7ytt}ozKYEU>E`28 z9^%s#Lp;dW^t=5Q_uAT}H+jrNRp00Nt&<*K=|Q@9^5)W~o`?_C#dERhmxpt8@m*Ky zR53r*-Pg}-S^WL9jgM_B(8sebbY5gV`^1TKpt;1On0-~8`PA>#d-Kz?57TeYmw&VV z{$2gY`?QsFf6{}_oAg_3U31bC(}DWL74ik*iJ`o8q5NDlH_U$PRzFX(KdVCh0ZZ%KtZ|n14olYV?tkzKz!B`+Kj4PE>v$*X-|p{!N#^)-!hx@gcr=E>``S&vhg=9>K^Z}`}9Fm>zNYk zLU?p@9r;9kJxo3P)T`oAJ|3*}dU^B09-p7qwLjl<*?v>2`*FdZO$jgeuXWs>>O3(Q zokTiNT|A1tzN}|Fx{m9CRUg0jX+3^_<$rCOTGi>`iF@nd>FeRB9^DG9Q`LL<(SGy7 zO4mN*Cw=*cTzj9XRekGCni8J-mHO_N{f$40skgncb?54g_VF23>-gM=pY)yU?|a4x zQ>%LZKKH2K)~DY?{8gd#=IV|1@flX@sPmJ)_nlXK=cuVw{lY!lj@5^k`ttTSzWZbU zTTfk_dg__4;!!>xtn_+${jlQmg`ck1=ig6x_TqVR%YSc5c>GI!=Mlg0#nzQiOg;6? zSMewx4_11;JfC63w@&))^Qse{)V?39zTupvq|dpN`jwyNagNbROg;6?SMex6c^Oyw znRi{ie*2A|`1a?2+tR*i@h*9H7g; z^o5`F)y*YOClMd2i|1n1pZSbO*Ku92>T}NW(|SIy`qBw^n_BJf&41C9=sJf}-+9DW z_3X2ryci#v|+V@^jJkz^b1w{Io8g4!>1<=2Sm-m&TxrpZd;|ydQL(WBTdA)T{EztKzC( z9#;Lmyncx9`oylApX%OUe&c0}e_!+IUucSS@##T+^B0f*>hkm;9a!bn6G!=YFm*;_ z{jlouyp?|YdGP`JZJJuuNB@46#|^)&&tGUhF~narF00?`a~-gnyV~Dgf1iBcS5K|{ z-q8Ns=C+>~+pno_|EAyOk#`-5si&U#DjwzI!Ah@}cU`c@=cjf3KKCbX{_v^Q`=Pg; z+?3KU`ueN=Z65atpPqW^;bmT2Vd^E1F0AUkymesm#a=)2)4Ke+&aHNvsO|^9>3V$7 zgSo%)Vfrm^eL8bdKb+f-Zys?E@e{L;-~2QuAJ6=F>rnsNt6EIgzD(WpTU{T&#fkXP zT;jQy{o=$*7q6=K=GSK*^OLUECtrWaGbXC~F&kPZp66S7u<~0UG^ZHSSv4-J-|KT7 zu$tTcV9mxe^bJ9E~LK44ZrcBb;XIPr=I!IxayaOqk746JuvHH`;4E~ zb)Fpa!}fpbUwm|9(A7^5x)1d5`R{&YT}%h+7ms4EPaTfxCQpAgKfdecC%&KWykU>_ z2YUU!;9(zY4EjC4`HSy9IN#-=_00#>#XYQaGEW!wbgR04Sn)jH@zXr^_i2wkacXrx zj(%`c!sB1+yN~=<*Jmy$5Aj!x%j);~TnDV?ro&IM{rBJBaHpwNec$%yIo9Y_epb(y z^z~;fZ(j98^&aZ0cJ3jq93n{gl)7t3L7L_6uq1H{G)-S>Jh-`aiGdi8;+L?qQG5M_B1q`Z{og1m|Jj(M%?0&codg9bm&wR$CJbn1qh3cb72UdOR{Is6e9pBsf%TufR zc^5S$efX*0^V{QsPGah*XMS#+I%3xWm(`_L_49?F)|21=8+V;r)!%>brbHJ%_4V7& z>iGIA#E1IC%Oaf$D_#7mp4U@$AS_5O07*B>>ts_*-Ut%LsD^The(eprv5IOkQ* zd=-!K@vI9g-6~H9_IUiX9>3rHtfruT{_?$_YcIIHAG-9P+R0AWIiI@n{Kj{G+z;!ii&IZM^Hn^`$Agt#FRvd~ zeCG*2U9Z>QpL=%uyh?rRkxda#f9lKQ!SvgF>M-@HeDZp6>WE>lzpCeTR6KcpTGzS! z$ge+cqS*iVc>I%x`tjx>on_H_Kdb)qyIQ|GPxy81iw~Yg=J&qmHzj(N-*oXHex4`l zFny}>$*bb3UmjNdy?pvz@m)7Rt;?^SuKTR1RsC!2?ftf&&pS6#pRV(puDRsJ>M-?I z6_4tV^33HrVAXFQ^AnHXUpi;;^Y69xvTS(zWay=%~{p)Qm@JAGSKafZ+OOu~yV$z0x~^5_=dRP^=XF$b@`ayt{k-ATryn=9 zs&9H`Q|jrvk9@VhK6&fOLws1})e}egc;hJs-O0irsN#K zr!Ur@zKWgi`1EpKb@~-nJo(hEbmT|-l4l)A54vuC>bJk2_PKVFtH14)je)119?boX z&sTN%L^@Di+`~~m9_;B>b^gMN$1i?b*Yo8q{>!7LR`tU^(UkD`m-@NC)vNtY58}fr zuMS6f$)DS&AMzQ>r>^Vgr#ipRdfVdpojR4@_Ah<9qiCIF(K|7je}3`&_I`(+^T@i; z{lZJMZsx^wV3k*|u-B&!E1h0GuLr-O=Q_t`FRQtPko|wyei8-%&=6i9kPaXF3s=Die z6^~#1w652KUpj1$sa5@uJ2fRd`#AM`e(SF=^_Gq4&+VVv$7fjetMe1j{{EZ87WcQg z@x*k*73#x>@=(6gSvF69Zhx=Oys*-@KhtlozxR0G;@|7N?R%P%_4TK|$BpwF-#k7? zkWWlK_00F;UY|Pb=~Z>t1uLG{5tZNPp4NUrN}W#5*IOt1^jA2lN4JO8>G85Y+Gk#v zI%4N6Kk3Uq?`1EWsD9q24^P}%2TxxQQxBg`g`+(E6~D^sOEfQ3PaSoBs{4JC!**#O z2>ShpFaCW~!gFrWhaNY1o{06)Nu&eS#iQ8kQ-`Cv$>Sr;y4ZE|)4IMtc;#nzpQ!f# zi|^eO`Q{v^2c7G9=8$(ktcNG21J%V9>bG9Slcx*i=c4ssub-dhb)M|{w1cNse_xgk zo_KT}eLYM)eCt&>%G00xjPZJCURd$z^Hc2g$te$9{Cjv8|DZAG`ng!@+sF0|UHRPK z^zc$oJ@cb+)h};dII4?B2g)bX&;8Bs8$EdO@7r&^YwOU*ztrVt`YrbOp_7<;>Y4Aw zy*_o=)2r%yh82%r>9_Oy)HfeGwW@#Z*rvp{N}r#2*2kBxVthF2N5zwem2Oqfyng69 zpgh#?`I4XV-p^m-UOP`M>Sx@qDbVE`UFe+2I(_{a%hQ2);tG?OarR|h43l5Q=7!lX zhWr#e51#Usr%y|$TMtic9dU*FdN``rTc@h`^5*OD)XfWf{rtq^_bFSSJV~n4#}ive zT%o=mj_Tz)bTdCUo~vVC*z4yfp7Z3>hwe0us=m*zZE`&4e_oIKRh=jLb3XahTUETQ zey`7Uz-nG~e$sXRes$C0{yuWI<{)0rZ#;teE;;#y=H&<`8kI%4L$GMUF+rGcc+Lo!+`%gMKM{k|%(_dlg z;nAsZl&3%W8RPZPys+YX{moD7`S%3w{Pp(tA>0o-cvUQ)sBaXre{O7@xqbMf{d|V{ z6Zy?g`hGrt&*#VYecC@4*!_O2$48zg=5Y>LFEQs-&wMZL^{K<2UR8Hpu;O`MOTWFJ z-|_DQHs^ZxgU_(?+w)rG_g=fSlSO^=32kM%xqq#r4$X^i|Enj`f%?Uxn0-~8`P3h+ z_js#EV@=zZf&6^wQPpn>_ zI8QEp%9ExRe&6)fO@S_-QrG>;x<2Q(I5G9MH%{L&uJrL>#T%X9KIA7|`G0-smT3v~ z%lB(5;_)r#(4T(mGoLsy^_Gq4Ufun!2UhEuho548oqf^fsa5^|*56yN=I`ZQPmfpC zNBj8hY#ns*=|Ja{{f(#3<3gNB2kIBkMg36!+*n^?Z~o-- z)B0YQZ+>HQPF)`!-{fI+e(UQYow-PNZePV8)f@Gj9zVstfAWeuwSN!B_a#2~+BO-U z`;zlkew)jE64QgJx2kwq{a&BzfYrR-@9>i@zb?7);(4;+UQLOv^Mbzq>O4``pRs)E ziJ?Amg`>Q={i$niSn2UA_qXTEH$3A>Q>)Kc=v3>eSE#RtIX8Z#TjhIwSo^yd7^!T7JFLpoaB;rGLaSuoNc(A8i)%gr7p7WQV z)^#2na{le7R`t#6--o~(^&8(jd>5<3YToV5FRS0{a~-gn)A`L$y7u2&-+Gs+)z2d~ zJhv(0xnHTD`y1c=Q8%ADOubddbJywdT?ee@Rp%#N`}@SR51U%uk1g%b<1PJpEPd-j zd05T6z4>MJdws40_U7a#UHk8`?|9(U>VEv@`ag_G*SSDnzsE;a$Ak1>>WQH~afPG2 z?Cy%jTEW9qr>YtmfnwKk3VFeeAubR(0oz zeGBPZ&+8s_x)4u{Cr?iu$|p}ACSN`g4`#iJbA2(a`uNIE>&owb%f;7&bnwKzb&|(l zI*F;LPN#>Xd_36Gt?K5570>fpK3_V2U;UQ$fr>4}%cxjN>B zy?%b;xeuFe_p8&$>gLB2TSr`>z8;S1by|~w>4tsi4ozJ5>{KWTq@LP|5>a>K{FJHJpQ^s?@ zat`NFp3~`f&Pg}*Rvph>r^k04u$q@2{G_|>_29QQPOa|8#qHOpw!QwY{I-ATTc18O zpBUm_T{w509^ZAqYHq&pldkWxUHMNZOs(q2-mEd`;-|iStd7rr`#h%j9JhAn}73%9@>fu+q@}s;Sf3A*sVWsbRji1)x z_vcS+A9%YTXI|2jl1CrXwV&n1^ywtxLv?WvNBMZLr(4ze4DnKzFZ@(@o_zRLcb;0+ zN56mKoZ>HCXfAQ0d7-*^E>`{WaIP-C>w)r#&QX3^*S{D2?>9PVYVmyf>2(60eOl@A zTU{SCpBUn=8kg1Y^|=mM&21m@ldk=J{$IRqYIQ&MUVmYZSNZL6)AO57Vm0rwdHQqv zNBj5;t97d9%NwsZy!3sKpZ<^Ew|st6G>87wcOUtke0BN6)Kky=Xk7JYKI2h+KEtZd zKIEtMe4p+6n+}*-J)e&Ld(rkizpYOnnokVzVU<^(i}axWjOBaiI$*_je)H3Mw^(o3 z|NDDQ|L1-j`|{=>UHsIy@71Av?r(Z9^{RaG#G`yXsDCc%cb&a{e&XM7yR+J z^+PUg3_Sj&K0od6{?uy!AM$%mi7r1= zKY!k!uFriEC*rRfT^}6PH($k9hZT>n{4|IB4!@goJs;KKT)%xj__PPLKhW#-;vP3@ zcf`JSf9Q*?3+45B-pD+Dr6&*diz`fBJb5}$y~5;)Vb$ll`HA-n>;Loa1Ab@vKj+Qo z|ELw57pZUle0?GhvtO)k4*FF-dHB7&I$eD*>ta~(=;1+p`;DLSUJo9A&BjpQ`*Dro ze&|nK=d(IpvGYp4=eIumMC)5utWTVX57jGudA?>W57p^IeyaQY`_(tR&oqMi=09r; zdLAFxUKJ* zFaFPJf1AU);zV_r_0d>A?Dg@J-##C_!7h7Gt)5TMcu!O0BR+jGU&ZE-cR$qW;iVoP zRPSNxC7&Lw>hcxOI#pf2{mD;!`}_5O(U|VX7k6z8`pyk{(0Rc}x?+9u^u%XBk{_;~@{+u7Rzdz`_IX!-#1dl$nfB7pf=C}E*3-MQt%j);~TnDV? zw!isFmtTMU)Yng~?#I#V8z0a4kox-htFEty_z+(_7pwlvXFR$NpJCO<7k*mL_kCV+ z_;aRK_05lJ47}0(Z4UlBZ|EfEyy}_n#l1dt*wd@(t`Al`e(}?~?!#BU)PBD~{fr+s zhJB4sU+laPtLvk$E}!$NyH9kWx)^3Yb@Aw`XCGc-@<#hzCqLM-@HeDX5x^<_Qd)T?6G1uGuE_-Q@w=WlRS`$CoT{pjyBCA#>j z?|A`_ztH|yPfWdPo#gdm>)_Fa@`=4V-5!rFKk?m%GoI3%cRwy)|KKEfbfEKszxbm_ z2kH}#Vy~~NSNUpPenY&}5%W`Qe_#LL=A8QI&o4b6@;leJj`_q8|LVfI>-6}p1NP?T zCtZFWbeBg=t=F(&*M;!tj;^DxhpC5ey$VNp`thOsTr@A#pStS&RQLOUCtZ23snz}H z>8Mvof7`n2M0u!Q>5S&-jq;N>+HYQ%I%1D^e$w}SpFcaPo#g75{-kx%#is{79O&bT zJzvVxgLELCcoZu={dA!`tn%v6I#52*>j-|*m%sRc#q(P~UKPtH>VxJJCuaZl#`Ndv z^!i)}tn~SsetZ3W|9^P-)avhbT=L6JiLQN``ttU(d7=I)PCfk0XFST&hY#iFqJCKQ zd)>!R*YExO+4sKx)arh0`chNmBTW4~Pt4(ckh=Bg;H92==Bs#=j|VHgUf$fW;;Zx1 z_1oX~y6_oOtNMn)-{l5#XcDiFz z!1MS>-BG{IM-Qf6l}}zTP8~7q^;dP*0V|&U$4~1zPcGVR^VI5oczuHBd`SH~ztyd$ zPA3r`s*6Xl*QX9gb(6*F)5 z^sCn=_V*)?eB;#We$dJFd+TJM{t8F+=qA$5{M>j{$GoswhaNx0eqQ{AAOHT;>gSsu zY=7>)`}>#93+IRR&Es)ly+r&~A^o{}qkXOmR_mzqlfLudQ}0>)_etpB@l77;=QsW+ z(peQ+cdp)OA0J`04!zvp{Mzr>r%kQum+aq^_z3CK)2}WM%_YW@Pdz+&SmmpF^2JbJ zV%9S@H_U#qK7NXQUbW-*0Xg;4kY4Aw zy*_p8!k%u{>As&thna`Uj>ep!>7jAp~J?gi0=$lKNh`+r^cdpLdKGy?#{rtqU|L*hk&C{qp zU)kq(nwwj~uA859y>9vEL)r^&_k#|exVH}8 z=y@`#N4LV9v*KlbZl8H!)i2LaF~3i`-%X}b)K5RJO-9!~&UxKO{;Fr67+N2HRY(_( z>f*zy&-}3B@q?dqohK*${^IYaee~?6gjby>^7O559`{L158_{4ICq^M-*v!hZs%$G z?eoF?pW41K=)5`ZlD0D4Y7Ty?<6F=E&pKYtTji4{9_8af{c};j>#6$fLw@4h-`Bju z%cfSJXKlPqQ^w<8>ZhN{SI;@|Qg2o9viiNgypC#K=QKa*wo`j;?b;U~Jhj^Y`<~wz zc$MGo7aqjtw|rvGtDgDMxayaOqk733_1ksx)4Ke+{TEM|q}4xoaa)Ogp3nL{Zq(() z>hkoU>rsc97bhk^>tZ}8Utz^FH>~>k%1?UyIOwG>nOfE9;E8+d;OXmO>fzI=aFnMX zAIi@~^FsZpTkUV(hd6!Thfl5izU`)_L^t)U<38HY^7`b(iTJQuN4a!2| zX?>r6-}bEd{hfUp1F!O1p0D^~>&hqQyy}^+;!!>xtn_+$KEsOd`g4CfPY(G1H%_fT zSvSb{PrT01pZd?H&ttT%|J@w3IJj&CDpX+9fUm+ccmpbbFRCk`7`sPEYR`stP+m!4F z{q&*p0#6=azKZdoxx^J#Jb9>pG)|s==sKW0)bE_-r@X(<_RdQmHnphxdo%j*Q@7f` ze0D$ddw$J%)vLU9Ve&Gjm;B^qJ^M1&pLwx5p^Iq<}E#1-o6;iz7& zLpSqtD)H7ejqkKGA z>GkriuczZ&;3q!6U-7}kzc;gQJ6_i4I|p(e=Y{*JPG5f&<3oJ$DAI@7m%14zzl!z2 zidXsV{mw_P^Wv%1{n+}aP09TFQ{Vngzs=)5iRr=AtMbX~#i=8Pz5c51I$*`~xaX&J z?a%i->C~y!?+enw6Zh7^)7Qh)!>3c>C{KUIuk!j5%?s62hu{2E_xpe^|ErUyR`=th z?`aIW`1GOutq!a6n;xVCtGs%Jy*_nV>GbmYU1!Djc;_d+^WdHkyj=dRP^yAD{*ZGZEVuJ`lr-nw~eee=43TYs}D(S@m>e&a#+!FuvA z^{RaG=#A#-W?#msmoXivP6yhb{FHYdyy6Yb8TVtq_T%HFzwcunev7RO&AVz`R=?Nh zI$$+-wZG**aGUn~4g98)>-W~lKK&J@9v+=h?Db_mNkY4}WgI>w#6jeaufh&zCztXYu!?E_`#Fh|l=ugkFcJ zL-XY8?~Lg{ed5Hbp7|;^AEa-;h!gSYied8Y;&5!50`DuNBZ~f`Ne%FcW`x19OvvuOx=k%aFeSCA-|6+AW z2UdCYQB0j)JgVzDVAbck`DuMWZ@Bf}HOJf!-?zu(YwCO4@LOG*&k-)%~G?pNx29?tzOR;QDgdZRI&QGUgntHWnl_4AdVc=E4!+2Z^83pcb0 z@hZRd^VfChgE^l({;F|V{a&BzfYsd2gY?_yRky#xi>6lfv-WLD)_4A=zWrPIZ7w>A zskdxQ_v-Ftx<~neWA; zeddLgZsm7-)xEZM+aq_KTJ7)m-l-|k^*Bg+}WB0ZRT_?fTb0kftsnz*ThmZL3V&_!y)D!WldGW0StGs&Z$-}B&9^%8yTL)G=ef$*r zyy|(kZ~y0hq>lR~51r3^q&tdq<|6&MeaWAz!)I9a+lTzb^M3xk?>%=KMg4&P)+XaK zzIm;eb$R}K9jYH6kDhv!SI39)P(G1v@-nXa-hZp9T(kxqN?k|6Q{0x zqCTik4CS+bdt>@@b$Wf~g_XX?JwNIC{j@(gV)N9he$0lZgy(*>>{b7zYd@>#gXR-M z{8i(q{wU9Ut^-#69>@H|v%f#`;-^fK>X*K*G3YvvtP88xC)RU6tVd5w2dayEILgO^ zJ>9CVA67iq$4@c8{^k$2Os)Seu`Yy1x3^C6?0Y(isi#h76nlN@a8x&W=7m|$n4i}5 z`sCgBJ$$0NAN!oxI_cu4p3hIxSAF)mI5G9Ab&^-b`XS!jSYKjq{^ax1`uw`$%decM zexK^Vziyp))%oq5Nx#MV%t;UCe5;C=)$jGW4p`00SANp9zc1Rk{eam0po3?h%EQWU zeLbWD@x?tH<>SGgZdKJf#=b^6_9#x2n4y*yHijx;~HH z&C5@^zW;sAv)dnxRrmWl_ANerv3;LB^LReR$5T(e zDvy_#{H%-dpnMO_ji(>VC(rrIPj%0CZ+iKLsa3tw$#w7&>Gv@8@KZ12QJ%i!XN=cF z^TLY1&DZt+v7Y>UKmPcs)#u}M@Wj1!@buw}6V%r%~=9i{c_hailni5_7)OTL+7Z2*MF!fdyTW_vj zuTLFT`h4LhKir3tUv;mk)#q88E^A77)&6!)<5{2obN9FYWijU%)fw&MbC2)3`HAm7 z{PCerm_~CyUh&8_DIQ;Qe)o&N>gJNClZX%1#iQ8kQ-`Cv$#Y#W>tgHhQw)FO8UJ~r zsvmoL>r}7&&hs1JJo?1y(0s7UtItJxiF0-ET?dp;v>rdj{QA^&_n2DL_2Y?q>)`3b zhw@Nf->Q)QT)keOd10mRanDb>e%|nym)v}6Rp0#7ri53W-|knQ8|KM8J(%-W`Q&BX z>r;o>KN`C(SoL{a^V7P1U+UyPZvTL;`gy!Fy#eN^~6EA8T+Uv#3Ki@j}gHI279>z0=*!|Eik4Fbqd38uH1zItj=-|wjA3|;32U6_8;^?367N1mRT4pbNSaFmY+ zd%9I!KdgBC;-~BN^M?QW+Qsh=ZoXqvq6_Kkr)%HJS4an@o<4b4<>%IWI`m-j%?PnZ=)dpkal>!xTCa!r+l$s&R%f)I&#+pLul%I%`SSJWFTUTQgXesZ zht3ncxkv}<7x!?Kj|Y3YRh`d09s4l-_I!EfecE~AyxD1=wjzDL<{X|6tNpD%u{Uq7 zGaA!{tM1F|=;`y*Jnq92|KQN6^(_+XLU?qoC$3Om4^t1n(v=_O_4spj%nK`h`;ed3 zY438@)^_}TWX}C`O1b~;EUB=hq^fR)H7ejqkOzvH{)s@c{)%&b@;_kbwA&E%adO}wW^=C zdsD*ue{Fx8n;)at>*F)*>GG4VeZOgs!>3mDGyWg>_cHQ&U00Q_>dCKSKEsOVoaLu= zo-e8MkVRbgS3j9yj!yBOVX*)#-_IUiHk6##O&O9Mwyn{%U@FKJXL&mg|In zJ7)js|J3#2Rk3`cxvVRumze#limfxM*W?v&my45`SdJnI6e$&l)t92ypGcpd zV*B$)?_WGm4v4=O=Dwt^etxUdHi+#S zy45_D-~9Ku&`%HMoO9!!wT|^*HE-p&{24cX)`H)++@&e;$$d$E{rpwe*F*eOq4nnK zjrQ>wR_pM^{-y8tNj`SBJ*QUn3-8$&e8f+E=d|_l^x6OF^5WFfpZQ*#eW|YwE4}PX zo_bHm>o$Jkd%S<-0nIt}Lw9Z~;qf)scOUtPKZ>cRFY~>)*O&E-d;049hVqI0;-~et z{eJN~A3n9Hf3y90>TR#T^{1}=TlsA+>m{b1dgiNml#d51y#vtIEs--}aMY;Gu@ zsLoI8`T6Bu`;7hkl1}PpOuvVtda0kWbuvF1kM^4v_Vm+lpP&4ve{4Un^!u3~ytpZu z!{a0Mom2L$x>%q2=s|o~<<&#gh#ix4xYXq zrXD_>xmfjQKI74K%nPeNe)H3M@;^SUz0+?aB`@`>^V>Y$*V0K$J@w3w##Mjj zGal7GN>Gm&Y@&dX>jZOn%nIcu>BF z=EBntH56tjW0NHYE|F*!=~iC zalhzW&pDNKvDZJ=!`DwwU0>Gm1r=Iy>F z9oXyVC!XiSH+|qv(^BeZ?$%bua}MV`xxdx*!JJPXf7Q6Gey`7Uz-n%Fe$w^&`-o$H zaccE?@q(8%B|MLh)UW)up8F)G2UD-gC$AT$PA~51sk;s+pXhPSPwVsR2KV1MwYVQg zp3oS0mEX=2JZQbDj+b**`Q(X5`FK$OT;%hp4nOgo-w!FW>l%M^4h-@1J|^#=xugx5tM$@cHj`Xx8!ada8W# zdU5LX;+~#*UWa_5>*lBR`S{*fF8)6Exo>X@$xGd8f1AteNHIN_daH_;)$jGW4p_}= zzvces*JJnJZE95?{rw&Hi@x>E4b3Np_^ZZc^?QA;16Fg}-~6O2|IzC#{{D`?Z;!{< z)OVhw-{zUSzpa<+%#Bk=4Cm_MyDli7$k+7S=T&EKX@3Auee~aNsQh+a(0pQ;bFM00 zR=?NhI$$-gIzQ?9{Nxu8Tm1co$L!J=`e5oik8*#D-Dm63fvH#JlP4bK<3as%Q9so0 z`ozwI^xOXY+XNGpzdg%1^u-t`Bhb zMVqJpQ$OtBR>1QZxZw<58YIeCtB> zQKSQ_K7R4jdOjb#|Gl3+wfa13^!<)?>7~E&$&;_~-ZzP3kG>0pLu@DJ6~^g{GBIB z^}{~VR>0$%b)oZ!kMd%VD>{kzP+dHVy*_m~s+&B1!>o&|*Ms~x;Q7BiQMdVhT-eaKJq+28N~Q2R^C>KohfvPQS^TYt_= zUw;)>^DdjW4jk3NhgF~JffbJqKgG`P9smA(<=kSP&v3DNRb+pr-(H{GpPd)R!xYwr+dwNw}e>J~8e(@8ZAGJ+vg`&{KJV;tM~gid{I+!pK=cVR(_kuebP@) z-Ms3MzIuhJQ}O7+@F!e6LR(Ryw`Bd11xpD?hF4b@|P^K;|W5xWk!tS-H(pD*cmdzZ7ecH57iKDDaT$+=m-?zQiNzk1kJx?0Q@5TxzE~Y!JyAVzl&7!9S5Lkenj6X|I=}fz z|2FHyv+mvg-i-G{o4?fxc+Q1NmyhaV&kyFK2k}>p%j);~TnDV?<`+Nd@@2=rchsEY zOXVY!?;`_!s_W&65p_w}H2im%o;kNaf3M0}_&u5gs6A0NukMb`zZe%H-U>pEZe zdHhjRtNNDTYYaT+QR?#>4_eRjseEGUsb{{5NBMZL((C2<+|%I~Kk@m!5e$uu7ZvE|J7X0?}OFa8E_4QYNn~P3j>Zxab zG_LyR=BxEw55!A-_4M28?`vOt@YJfl@A*xEPUZLi?eoO-Li38DJd~e{eC+Aihy29% z`eeu7j~#QK->~Yk57Te?-#O>Rsnx$HO~-u^_twGFhc8Z4&pvU5bgFvh@nP0SWAj3N zsmoV>s@wOMf8e00)&1CSV^hMb&J+3G`E5RW(7db0W%YY~t^-zc|8Ms0JzD#!EEoT( zO+^XHww=0@W(GS)S`oAdj;3rCId{&lVUzOVbb=lA-~XFkuf z*0a{)D?jP_{K<)@J)*U$udV;RFh1hb7w7vO>(J+U8ebkS^{PBxh4f+a=)$a5aq8)Z zRi8RP>9`Mfe|G=-C+dgw?LPc`6&46bY;0cgQ+E_+zgS;22ag~8l;`&+ zEor>LVQS)#0jo)(tBi=PWzt->wH% z>s>X!s_twbpJBC5&ujdo?>xETi``NCpAMdQGzU)~zBo}`-;N^vr8={H)(fjS)&BPW z@+H5v@%JwezfD)7Tdilbzx6?Suv+hI{yVK>9<0`Bf7`#V-`}r#>x(yjKKA{mb|t*( zJn{H&e&bt@yjUIPdaHc$Msezh;i$i=yAD|KD!)D79e?imt=0XwX8*2aeI6gFpMI^WF_b%_)4p}Ke$M}1k(cy^BKfulZtn&&+E zxmWx`YgIqz$GQ@p$4BbB5B$c5)+J6%J@w32@hl$?R(hkn>wy*D9Da)VeeAn`zO|}< zFwSrM)b~8BZXM8l5GSVIX2s?#)f@GxyWUFQ zeBXS1XW&)Om-^Fh^PFSWM-Nu(-O>E2`lCMA0jqU7XZcB&kDol_g4XJOT=vSYgvY(M7xhpD$&@v8cxzPyfVy?Fej>*tqm`=8wz^|O!eO6~_fefwEmtd4K4*!_`D zz8LDO5HI!QN0_?Fr(0ol-E{dWw(tM^AGWqp>gM8!%@tRuZ-ldY)f{6&l{To?ytNZAFsne~H9<<(yuU?_PsxF_rjPYPqw{D1^$Zvkq zlYhuRJ*Tz!ewKbb@n{a7z7eJ#KAl+{^;PvMU(K~%h?hFfO@6A|-{=3sD_X1l@9!zm zh4i6*`&(Y@d^e8{#9xZ^;L`r&&#ng#rjA&hpVrBjZyepfQs*}w-{`?vzs;uy>A)(l zK8vX%hO;`!a~&}2V!razT<*&Fe-_CbBiFBa4c!aZjJUG&= z>U@S3ukzdXt9Jgs(eB*ne()Jqemf8BU%IzmZ#eZc8()84|G16wtNrcyHtW{oKAV@A z>s8PEY+Uur!&$xLxh|M>vFFS5+xwj>F5UR^o!39TJ4IJNJ?K926`%jk8`r5WrUTW* zBb?>q!I5rN*AGX$^xOIUhkJI%)ZcVNSE5__!%u#jFLpoZB;vzrj`|4oLwqrmPaM^& z^=F^!=BK*P4<3ELi`!`R{Cz3=HP=`9O&?mP7^dE4#jEO%`dkOB)@#4DxbVjoI0a;q^IsWpnRg|HGZ1!{J#ABN4FOD<4)@z z{7fI8^GByOqWeJwPdvJ>$upNuV(O{WS&CJ^JY1@aZ@o}H(fba5 znrq)*dV2r)j=Fw4@n}x+zEghZI;{g{J!8BPx_(&kT{l0?bAG?^`U_gC{eQ$^U5PG! z>W|J7{S~I(X2sSEXLYT!;;X}o$8UaGhu4EI`o(=(tNPi`>Pq_X>5KFE(mG)Fi`7$) zE>y1&53b6~{*38bHFr+9#yBS?taMQsnb)3 zqr5uIK6$7P}?hx;|)qVu*iB;nF!HzUzS1 zy6rQ5()E1z%a7=(3F^@DET74huX)VF`rZ+vw+iK(}vG5w`FvweJq)f{zx(zic<;8w@BR=+QJ&3V0p zkNEV(d7h|Oe$%rae0oqGR{5+~Jb9=O&gQKf>QA)K_(|XM;C-&VytTL=`|sWvbooR# z`_<*e?uR@*n0jKUPdvhw zwcZ`guc|-l%j+1elb>|?@%eA{uiTH>|Ih85q6gD&`NUkWdge#*s81b^^s4%--}WIt z&GmY4=l>Jv4)bN?H{UCs{g!^)-?#elgIcTlNiXh`&9Bar>U=h@s?*K&R{7+KXZd(g z|5D`htPVf%?e8N#dsZ8*zOBFgS^WEebZ0TIS6}9~9W&zIisoO|wL zS}VV=J)|pHU$wuT-#K5Oyg0F1?`)plEWhH->g4rQde!;O#{*yZ@Yc%jEidaz*5Q7o zzVpKQp>AFJ=p^E=iu6|P*Eg%k~>fSvfIC=j-PYs+TZHZzBpK728h zhw`bz4}Pk9eeykjzw!5rf8+el;E!{GKJ>VuPgm^uRGyxg4%8y5eooI^j6Z|(=3#MDzyz1g_xmxr@@$+KRVbukVAChkM~YI`-2`_dY;gObaoV1)mz%fXE^HTC!W9e`1U)U+b-Gj>G!^>Gx&_p z7qPniY#s9MNAmGfPaa=AQ9Utv;?y1Wsq2SyVDiL}pJLzt{_wf|16A(FIj3|6AMwo< zd)y>Xtd1{ly{U%>)hkS$iboG7FY_7Wn+N3+T{l1Jc|CaY@AdQKU)PD-4(WZxabX`DJ@*8x}69rfE+{KWVA69YeAfl#6YazF z+qwRh%U;!5)b;23MsxJxjr_*bm-FNkXLF<2(|2IFWE75hoQr~&R zUv+)*=E;lkVU<@;#D`fIXMGgwOSFENb+PN_r+N10KHIvZ>Rb2iop|;sJvjS%(E8{> zI=2)qoipOQ4p^<*{^TcJ@8=IV{!y*f{=a%%5br;m-_~9EQRPQ{e1?^d$2~vk^81ua z`hPN3KWWl$x*k8~(}&h4hWML}tLl&XTn8Mjo1b*OK6&rkUfo*l|I3c-N_fuy)E}KE z)&=Ro)Z46hRsB()>wwjIozwiJ>-EViuRpG}x*xyv`mRLRd6fFj8T%U_>N8JXoO_`bJpJ_HXn*Ukkj`eIIZO3s`}hp2 zIsD=$eczA$#J^qATGik8Q(cKJq_5xO#{QPaukz}tr$6)J3bQYH>M-k7oP6tsRi8XR z>3F{TmxCVHDC!sgZ12Exzv#efe_K!bO%Kx9QQT3z*?!jrt93hP`AOgVotGYZ@7AiG zfB!^(>N}tLs!o4|_?w01E!CUt<1?)0c--@ozSk$aAJaFq`XztfmGCOR>89UeeRL9Y zz3Q2-;#oc(tn@~CKEsOdy7_4?zjppV#*NPLXJdA4`~L$S`R)8ozukvR|6$|LFa7?d zxp+B0&u@I|ao)%$rk;A{NAajn9gg&>y6b`?Ui!_i+r9q|tyTT1-|I@&=YFNWe&>g} z_3&Fv58`h&uBt!ka~-f+H^2BvmtXJx=3cE;{iHi~CA!X?)YqSW(@&%)pL$h3d2}*g z#g*<-9ehX^nlI+3n2%RowDJEjKINgklWygw*OOIUzj%+^uxWep9o;u8W#ud-H zp}yqXhv~QXmw)rZD_X1i)Bm(9(oH>c=<*xS<4Ikf9;5@Syn5m+9}lL^Y^)zveV*_5 z>H7J7$PYZIwW>e$!CeVYf9mJ?tgg?xpghFCrEuw-5kIe^S~nejitWSS_>*&5tNJw$ z=t_8%-{wPnC=cafwcgFjFP$^utHWxY9`F3bbAJEYMW?ou`Um&z41G|ad86|i57Oa( z>SaFbRXobmwGR9d;zK%6zZj3N{FJ}#dcz<8-P77v>T3_`3V7B2_V~bq@_Bx%L+gcA zUVVgBzdRi2RQ2?=;+exwvCp%8;c-uHt?FkU*Ollyr&B-oH(jW|ic=3i^BL2V&sZMP zh4M>LKdk!gH-1{5zZdrTPv5V#s{d#E|HZD`b%|$jwvW%S(x=Byv7cZ5_+C$Jt$rSn z|G&ZgO8wm5>dtBBka>ytP+dI2Sw0>d=~i_U&<`ue$Zfi|IjpSmo7cF?B}qtgh>TRiEcAewxp(zk0~At=0a&>Wlrv#vAQ#Jcw_u zd}6LwJ@cb@)Ta(ddR3jzvpW35=g0luxNjSwe$*bF;e5xp4t_%W7*Fhe%6s0mZhGqa z)FB>}H($ScqQ@s*_Q~tBj?9}2@$uwcCqLDlGynY48-ITJ!6Uj7p7qg(?pLl)pZVfM zIy;JVm+CC-b3L%?S5LqDU!h)G+vi!Gp+5WnNz!lEYhHC9s&!U*x)qN4)M2GF%DXOD z@%h3}bAA8hYtOhxYgOO>_^yQK@tFD^AAD7}9(g*6_)uLu!dX5Z9O+hd*99w{JU`9l z_ZQy&f<{q4vwys8`1=j+BVWy-KSKN+MRQiwneFE@tmg59pY*-IeBVQVy0v=!a?^2L zk*?>x)K94{TMJ@Xmshgr{a+h3hAw~2w;p-(67iwB zcos)}>Tp&!d9Dkpr>;6b)t%qxePQFz=kxDZRr}j{!PlH`u6c?0P+dHWqrR$M<*T`_ z2jZm;U(#>q_XGdx{MM?z^@6TMr}Eo5m3~`~K01lk3)RK5IOZ}#$K#%#_&z`QrGq*{{fYlypTsx5=6t$%)+0{8 z<;_n$^~`6i-@L46Om}Ht>X-+!E>`EKx%__Xi#Ps0_xn%nop^ku2R$!T*CTH}JxB*u zdG%RL9Wk8MNuKL~Sr^-%>9@}#zx=X|KcC;)|9!^x&nH&vOux;u9)9bWhxo9{t0&I# z@vH-;-YBL6NBs2L^X0`qeRgYAzwVoTMRcq6c^*~Amv=w1j+g7L^2y7XKFm5@nDyCM ze>I1W^MIfDcUUKUdNbf@^Cg! zzv9z#{rtpte*ef@4{B8RzmZq&u@I|@w#6=G4<3lKZ-|v>Tsl2)m;~?c$MGw-}M)s*joAh z&i?P{xBtJ<)VFT;q4L{Ye7s5zueuI(n7mPbR8KvzKI?|^$>R$@@qE7OZ?5UTK%)NY zdvyj~KBaD+-*oZS<>|rH!;^ zr*57n>iWzTC*s3uj`}Q`gHIRACywfLVfKsdH-6G}o?P@@w{FzeuWv{{-=V9YUiPcY zL;V@cr(PAyLwe#J#nj7~zI8+SM85Kqp1*(c&sW@~wYVQYd_h;Bi=Vpx(e}4>LhBVn z`NUD3A29nf<|kd}_inp9y-}Sv?|e(|#B(mBp7S~PxAk~^BBlpZugWKH6sL|Dj{2** z>wqI3Kh5p`m3wXN(sLi(TGhA4-&^;5oBI6D`Sj(XJWRdKidWSi_2qR`>*WhS>H56= zr%yYowc7uFzZkFb+j(OD<0s}i)vNXHXns}wQJ?F8)jIjYPrAPUz29$lzuk{ZPVNl4 z_^I!C!Fi%CcAxdrgZQw@tHW7d#b4T=I-`Eq%}>|QuRDM5K27q}^~9b}?+m*9Ggm)d zb$PM6JUy6t;za$K&v=%nZ^X~Kb;GRFh5W?x{lT5T-@u`NA7_20lKb21!4psa;YM-Z zeDH<6!+ojFZ;ucA*&OR~u8ZkGd|2hxXEAj~@vN@vfK?wq_-VeMH{AUbN3>S=aGV?JihSLT=~a*pg*Jf{Op^r?+mkk`sl%Gy*rv;Re#jyI$*Uio7I=L(%fI#69);Ve%-K9paIt_xQEuA86c_V2>3t$qH}-C*@C&*%*M z)OnQp@_6cE_rpASF+Nlmk8qZcXYRZxabHm>^R;jCWr_zbfyR_CX=_TL%5`qDKmbT(ZLs2 zILp&t@vFSPMC*m>sbinl+z)xL*K@t*Pc#QlU!u7%>vZMm$&XMUp6li(J?HoP zzIJwNQ9t63xx_nL3+ z+oih5B z8Fcm2*UwLNd9nK;PY-TQ0>eoG^E78SI{mO5B=86+jZ?j@^ zmgw)r#eBr0L z&XX%oc};6kzhu9zfai5ZrCa%}-}=Nb^)@SBRe#jyI^bx%{G{tX{QejF->*`qL)YU& z9(ukV`E4FOF`ZSB-gm0syu_UE`uHj4*ROo|q}HOYA8$wZH=RUtb`#mVU)Eus`{VJWpI-9yRe5ztPYkPmd3{h{#`1}&lXW_H z{FHYee)Deq^Hu8S?cbH~s@H@3=A-%gMu@+oXwIrSv;BO8)jV~6(&yLXKi~gefzPMV z!4uOFSEz4mog4hbbI$(A<2L?2_n%(W8FbyRT$lblPvrCbre}WYRr%y)OgHmlx=9=$F(jU8DYf(S!_xlR0Lx1Wzk7oU*2UD-gCr>=f$AkKpBA;Q^&o6$O z%kS@c=Dw{}oerLOG$(oP1D(XwQ>Qb+S$^^|9?em=UMQdFam`QjydHe_CpZ3mRk zE8w|bsax%DK0C+gB&MEv=11|UPaTf*syd(Hh?jnQzP#PVN3~XUf3JXV)jBJ`t;e~q zpPqWIx5_7vp1c^!L%L9Y7U}AT@^r+IpJIOh#jTESEk2*(@AcBPPw7IBoAgniym|6s zI#69)p?+BLt77>?eY2STOJj4E_TkU=^BL+-q{mOObNPMWczkP7&(FtJ`&&Of zi0^SFpIEIk^P`w<#Z!kBZQA%} z`AOgVop-(9cCAHy*Z%RJ;pYcEZtQ2e){}X4nCo0M-ch~Te%A%7^?F?MlfIu{-uJAH z|1X*j-qQ0#-w4ft_~H@H^6}tEx2p3QRy_MG{dOPTc<3*-R`s>@Kh$A;&ZE@#xS_AE z4_coX;%_#tsz2&;9k5zA9e#@a{?4|mE^4j5KYr4~yAoae)F1h+zrxhptk`*Ey4Z<0sOCsaNHbm+`1C>lvqB73+f)&pzWPADkyIJ+l9N-u<}#lU<3f zeVqDq({HhJ%shJH)T{Ez6Vnq{NEhN|EMK8{P(Re?oaLvw_m`*M`N&39KX3h@!Q&e} zNSD9z`bLNk@x>#Y{hZI;bFbE_e$;+lkuE-cnEM-F-u=+8 z4(Y%uubw!|$AhUe8|#NvpZ&>C*YEGm+~uz~`b`I~iscja&0_X1jm=rwhd*5MW>(}S`j-S@! z@1K0=Ir}uK_e1x3T35z%KG1`CUL;@LI@KW^Smo7cF?B}qtZrUM)rZGV^X<<=zP0iH zr=7QN@6hLdrM~+>4_}_&Sr@0C{>+c!QJ*>-=~Z?8)%x}EEBCi^=E`I5)~M=!K2Mi_ z^q}u|IXC33OP)?5K2#Ua;;2s@&gv%5b-}EQ`NdCj*S`(8cK?q*rcwRHSEB3j zK_ANJb=WsFeB?U#o%`Fmw+U5 zKh5?2^0*H?thK7|ap$gtSNZMr2Vc$CXI zv=*-y_rAI-&~;9yuKSRFXP+2W*IngvZpPVH#hD-JWZn8<*2U)VQ`~dWh>*3B<|()IJSoqun=a&BXO)^AiPdNMz2ruJfS~A>w@^=rC9Z6KI7Rr{DxH@zxZjM^WcV;{8VdIzyAK6fu}$9 ztLIDp)2EY|dOI4^U#c_P$7fj0QRgRp=gieF?iuGh9Xv4|afSLun0ok?uKX--#9yjo zy|B`^5BX`1@5dhbJ%_ed_v702e}sfLyT9>g(Rw!v&0VTD+s9{E&GCAUpY*-I-1&Lr z%DIhsKJ#y5_2$U?z{>9{PCL7i)gOEB-b;6Of19W;*C!A0cNBM2Z?@m{z-rx|xA;k4 ze&_dNE9W-m`3yH!Z;tHG%I`;?vVSA1AF+Nz+!y^fEB3FPQ(lj#>l-2dsz`s;etok# ze1`fH>G4zS^HsmKwLf#JU)O)Vd;90Vy^cv;&%^W*tqW#7^)@RuXQ|$(FR!E0w?Fww zmyeJ7+%sCM`qm4&Qu0!t-?_i7%Um%%n0lKPuc|-la~*KBUVhT`etxg#9o1UZKlbIW zgvYnkul(k}=V|Ms2UD-gCvOy|&L|$~sk;s+pXhm!pXPi0{b#T3-LZ@>m!I_b@s;QG6UYAlA}>)gX)Q+dc~jZt9aH8bB@?~l72f+-gx~5jjDdc zVZD=XHNWy3Uw?(xx7lbuoYl9^imwhUp8d&B>+t&x=e+*OtyTS|mK@a zonP*UJUwyhsb{{5>A;Fd7goGcUO&Xom>>MKK7K#>buVeH>VLide5ublPanDucyz`3 zGB2hBbKUY4np4%~>6)8}H$pm)PWDyjiPzsJ@76zX!tej=|9{8zSAM7`j`)0r*)OKU zPqF>^yRUv|qpIKQuDuh_`9KdkFLIyhvo0tP>1;Nxsz2&;9k5!rbC#cU?eA;v^zzoK zzUvQl23`ErcOTU8`Jd;BI!rw}nV*eEx|M#F&$;wRe06?``SFq~HvT=13)f#L(&t~! z&;3oeLV7Uumd57j%UHfbd|2`1ts7Q-d`-W7zUuOa^Uz>|=l7#Nd~9n~uXJ(_UZVLUoYhPHDmJgGSNYjK>xGq`JU{9C`TUh< zy{u8xZ+k`WaE{>9G0*f#a3^6}tEx2n4?h?lzj;HSF%d8cpvL~B+5&;I_>_3>jCM}2&Tm2Ty? z*Uwjfa^v5Jcbqa`)%97II1wMJi)V4vrw(UzljnM1*2Uhp+rQ@W>(bNv z8=U-k1RkByoaDI=bP`ifoz5(d`m&z!>>TTbRiEqTr+I$AVawzD8yr7x+jFnZpbP2i zcfaV%i#>11(}AgnC!bi=tGs@7C_jt(tQ*RwjygZ__;rtezC&xV|4&$dh#6Vd-uaUb#bMa z`6{MY)#dS^{8F^;k&gYzPjkG#{Gs3Jj;VXTq-%~o$WMObLH!ldf%?R&qIvpLH{*&Y z59vbrM85FTJfA<=?MKgOEzX;39?%u&;-{|jncwR8`YXhT`ov37Kh!UV@`1Y1RepP3$n!)Wv_3Jc*1MzmRrN=G zt^-!<f3tB_{ZXImfYrJyzr8;BmFM?YhMrH) zd~#Q^Z>x3Wd1Aix=&xdYSj|yStoY{PCof~X?30J;P`*Nbn&$8P)4*6Mzovu+e!NMFD6 zh~HVqgLGiUmxubr6;`~=XFRKC-LUHOe8*4o?9X4k@yyohe$c@akLD!LTsn!Vr%tEB zS)TsnXN)&O>xC76w7)-g!YQrQ{kUSEu4G;AOX|<=Z|kE6Q*X24RrN=Gt^-!agPRo1fO<`?06&+WmGv&OELw;ptEP%5Qzv zB~DB|^~}%4Rlhu()k~i1fms*ZXZ$qR`Tg-Lw>GN(&!Z1dJeq^24_}<9o_*p9=~VU1 zlMcj(RbIWqQJ=bXz)G*m(}5!%Kh3kh zcinyC@4N2ts?MP6{7-%Ni@$W;Uv+tU;?z^m{3ssvshbN&x>=_Kvo3bsmEZsLyzZ#_ zVMq5)>vGQ1gLL^Vug|)~iTF@mJi=Li@-iOHQFlF1K9Mi{G|&FN?(&WMd)qy`0^WaA zzwUO1~~eHC9F zj(GgE4nHs6`SY~Oxs7=~!;RIOBd-(EZ@(|~lE-iS`TY5N_FnhJ^WV*i{VR`~YJcmm zF!ffA>EF`*e1_FL=WqJ${ro>3eR6A6KWM+Mt}w__4AzvKk&@f>ibz| zp3+Ycy7;ND4)L5n?uU9}>TOnR&QiTmpX-B_zH^qJc+Qi{{!9OdY3|3?eLI8h?0G_0 zA9NjJh`-sms{W|Yb--%f_8ULxdj0*#PxNO_zrS|UmaatCd6fF@L)Pi*&saY7s(kY3 z$!9DN>5h2#iTdb3JTaa;Kjppte*Z@gY!vqcA5TnIT%o=Z&gzZkRQ1`sb&mMf3oE|o zMSjw^zfXL^-CL{rK~L#Qcpe|A@0_x~@vSHG>M-@HeDbQe>X(OA|0wUeV8!>m#ZPnH zhd;UTm916%hJKuH|9{bZqo4g%-Ma9_F!jV0(pj~>YM;5*4dqj}dVRw0OCIx**1~VU z4@hrzf19h%x}ZG7-)vk}f7ItXV6|>K{1iLCKlkGP4J!3*ztffIx?icE=ZSjunI|vC zhw9=A^}~uMPdE7)<5x%r;$`gd&QEiE|Kv-*+CSN*e%(1;fet=>n117VJUGYX=|MWM z%Bxp6>Qjf6&M2=RR($6MKV3gR-gxLQwpR7E^*2IzUiYQGe)m&dAGAI(#NTXORe#jy zI$*VKI{XwnPhNPR{`(E;&w71V!n0pfzuMo{;~b)sn0o4&ui{xg9<206dDjIiezm{l z&-nNyt(D(5ez_~st=8#2*x&f__P>60nCq?b$rI1=@u2>ts2^7S_8C82zw>*Kdu;qX z+x5qG2l1AkCzapyV6L~yC$EaDetB5+kMewm72iHfzkUDYuty%(TGdZqeYT-*=bRs=no{u7p?ltv~&?9(g*6xnA|mkK$2Z)-xXItGg~JpGcpdV&6Y` z!vQC>7T>SEzW?}P`}=w4f9h8I+j`P(dGk|GJ@ZvO%f~YpR=QQ54#Z16`;edN{5t$S zJ>&a_Z+=Wyq>E1<+V_0Lm#42^9nyh#;#sWp^wWj%u*$1LbD(^p>*gnY`}=OM+^4nJ z|8M$2SD@>4W9r)X>9^SPgL(ACsi&U#DxT%z!AftG*AGX0e!5=ockb|}jjsoaV{`A}H?_c=0%Ui4Z3CDCL>vKM&K0oQJ z>$5I#B0f|X&*G>r>lx3^aXql=;|o8{yX|_z&c8?f?VU?s#qisAt!@85t?9S(;N72m zL2FgN*H^ld`?7R@+wabOI*GYn^~{grQJ*>-=~Z<;!x4|4=K4Iucl~Jpd79TR2j96X z;<+!W?{Sm!-5<}>>hxghZFanL&WNAaQLWd0<0oCOPY(O*XS7!LHNUAp3JL5>w{HZ zJ#m(g2UBM@c3rUQQ|G6-{vP!UkLa(&oj2F*-IeevzpL{^A2eSKtM%?^epUTZpX-3t zI{C#-v8{@>VE8bKv%-EPg8%^Z#s#ow`xrHmhR^>tmat{KgHf({y+EY2CHv5 zzAHJ0QeS`iEsw8XUaW4t>WS(VrcT8h>8k6`Sl&D+pXi+CCq4W7^WV4e^Hu-Zf1hOJ zNBSck^{GRBiF{4Jy*_!%mo928>f0XJ73jJ@m9BlOj<3H$e5g;n6!k;>OJjYBc^z37 zd))KWe13oQ`}S{Cb$xhZ^TZYE!-w)vzS3DWPk(9usLy&~rC;rDKfip`URzu1X8UdZ z)+dJY)w);B(_h*@>a$*0>C@q-*z@HjcR#kZs-JVGu7v0HP3n8xROgBQ3R4d+^Rsc) zpZSbu_4y2|K7R4jJkO(tUcT|~SLN?Z?da=4>oXTt>)p}(s`{fo*8!__dLNyB+n?Wm zO#ezf|DKZjmHN)9(f+nR*8@{;v*V?6Mts)+N9*M$UGFb%`-)>)tM@}^Uf31!M*ABN z;=7;Lrw((yRX%y6ICVzxNKZYlLq5?y*XiDb7tr3?~!x8&Tm+C*=Kp4c>Vonw?DGAs({Z6Q>5cMyh83S*{501&^F5!xV{7$!i4R`WmFV&__37f#6?^=M6H{-q zVsn=2jr!DGXQj_qe&XAI@A%BmwpRD!h!sEoZN7E!+q^{l9YwlJb(Z$IE?D*3pZvt* z_ub#pf8SMo>luCJc=mB#k31f<9&_arQ%^nfvvJig4`=m~=ekBZ-q-LG-`_L+>SxYv z((2~mRk3`czFExvRb%s(_Rse58&-2FznwEr-EE)Ndi?r^z2OaA(f)KUq`t=oo^{yw z>aGV*oO-5mTK#nh|j#Xw0%Xrk6^^8-mid`41 zc=G%-&*uk!=&l=opL^F2cLrXyzum9&72p1+lW1K~T|B~BJ{}zDR&_qZis#%&zdc_b z@Vfq;S@oSiPy6;x@)>@6*4p;}d%;il+4sMn{LudUmtLP=f9tM<=iEqrdHY*kAGAI( z#D`U0eHK$k3}8|7UWtoZ!kr@8*$(F+cFbZb?=@UvYB z&;3e$_W=)@r%ygH_0%&zibs9waHLn&`8?8b{rtrD^M;F#?!Qms`{P^wzE2$K+rRYj z#a=I(rw;SF@G?J&D?NR5p**be>d+i0pXhm!pY;7b)2p9$WNUFh_Wo#Bz_U*)UH+`SoO=prMmchhVqI0=BK&d?|kx0%OhWML} zv--0<>vJ8j>aYCf*WIqzze)0Yt-rnB{` zA|0qN^_-*pRF^;Tb=^7j!yeL=@T&ct=QlpIE^(suLUr*{tor5QQeAx41LYI#Gk%)u z=a)ab`-!c^`M$NkT-mOH`V@@H_tl7si&U#jP=8;XG|9#CT|qeffb)G{4|f> zH{4eivp z<4Ro~FZHT?^5{YJjOFQOT^?_Q_;g_A#r(wc{_<~Lctsnf{`5cX9s2NdJ-NT}++THh zdNB2>eDcJzd_1UsDe8w+zkSG0*UyjVJnN;c)%OptStro7FX`*2t1e$5J(zl8NJpGF zswcmSE1k?+H>~tLuK7vlHtP*LKVMZjw=vIWxUqV3u3HsOo#)r+2FBr>Ech zC3ShIKV$jS6GMIC3TJss`%~AtVWr0pe$uf&ANSE0wpR5!+|(I#`I`FnFFh#l`9YqZ zIQ7&sU&VA_#iI)=-YBnsG>>2W#P@n|=kGUkr}h|9|zq+k0QT)V@%oQ();@jOQLWYY5A*-0*~h8xemSq`CR$(SQ*X0kbC&9j`dkOB z^!dV1x_sR4&8M_h_hZ++yHfH}f3&~#Tb~%F-e$$C>W}(d2dvg>|M8Qq^ZO(J_u;M8 z{n&C~SE7rb`o2#-b~Ssy)! z539WTET+yVp4D|7uq>ZhOMUy7zv}u%h`(8A-cr5U zK0d-~4!`(G-}B{F7d^AJsz2qSU5Rex=g4pU=82*8s>95SE6l#+sl%*iT=A?M>Px=I zJwMewkM6e1(_5?hJKxfk@T&7W{l>Q*&!=K_XuYt?tIuNUh~cbG@>~bZx>%i`=Gxz{ zd(Q!ls(#6(onc?&(}Vm}H^=>PpVjH%(ShpX5z@sQ=`Pi$3+d-vb!ZMh@%?;f=iiUz zR7d`7%&u+!{8Rd#{K{|V$>Cq@e*n+@_|`ss5?%e~)7M|s@gO~zdSa+gT;VKlX@BZk zH>~tLuK7u)zY1Ji`){Ax`1jTieoJR~p2epxPQR_AI!~NGc&S(2H@rkT)}t;jri+(# zJb65Ms1D^5^{3yS?{0m_&$kvozt^u%Jeq^2Z-l9bPp86Jp8kqo<@F_6FH}z*zVcID z{=IiRsI|Hul}^sVOQer4PE^;gF0OEvr@!J?d3}l23)NG{xxi0#Ki|3L9-X0n(l7K! zK3#nJP#xmsd7_?32kIBEigXg`$%|o?&-#ex`uVBf`Fr3C4sI#?fB#?a47%=@KIlBg zljp0t7!TraHm<5a>dWh>*3B<|()IfL7tTARwdVc6qideHLVY8g)vM;nnU-Ve`Cr|7Jg(>@;_oQZU8=LR&vn77-?_n0JbwM{ zZT4zQRo{EhzH&VGC9g++`mJw-skbzyzqAj3X+NJ~)z24x;<*oh_l*~|C8%GqXFqxI z>{IK6borfpb@@a(P+hzftA2U7R2QGmP(G0#{503^yT1SK`?VI|uig5Fu7Ef48xLB~ z(&tP4x&Ebb>Wt!9eb<$G+3&jfiSPOH(a-PyNzMJ(c3M}!tIiX;?kB!=WnLZTI;(v0 zG9LA*!|b1pT^FqS>`#80>+$}smv%?hZ~T|8L>E8x^{205=aqhXF!k^xEJ(;n8S>U&?^JL$S#^q@Rn<+IN`c`-gz7msk3j|WG(Rb4+E@%ZU_ z?eAZI*Uz+8_1XV7n&*l6^sP&rh!54pOR?&ghf8(wT@RE`mQ#^&78{d|ViJnP}7*!KtD{EDMntNJca=t`bv^Sm&}eayP`$eTw`olYWt z=EYV2s82t{Pd#}&hzFA=@0{f)p8b8syU%WtUN2tu*nZ;c$2S+I-*|jgr;|trs*9JR z`A|PyD8Cf-!>V7MpVni4zvYy}TdVuA$D6v6{i#3oJrCp2#dn|O@zhffZ#Ivo56VM% zC=X}z)(!Pn$WQwA_jA9xUu#kS;J#e}&wfo^_lu9}`bLNk@x>L+^7P|F`K8EbSoQOT zpXT~J+c$ppF0Ix6Kj=k$XVS$_edmQbbbr+8B&MGJ%+JR9p*}H`hgDu3TK|Y||M3&w zefZ|9Pi~Ux^zp>zi7Uj*^MoG6qa!a)9MzLAUbQd#truop?7I1BzSna{?y^s#+W#N< zmCjJdrzf_L-A{FS`y1ans`b*zI$fv^<;@X8>!F{#j8m76Je1cj?>hOZ?(@iZ-0SCB ztGeIc!Lwfa(EV^7^7_mdC*toY(p^<&w%_%@YM$2-{G`wCv)_7BYgIqzXu@Adb!uRW?!)wi70J3Y_h(=(5Mt{YE1^Z4rY z^ktkpb*OK|!>`af)#=gCyt;ga{G`M0vu^jK)}nrK|8~k6p8b)!`g6VdtV^7jdOI4^ zU#c_P=Xzi@$K#rx^!fF`^ZNVCD<#&2@aT@_;OWB`C#q+kxI#KrJ@Xa6%4fgWdZB#k zc-_ZOb$<`<0snOO*6Mzo`J%pK>EhFe_PsikH&>n>qywwGdg3e}52ns+tl#yF`uK@& z-@pHh&ugvf+YalDkv?CYC-`Cy7xUC%t`{%!Rb1&LKXpfO_KVGf@`-%qr+NH1GpZ}2G zcw+Zie)Rf8pX-6&U#h&>TE@*U3+HeqVjpd$v~fgHP@ZJnN+o zbARJoug8ykA|0qMp2bmL)-#@+!%tZCResxlAA4B;KrFux-KQ&Am;I3X>9@K*XnkUc zzuCB|{;1D&z-ry<{G`jTUpjGHYgIpe@2-S5yT5za+V49QCQgS>5EhE|_((eaKI9Jzwtp z`#zld_mR*1tJogbmESvme}1ELm7n~Ec#w+6d-{Ewb;8d7zr(42ANdTi({JZ&`n^5> zjssh(`s?@YN_3pV^r6R%`zfyvTAvu=Z#J%~Kk9QGuv)h|Kk4%8yZ7Jt{~HefA6O)KiZxRIe~~DqiX>jp^ze&CTaa{rrCaXOC_v_ruSN=~w&P>mEG1)?==E zB0f|Xk8qZsyo^V4)Lqx8ACI5-UJrit%TH>O>et=5??iph9dn_4?pycA{WOoBm=07I zk8qZc2S>V9T|caNeBr0-_4|P5ePH9~kzCyaWUUS?Bb$ogHS;wOTtGs%J^kMSo!mQ86`l~r~ zeE)-=`0_vTtc^e4Ipwwj|`I>$^PyWxAJGNH) z|A^OjCG+*CzH^G-_}23u?fKIB@;YW?*9EIS`;edJdO!cW-+NwbbwBd;N#!?Pew%MS z{1%%F@nMx$pT*P>!&#l=xel0hv3m*RH)sOSvDjepmaO|NOSTMC*X+;#Dzq#C(8D z_36N>pD+A0&v~-*|LIoFZOro-ZmixMd3}+7J7@mOANP~R_s92qQ15VG?9-bS`&Z6q z`&r#Q_u2aBL3~){)n_qvM)9n!>wr}szxZiBKhE9nnXT3RxZ$AA;3GbLv3@*tvDf|h z^0{91Do?jU{puOhm52BhCXWuR`uM_6I{tp_wyQ2`t?tK35ARBJ@l&5JAJy^oSBSsa zXg-|Px6X>M4lABKKdr;(zt4F@e`Di2YlOx_&(IXbzsf5zgw-9ie$6Ue;&(tQV$^*z+Pk>D%A0 zyxT#Is=n*qy%W##A3ZoaPxMzvXS2|prFye{e1_GW%5V8E9C$)&<@Y^a)0M2lxsm!F zH|__%^<-Whre2j#o_Lmz2lX#S*9EJ7b$*&_e;)pxo^igv`7ymiAAaidGtXst`ugd? z)WggCD6aJM(S`D`%Bw?jpnRh1<|lpU$(8@sce?xWroFlX9{*C;{^qy3z7gVY7MizI zZ?=!mu$rUJPx}12;o?hLt2!M#F&%M*`bL;~_?52wEHC*>`}9NWh4P8^A3x3Y_o(0c z;*I}L;Qgm|1-$C~_PC*sZ$0v2b(rg{^2r;;sUwD?{;KXeV8!#kJN@>2dBDf-+gjDX z{CPgZrAU8iU-Fmg@EKP9qy4@6gZmHA_PfSN# zp}rBO9)6`OKg(N{uMe7My)f%yuS58$U;d^mpVO%7?>we=;yI7#!D@f&vo3KW9jGpz z#Zh0@GoGE}dSKPZFMgV5e}D3S-@3J`pY-a^z|)`l>38zgLkx~z^se;!cTL3fAFds&TmxrQ8;o!~9jJpGZ$0e@Bt-syeg%c(9u1Jm9D6 z^?Z5LJC1Ix>WBBYKiki5&$p>BU-@la&|H{$n-#CBKk9QGuv)Ku$WOZN!_`kavbC!3 z{n4(JywtDGZ~oJ#lbCwynV*fTet9^nmpnehtc&@=PjlUeTit1EqpDxHduQPBjULp` zUwm<%-|CPKtn%uKvwS?5Id=~i{u1MyPV{^X}Rzt(o?AAnHb`S(vaxe@scH&$Gql^Op9{_VF23b1J{RJ~{2duWYR& zzejV-xBd~%>d{T4pZTTntd8}#EH3H^~_iCEFTY6dZWDSffb)G{5043 zz2&hRpD!=zZ-3U(NBZVDr|{^iXI`vMPoFsJ@`>4(_1QRes`>J;(y#oM-{m1MYOTIM zzU!m95?wygw?3~k)#bA<^XjRG2h}TF%3I3Mxz-J{E;fgsVn4t9*t?HxRQvzU>wiC% zEyu z=HTfYVd~-2sc@F3KlvHsjnH~w#pf$O&GY$_uRW~)f|UEQt$%xVjjsEZ`W_$ZbhA&K zn0l+m=4OAD$IE)gcre#r@#Xm`_V;E!{pJ37P50v=5A5H$rjKtf^!;sqB6jw>Ec24S)>E2zG{DaeZ2Gk2~^H)%<~y;tlk`Xot*pI`<+W(by_2<@45bu zjqL;b^=8HXmHlgftLuZ-Cx-Z&jjQU9`dkOB)?NAS`zLQb_8F~}-%oi;SE5_3gKqk* zPhOmu>#gP_FXO6DJ~8{nc;dv-`jgL3y6f9`?b@H|Pms>GL)9?0dedi=9_= zp!xVvUAz?OLj9?mG2RIE!HVyFAV00gd2+((4{fdP$C>}NGw9-{ex4_I>9;&Rn0i${ zdE!|<9@M`S^~0*4FZ^`u7tPLZ}XfdbP{vD>X{$Kqds*w(yQuxh855G%TIH?-}%;- z7q?b*I(XvI96WtcpBT#P+bpENRBzO0y|B_(=O22xs+1 zbE^7m-a1Eo>xCmeKj}M9uHEZ#tyTSf{o_B|f1ZX#lV4JAe5pf7|shPX3Ml`#b7SJgXNffAo94#gn%#dFvCa<3n|Eh4d0DK3!Px zMtSRo$+sTQTl|!_zaRh8_inA~C-r}yG5q;_zW%0bJ?ip_)p|2u#q_GWJRX!^imnTe z`uS<@53V=d`(wwquhh4m(F^tiK7DcSZ+Yv_ zIA?Bq<7ush-)qn96V0_xD_uU~i`@_T#MD#I{3ssvWj*7OzB-?weB$Ul`R|{&b8F%E zx4yfth;NXtdCsG(i_LX?^7`qiXFltA@`+WQZdFemF+QXNlP9kBxBd5y@9Tfx-uwM6 zzuhPB$2@vse&Sh&$AkNUpS;w=gX$wRx8l)*$;*7k_~t+e%PcT8)szxCsZ zXXoe};jEtdBQ$Ts%ld4e^}^H<+h_cwZ-4LduH70{ee1rx6VHC72R&}=TX}sW#NSag zXH}iqem=u$o;pA2yAOYI(5v|`Qi%ctXf~S&s^(<@~LYd zrr$mv`}iju*ILxKtiRu(SM6{2EANN($kR#8b*g866p#AU;YhElyDnJq_{C3ieV*+f zF5SDes$bW`?eBNIZcP2kZ~D;s#4z2wcg5aKTmt*U0>E(`Tf-o^vU+8 z$4BbR<3aP>57(hCPQ5ChJTX0Sg>)gF7|K`3XIS;QZhqqN`-bbgbL!{+LTAwBYp#Q@ zuA8pBxpW|2>ZxabHl_>p(Uphlvq%S4ef;33dHi_vGdF(T{-(!tC4Knx#eBt6w;p+X zu{yqb70W|>D8DL>)=YH@-9;V)^G5zeX z@)d8yw{Dm^Vz1lyN!RP|k3Z-kjrwmA>y3DHMsx7^?|GI^V(O{WnZ;3`I-J!_p7lcY z)OG&yQ~eI>4aa=5zu@-%^IJdJ3wZV^eK^|Re5aF02daygV%0AXm+Ip48OkT}o1f-4KnZ7ufydF!9I$FomUSDvrx`k?iRA^v9Ls`{fo*8!__^Oc`;9Uu4p{?l5k`n}e_ zv4XdBf9r$RCx+E}H!Ht%&WP_iV6{$le$rjHb?v$L>7TSxzwuwXl0J`*)OR23Z+x-m z4fE8+si&U#DxT%z!AftG*AFYc>*uHIweLTB{Li#jbvk(B(HuN|_~Jx$eXAnfTe{zR zVKtAh{Iovj$#*^SKCM;#(ffBLJo`BH?O*p3e-=}3)!4jSx}VRmn#T`*TA$Y^zxAH} z0XEO4d;e>97_ai%eWag!b@{|xuX^T3@u*K7j`XTJpJB!02S3gAe*O)g+R|G6KJ4CC zcO|;j`CYv}$v!bOKi6C3lb3P!RdMD=I$5`Vn02u^{1n@NFW>FQ8rA;a_O8yL>)fCR z?R)nVU!J~s>S8)jT|7cMu;SB&6>pT+4=cWX$WQBYexLB`$Fx@Wj&rg1< zwf3wlNrMja&by(@!pZw&9&uiZ1r_OGz?#I?gcO^XgHTB)E^xJym#q?n6Rr%zN z;?xntQGZo;9kAk=!%y+8*BhR9UjIpt`c)Tn2HooXb|3j_zWZbUn@3NK539U-BE8IK zOc$zWEMK8{ug_8{l<4ctdAZ{z0HbO)gSe_4p^<1PVR5- z=YQ_d=eAaLzrWKzC9$^s=a}^QndgZ){8yLvILq~_>w^^!AL3h|yjUH|Lv!@GJ~70D z@^r+mo1gOh+-;Yqw-)txzNIVRnNJty{>HbS^j96yfmL387E?zIXLXY2x?tAD>ijg< z%$Y%5m%^hgtL01IaPg>x4sci-FjiwZy)j#&w295qdG%9|2|vweCctM zk5l@&E_vuWV3k*|aMYLej4R#DyB?TzF@1iDz5agfR&pzcj8rkdwh)iwmy2$ z`ZgO^)gSe_4p^<5AN-`t?|1#?JzJ~#74Pm!bn#PP9hxJ~^IIOKUNt9qqd4_bH)DEL zKIiS{DbA9|2`~9?o zUwu|03z0PM? zb@4a-{(<#|o&V3Pa&BXu&v0Y)=EymnetZ3W&F}2f$m&mBe9Zbl)uBH83bP-Nj@Wgk-_DcU{r%~Us(#Sny_1gh(u4fW_3F#@$wNA@%BzpC z>R+0#=C~e+m-_1bROk1zpY+7ms(#WPyAqy#Ods0+mEZa+qyzE9vpDKghqJoL<0H(v zm|y%f*Xy~re{SR7dwf$5w|^g2KRr0w-{x4K7}D8nTvdP6=Q`kM-RZaY^DlVf#{Eqv z=g-d3H^S7zH-9Ns{Y&%JT@QW-}veJ z?C-DK_JY>petc~5{iXAQzvj{(A^v8ec}w+X`}hp2Ieg(KeXmdc@mDu~e{hff=fhTh zJ!n1dgVX#ka zSCG717eDE%>&x}YLws1})e~p=$;&u(tJw9x5s#ndIS)Sd{O-K^n?Ki;_=rzm?0G>9 z^|_y!&-JRyi_IIMzKW+#7gqeLo;ss`e$wN|mwvVX<#qMz&gse{{oLPloipyIJUy7} zt@6psc+{s3vwt?$534@!^Z4od`SFFHIHI+xUv+U;!gF6zKmAs>9=_8_#E0tQ5zg}Q z;7GTsyDo^Ax|QFaFVA>)|9ukgKQDW1cbIPKnd5#rKk)dhPA8ELR2MHr^PzsaP<|=u zhgH8iKdpx!&;F}@TC4qi{U^H;9{*BbKfl$*9v5^H@u9kSgtL4+IMS`^e1sK`FZ?vu z>+g^J@{3!m`q}Hh*n`Kn)Thf|b$uhm-z+q5sorcKpJ6qJFZ`tM|C2oRf5rE|AJrA{ zD!-jmc+lg)T=~RYuX^UIc$SX`E4@*k&m$f6^xOWv-N|=uqty4_vrnd<``5a>9?ZIV ziS!^Hh%X*t@-x<-`BAJd(Yj&Q#r)u>dG_CjzIvZVRln&GouSV@rU%n+eCYLydGca9 zP+dGi*Inu9rwiqmqIt0Dw;q0qeSh$K-+TAgs-8cecRr-P`<2&0e}t*Gqp^8Qb!Pkc z3`cYLN#A+$7h6wht?DQ3+a1Ik`Hcth-4E+ihq>M=pS)3=I-_``r=Hg#pXhPTPxJk} zVdvlLO~*FI{Ngv1-^gBDbIzvUUZ1?}L;V$q`d>bB(ECy1W?T zRj6-NPrZ!wS9oHe8kq%TBk8qZc2S>V9 z-F3l=#}|H@>vi>|XWhHCs{iS4b|pUJ(>KrKhOhEaT|Yf_dg__a7$53`@`-d{)~h)A zVp#Q6=ePUt;xnJqTKRq0+q#nLa9>hiKOVH6^jjUKUX@Q?#-l!UnEkV{>l*2J{mf5% ze;?x2Uw=s(>H8~N|F*9H&%VrcxexZYy4YMgiTF@mT;VKFfATZN8zH}8#i!3tF~85a z^NFoh{l7h@E8_7r_4&zPeD|l?|9Gia<&&53s81bc|7^@>sISuHr@GhQcX&$whPVB{ z42~vWUtVn{iB7$gxB_h&LLLK6qgAkFjXq{SzM1rMGaj34ng_rW~hN4m4 zok(}fu+qnV;oXyegmTh|LS-Q^!8$r@H<5NAKA9{>gv1cUQu5 zZqSExtNm>*I*D|47wIn5S=#4%VAXFQr{8|wx!c2@-9}Y6H@#K;)?cA@HVdt@RByJA z&v3L3Kk57Y^8Hu!U(i(Fc0pI7>v=BqbAQv#zRZhLPhaM%c$SX`E4@)(Kdkut;-~Ai z??3VKjr;rRgF1sQe(LjE9pCv<)$vlV$|p~3T_~SO7iN7n)<0U$>$3FQd2;qmH-6va zXP?+7pj(|M^87Uql&6z8dz~X5-4U;<&-S}6Sn2U4{r2*b>!Bx3J@w32@hl$?R(hknepvDO#ZTAkJbC4{x?`uV8|3@&=<<_(_N&W_ z-4A(sF!jVxpEz+;Pkt3wI+-^&9O?0sj{SF|z3$ao?fGGAI z^gUmG^G&a5t?C!7fAKw@^EmZAKC1n#zrxg8HKu<}_wyN6>zRk2Vz0lS|35bVzRx8) zx)PpqKJ~53Z+v<8Nj@?4)H7ejvwS>Q>5cOGU0=nw5BZJf^UI^ZeN0QKpMOYKf#a3^6~gotv^~%KdkuGdBTsk?{`vb{n>S+_Bo(WMAy9Z%}ZBZ zUaT%p52l_tQGe#eqdH!NRe$Eq4J$pq@{^A5XZs&Naqre@f78LMV);aUvzYy(nC^%- zTc6eAGpzL0`AL7*^W}GsY^~~tKe2b{!%uzZN!F|LgdR-2DxbWJM}6ur`)6bQdA-@M z&QG!TH-7)>=eHJh{di*Qi7Ui&uFy%uqoXdK#ZjL+oYhU9d12PY*5Rkv&-1U|d1j;f z`SYq@>z#Pj>l5d5`f48gTucv|4_0~gSxg-wsAo^NXL>_4(y3p4JU^KQ4Pi z@6?A+5AstTn#Xxl#dIK^II*f{zKYEU>Dw>jM0~npn0)63Kk@osU9PP?^Qx__Mg7AE zb_G1XR=PQ_z7eJ#e&#ct<>|wR@=KB5u5F3Abyl{(qi*-`kbUSNZLA55Muv zllxm8R`c#|epUTZpX-9voP6acUFXSlZ`=L0zjq$l8FcxU`tAcgeEz%7Sr@0C{>+bJ zdNBEPVb*73{na{j)cGm?@pZy?&hD=ao$tO+3C}rB5Aqe_!<@ArT5 zU0Q424?Mb~b@23!F!k{1jBu8pyo^WdsGAqcC)$Vnw4QVMci#1Lt;PM=w&(jb)4~?> z8U8@h|Ce+g@RQ$up8v{z8-Kn$YfD$c)1Uh8qw`x`A2gpB;$Ks^be$33b--$FzNFvI zlbioV|4SbAE%)zA*7rIj_382(-#nRDhpD$}ysF;PKGy|D{rtq+cl`qY^eO%OW4&Jd z=t*4>uiD@8c+mZEKXN|2^tZ|@d@dR|Axljo;(y&wAzPdc$t+>d?V)fx1i z^K_tm?&I{^di2C}pt^X3bYR7&3oG6zuOC)?b$*)9d2-dOf2Fmm)4>zd5m%^hgsF#L z=~nqsU)D3ObTe;Wn02vz$WQBee{kz*FK<+@U(R?>SHyE}(1Xs4+5Jrq(t%Z8J#m(g z2UBM@<}hlp+ea;Pj zTF>7XU+4cmytS&YeZDJse}$i^?{SlLvHM{@d~xdOtLhcs^*}oKV!C+h^2t+&cu+o3 zouAgTzklnOj%_XK=dFJq2cGA@)O8=ti9d^}x4W@*m+H*+@ex+*@P(iB{ha@@$87xl z*vr1%8FcYe-+fTWmv=w2j+c5>K6x3_hgqi!vpyT^uhyaCah-nid(A*RL4sk^<~}MFzfVPH$U;5-`}~(@r~+!?09MK z9O=4`mEZO`oka6NbBR|)I*Ih;#jwh&kLJhY2S4fB-(NrF&_-3)hbOk4xI%p+oYkw= zk++U`DbDtr7gpi*;XuJ2p7Pg7sN`>Adod9igN{$}H<`lCMA z0js&`@Kfybotu5(@vYVC^9yd+8FcwZU%&gPE-&_cEKd)n9v)P$a4ByoKi4%k%(~e1 z^V7P{-}m4AnmJF*WxYgobE=CgoaO1S_*GtCqIscu>hLx9xAzCH`0$R_sy_Sw)!D!9 zXRdE9XfB8ktGxOwrj8iS>Lkx~z^sea`DtCh|NZVe|8%3OpZvMr!AD3B=J{f#a3^6}tEx2l^Pj(GfZ{az2=;ciE_R_D!?lRsbP{x%1FSmkqG{LC+nQ%4M! z>fyT%D4%E@ev0kyyMC+x{f_s?-*Hh_q|4XT^}3!OtoAoOywp?Ad=<~~@vI9g-723t z70>z2PwUy=FL+viKi2-=?<<|*@rO@e?7ZNwdBp1Y^zc#-52{y)2Uq1~f5vpp4doN< zLw?fpdGT%jtnYO7wflAjJohVg(@%V8E^%V&sb_u^kNVW%NUy5r_25;y{8YEUZ}ISr ze?R}pmvjbw_lrKXkL}yc=l!5(eSBEu)hnc1)#<|IRr%yu2WDN&m-O4uE0=zA<9Xuu z+2~gD=uf}#^;c-V%|`3PS$%U>e05my?6dUSd9wef@7G$@|GEG7(Yt=0ug;UoZ}Yeh zt~)X3RnL6JvpjwHP<|=8E;#Dvr*)kt*MHo$)~ZeiPdvL0-pFq}`njI`2uFSDaHLn& z%?t4=U4E+DxBv9_k8G{#=dJkrKGru6zs1&t_?wNZ>W}(d2dw7ye8*3^_V;i8*%7T( zefIyqbWZWr`t+gs#1Mb8aaH|MpX-3t+|F5k(*4o(hNph@7uvtn>EMY+>m<*8pp!VO zN4G+{RehA7?KdwR>GG4l{rT?eZ2Wtveb*llc+Q>FcOUtxu5X0+t0MhX`}NK0@EPh) zOzR$L-FIF$Ny?; zRsYIEx)R>f{cSGyflgx1tDgBPp5^1gN^g{RT@XKYhtHSi_Aj*O_lHjJN^~l}-N*FR zJn~|C(7dq9tIuNUh~cbG@>~bZx_IRG3;$DB;P;_hd#CyQd`b_d-}3sP`NR-^vvF1Z zQJ?F8qq+G>*XzMOe^1h#<4ffupJByw9;Dx158mRzw{NZL`(JP4`n$WoT}NUy@6tTo zrG5Cb{d|V{6TNQZC;fF>*Yb;LxATCXbe#v!{F_&`R-d=Mb??rgi=X=XlgEFbd#Wd<-e$$tS*kbcQ?LB6 zp6ll)KEKZV{4tH{e(1v!TTfh}z7eJ#ex+OGM}6wB(i!E=3oE{L_$lW1yT5coYjr=a zJfH7Ie*Xxs~{mC!4R`=s&Te}jT{?zBE z$B(+c5#sMIT4zg0_fg~1gSo%)+(-9W-kgvQtn%uKvwS?5IPyUe#^#3E zFXlHt>G*xN51zj9`#5&|N@vh@ZqS3CZ}}+CXX{#zo){mhi$_QYR(!g!;*Ik9;fSC6 zo8RyH&j+_w_04{NkROSod6(AdUsHX2hSfUuCqK>Q`|ytZ_QubP>EKnde4@Tt%>JdZ zb(Z$w&-U{f>QD6gIsNwe&h4JDwY8}G|L4u&d`R8uJkdA8)Wgqw70>dMmvN<^c|OCe zi>v+3uU~&%XE@*WOo6CBM_)C$_(!S&`)p0$r>gOvz@to`5I^yAN6!p*cug4qu?LLnD&iSmr zyVsxn;?y6-`bOwFV8!PvKdtXR{LRB&&|1~6{7zTGb3UZLa|RFcpT2xz>Zxab6p#AU z;YhEl^LeCG`R(=K&R1=Gzv_ZByJPhEnsd)L zSGRv%uk+ydzOc2ms?)&}(-Bvw4_}<9PDfo_;Ve%-K9paI=7suGSDl~g-amQ7ulJp( ze#QDPO5oYA^r8K0f6MC|A^v8e^_J?*_VE!`>+p-8^!fF-mu!50@YRQR2Hna}j}LXa zVs&|X(7ftU9#;9K^^p!en0#|Xyu`}y8>|z*^^g9Pe z^7st1F6IkAt?TiA<BobqSLKsO4`w}Mx-jdrvHsDz9`F3b_x|@QPu$s- zR6p+teF8kb=3M-wuWl}RI*IsDT|B~BJ{}zDR(00}@lw}y^HbgV`h;J5acfmi9s5=u z>ZdPHADT}L@vkXdy3UC2I$$-oeaKI`UY}fa<|(aJ{gZ#$mGG+l?Rg>n7Q0V$5_4Yl z%#Y$xU)D1o>8tY@$|tJx(|SH{_~0Y%*;>>uJf$n(Reo3d+j@MblbCa=XMPlq`m&z! zNMGG`LHR_!@Y8zs-<=-ZcdB~+|E{b0t!|aSzWsf}Pu#n; zs$aeSM2g3^)OSwV$Ljh+TZTuzqS3%hglzI`Fw`@5=ZZ!>^S~0t;PMAeZOktx4A0JIX5e|-cr3$ zpE|7ctNraf`Q$|#f8M!j-@Y^K5B;g{KH@?9-RpDt#MD#I{3ssvsl$<8Rp;|a2haY+ z=lAPBd2Cxk-FkRpI^qiTjc``4T1VbG;-xs-Z(dlfV}J6KzW<-(#ZSLyYxVtvd4B6p zefP_LR;LfmCx-af6fRw7#CILAnwt(k#l9c;ssF7zs($%iok15r^_?flg;$Ks^be$33b--$F`;(t^y`FpX+xp*g+y8X% z#G`eR=RVO%Og(iv70&YX<3stSXkMs4b?t9{s{8w$AHVOZt=0Y5>%Vsfp8G`~R{NXJ z>U0w6Ky~p_tor5QQeAvLL-`8%X8q|s zebv6~r(=F`uCGrX%IkynCqMD{dBx@33H9?{(;0O6L=W1>=A|p|e$atB>X(S)>E2K7R1idfq?T^ZsCWjxUvue1;Xz^J4mK|NZVKUeQ|BKlZ+Uhv-)OTfh5> zpZnW&sEc!6^~{grQJ*>-=~Z?8u;R({)Af4)`;GUxU8AVa{{O}9BfqU<9(n5};zM=u zEROor;jC`*To=r`*nZ=ub^W|^uOnXEsOsBq+ZpK>J!s$4GlxF+LtP$^4y^L(ke+zd zrw;X{j(mms^+9~G>*l9Czwh$x(_4%B&i>yy?E3o#_lqv1%SUdeNj2kJ}R>h;O4_p2^GzO{ONzWq7fVLJNhLw@5y_s97zpGXI)izAxaOzz+EttYNfA3l_a@|Di+=B+cUGvb>Uj&%7+*M0c=FaC6Eb-wTW=&p!Y zJzrM)+rCyeA3ZplcX#Vc*BS9$2dw6^kNHWL9~b@o*{#+7KYH)3gvYSFh zZuqjm7~jWG4_=~Ot&(_itcyuL*9LiN<)D?in} z9{ju4KBBd{AD0}|mGG9H-}<2W#L&E(jjQU9`dkOB=H?ea>3To*cIO__TGiirtFDBn zKlPn6&JT5c(0pQuzuCB|{;1D&z-n&$o1b*~z2(Dwr+fYKvIleqUHsHnPvpOKvo5am zGQTuV{ZTxt&j(obsq+)h`+Ha4=(#QBeth(8y+faUtPk>&zsdJ>;Cj^|oz2Eo^+$cK z16FhMgP(NyamC;D6GNR2o_Mqlo<4kWqPo7_Mfyv1X8X(wt99&8e$wynnyjrIf7Ge1 zRsHgx>kK^osqb8OKh?$dKb=H;s4gDiEFTY!bgMd_;fTji>pH)`u>K!Psr&m&zTv07 z#|NIedE~1YA6DzACnjGE@x@Rc%0u}?*8$bD&vo-tec$zlTRr>O_Am91f3+9zs{LL0 zZ9PBln2#PbFRb$FvzR)gcvjbSz^c#VnxEG9`uk?D+4y~rJMP_;@SGc|U-_-iT;jyk zQ_p-A&+_qLr8mmE9$4}Dm43SqC%vQ_tbX{9_Q~j0bEe;T)&8ajt9e(=TOZEq;KQoV z+_2)&;iq{0zr|R)+&;5<>yjQoLI?Oq%eDX3L^{K<`pN;jys?YWEQ_SzZ zuD|j7sP}ngXVA5eQ=gyqGu@Hjc&TT-%#Y$xpSpG7NH^a*YP zLzmyytNeDI5Fb`~^~6~|9!#Ctn9s24^SsDU>-v2D>!%#rTGi>`iAU?;=^Np!9^DG9 zQ`JZL*?#lFO1JXc&pTJ#@K;(Zzc24^7p~E*=Bf5KzWxfUd3QH&y`?&%K6O~>dwlbg zA6|bS_}6!At?Ea*uGs z-`9NLx&4)!`d8l6JMr|>gXt%}c|6aG)ghg03YV@k;^%c#bK~)oF27&)-Hm_G{j9(4 zO8WSg`uw(U=!(7mmZv99J@w3IJj>IEpX+9fUm+ccSLvkR-aq;Lb$_C@s?YxYeEOa1 zo2x?n%|`1j)gASz!%Dw;zO?_o_NV)`R(^lr*ses^`%bA(*M3%azx5}o<8Kzyg|oW& zuVeUvxfh^KB|SoPb7>9=$FxF6~t z5Uc<53;POmp>?7AmHQjt^Q<_L4y@)^uQ2=eEGMBU%ye+Xa9c3eXRDky!)GIKBz99#ZjL+oYhSppJCR;)p_E1@XSZu zr&06s`yxkj?vB=~?)yxg6Og|7JP&8xJkB9} zJoVJWlZT^xRnPv4H=8#%OntHIiVGh#1Mb8aaMnpXFk^fNB#W7sQlDl)pIT}bRFgq!z!;1XLsMM{9LI9`%f_MA!3T>bpI= z^w$acoI}==7pLB;G2Ny8=76Jq>%gjyul&U0*L7d=vu!E$^x}rW16K3dXZ)n=^PNY&>vkLbKCJ(DuS37-j{LS>Vm0qSmn$D%J-pp2s~u&B?D9 zK7Ql>cXU$!^T6`;iFLdmz;Ai_(0qybt0MjEuky(g<3stx+4-yUTYkSAZ~Xo$>*V^@ zQ?F1TzBo~xj=H$QS)P7;D8CfV3rGEVp4gY4-PRug>U46xjPVljp!vj!*}uCn{iQmi zKGy*&eXr;EN!R&(@jaf`TGdbNAJ^}CzVvf{>N}_0H+A~v5+~wAb@42Y`m&z!>^iOo zR(C>|eUh@59dC_TH3y4X7W z6!YneC!f}+_Wwx-_6i>V=)uZQe0B5DgLGh(SD(ey8O5`@c^y^X=sY?4!uz&Xet-F} zu4GRBr9NHv4Ik!w@-X$PeDcJzd_1UsDY_n5^?RMbPwU!$2mHkKTC4gQ@9Bzk^{2jm ze&f48>ef>ir=EJ|t9X`=2P?f%UO%k(uA85(*XxtdoqPM%s($wGbS1i#-?_i7pMBPo z7vn>9afSL}#gnI-{EYD{qyzCXw*UBP9lu|7*KhSNMD~38j*Gei9nXvOq5D7&{#)JO z=7X+d7Wpyi_xhQi`2K&X`nwxs9N3t!tE8|l+cUiz(`nAd5p%rA|tqc3$cu6Xj0 zZq<+HJmDukzYjg`s76))=(f(Ve?30viTN$gy7R_*^vsQ~4&`C$%fqTqK2cvHo_wxr zZituYJm4o?em(8jt*ym*bJ06HgD!sR>Ngi&?r(W|F!ic@@-iOvshdN8#_|=?fq1Fo z`uVBe|7vG#ZSVb0YOU_a$$!-ubn)p!_fZ{M&*O?tBAsgrtv9Pb;;XyPO4oJs6W`-~ z>%O;bteJkr$6}mX8NVx>eo0u;THXpVsw$?3WMiCxiX}sDJEA z{BrK5ex4`h(3ksP-ukJhp81URTQBPw(_Pw^I@W_(7pwEry7u?u-}|IS{hq{nBVOu= zE7WIQaiTgMb@5WH`sLwLU3~LG`9%AhpVsyI`@6sQldZ-5xN`rlfM*}4uG?e(sOy8~ z6GQyX##Qx4eXavmb9?^cCtZ*Cx4m%V?^peM`Ti5v>w3hA>M-lGF`r@8$Jg}R?^oUJ zZ~Gfm>KEO)E77gy^th?^xBd!q-fErXjbb_#R{i)@J+GtUIS=?r*Lm{0w?Cw{s$X`; z&fp_{>U-Rn15cmxMqOT3Ds?Pki53u=7_p{=U86ui_8B z^~Kgto_ZplzSI*Z(#d?rshjmGuKIFaamA;v&To0|4?gow{gWQ`E5Fkj`rI#l(Ed&z z#m=#uQ%nb1Pdvg|J{}zDR(1Wb;<H@s`(_iN7neX)M});Ev5^%C)+x_A~x zed=&lH+ilLW?k%gE&cXipFAD`i99e&bx zo}Bc^qg$)`vHjzRUBAz^s^8|aUSiIxp846h>X(PJddYJ=FzaIbkDu1%_h0S3@$X$P zJf(Nyd3{3V9-F3l=XPxxhd2+q$oY`8{-~0Biq_5iF zd4A)Y$MdXM9p=1MK6#@!b;NMgU)5a)9P#*RU9Z0n-g#5P!4KdQ0_Y`}hp2bt=D|-#`4> z<60}fkNRMrz#OCV1P|hS{UV>3^QvclHm>^R;jCWr_&m~a-TcJ&`Ob;oIj2ecJmRDq zcg9FR_cvYp-F>E$nAf8}^RuyjsBdYkFL5+KzWv5e>pM?wzrDX-<@1QcwsmFt`1BxM z{$}2N)=v-O!z!n9 zn0o4&AH}0SbvV+i>aGipczK@i>m!FgwzaD7^Ts|IU6}gr17Gpv>AOyKn0i${dE!|< z9@M`S^~0*)_4Cv9^W&B`*!ceg9(1FwM3Zad0hyDsv4=?joJj+jB#+82NT_4Q4*#6_E_4xgzgZuBF zs2{#wz;nOoL9ajP%j+8<{$`=|mg>#+@flX@sPmJ)pXVR;JGW`A>W6LZN_f@&{6@spJt?=!`-UsKoquKd$834g{dR9&-h8-^X1Q9{ELn1{o2<)wBI4R z`1GLjDEGJZ)afMBf$HL=Xilh~E|g!2`eD_t&QJ3=Pu~BYhqhMl*Z%Q8b_O4v8>ye? zH-7qUJ#}&Fsb@apS)M+8D8Cf>2&;bQ2|r!0{dbFpZ+yP|zukT>>{JdCy>Uy6b{WkvyQ*U=;>n+up z?c+17)=}pteSSZGpT2|D-}<)Rp|9HC_AkHbxGfNy^Zgx9?Mig3^P4U`zUvdK%Zqbf^~{fAdNBEPVb*73{na{j>^FYm z^Xnr|`T0h5zQ61ty>p~*|MJy(`sBrl_?wNc56N78_^y%PJo|N>z6(Zv>o=bm?h?9& z{;_nO5#M#dYHYsnldk;hF4)pq?f=XApBwMm-}tHTyl_9&%_A>P#NTYR&Qje`pE|7c zy?*8=|GZyy)*nBvwW`y>6OY!x(>KCdJ-QWIr>c+gv;F3Um2Ty?{k`Y=*1K~f-}wwH zzxita(v^SK&L_84_4TiRTyq}a(-%9}lV=`vd_48k6DR7=e8#Dp^(wCVa$P)F>FMLA z*!x)T`OHSYw_VVc@c5Sc?iU}`&Es)JClP;lk?vBRrG2goR{i`)zdhgm*2(=OSNH#+ z({o?)di3KZnhRz<^)@TE&QiTmUtUM0Z-4TWuHUym_B+pOtzN(UwwwkVboeRu`zNn|`?0On{h)&<9<76?Z-ldYbStz@RUhSN`^^h0 zT|am8lfK{A+iF``8~Mc4Q_uWtY!0Z8t~^wq#nh?# zJns2vJ^TCG4|;lQbwBpn(v|S^r#?Tc^F)8nDW7_q9WPyH#CILAnwJhg#rFNv-m>xU zx$n3BeKUCWYwD-p_|RNX9;RNEPu?g_ol!i}Q+FLuK9OJPcmG!8wY4KYbeGnme%McU zMRV#;-RkuqzWZT5dNB1iD_&K9)aN>2H7^~0iuv`f|9o(3RX=oVSHk08>hp7Sp3q55 zz1@xJFV><1?(*@pA`1>3cu+@h{r=`g_OMyTf?1=QqCl<9^5|=Dg~epN*@2c{r<= zJU+v$i`CO_uTL)hT6bPuU(QEIy+VB>Og;Qcx5|(DvYv6Jn|brXtcyL~`Ds1p$>VOd ztx?slcWUpXi%$>gr!TKhUcWrV-(93nCu8{v@nOX?H>`N0^W;nby)dr|E1r4|K!Qn@1I}# zdH>}2{cn$t)L->Halcb9X!o_gl1*mYNW`shOWrD#1^^_z#EV&}=-Z}!C2s=n&?$I^A)JICY` zb6)k#SMe+#4_11kJfC63cisH7uFo&;{-pg|tNK3Mx)NRd)Tf*K248=LskdruJ^dNW zSBMWQUS3DV;|o9O^7{+lI<&Q_ANa$a!AE@hV!C+d5j%(Q5=s3&?^|D0##?|U>)`fXj9b5{A}WjyLrhuJ?HyDnJu@tdF4_4|X* z`jInQt2!OLDwa>wH;dUn8`GcVSG-vrKEq1S3Kc)i5(AYE$+vm59tbY(@*;Od?}xO z;zWFCF7Yg8UlnIQ^+)y5{PgT&e$sW$9B}WOH>$cmyegJYG?)8Cr^4)?jp@zuE8eV* z>w=Y@IzQ>#_ou%8g{@Wnum59b@Cz^X-7h@4Vtttxr=B@8U&Yp`>hg50n}|0;I*?BG zIcNEa=ktd5ey(TK_c^31;Q4tmb*uA4U(O{DQ*U?URdr|kTo0_~c0uD z{_$hF5?%W=^{f3&Uw;*+9$x0Fn0{54PhBy@8=>`J)nA?8o-Z%@+^yPD{La6>^!Uhm z+{b)en*-*2@~O93@v8cxKGy+9^YW7}ANTzK#Utl>ozJl9vJcbmeb*cQ^2)>8ztsP| ze}C1;kIJV}pE|5`gE-zn^Rs)2kIBkV(O)i7^bc~ z%(~b*P(IOq<0oD3SDo6Y-(CxWZYUetamu6kQLj z`uW07>+<`huYFc)RX@4^dH1IMeVX;m33J{mpFHs_AI}_+&MeY_qdtCG&-;VNeCrmi zRh9 zo+n?~+OGg_&;IUy@EKP3nXl=$=garqtbe|ve#ZOf{B|BWr>$=u>$-1=_)uLu!dX5Z z9O+hd*99w{bC#dh_4^PIz3NLF|387VZ{3yf{@d@y6b=y&+}IL?e9t6{E-*6R&_c#-)Noe!>5y|u5VSOdrkM77mn8B zr}_MR`L!QxcNc{^38?$|k}jmL-+kn_ ze1&vi>gkh*ReouGq(cuT-`o%{Q6E3W_UBW-`U|Z^T|b_9v`+Fy_hD9#ZiP8##jEnO z{pN+0o;*M4dwp@$C;I`Se&yM{1Ft$yu7t-|`q1NpzP!E>;zNA#EROor;jC`*_zbfyR!_hA__Md|e*1Yy zU(QEIy+VEX;zV^i>f%`(^{K;I-Q<}UW?k%c89%LSfBuJS-L_HHuRf?V?AuD;c|;E% zs_T~*n{O#v4`zRrAMvU_d2?r6>GBhwA3HDH`2X+hd%LcT=W&p_?iU}`o!ja;Ctdu_ z###MYp7~q{toprfOTWDyJnFPVnxyk)_V?}i%5Uq#oJ$_M4p`;YD;)J@J>yC@^R5SG zUCbALT94m%`{u^iC);1p9i)p-57JeK^7gkpJxB*udG*9uJ|0Y+*;v2p8TF;#&XZH$ z-G6h_{=ewzu1I%up5z?qch>2S=3TYEs{W`iucMle4nM`-ue$Xs`%YKCX#M*K>3hCS zedm<(1fTvWPCfk0XFSW(SMjU7zC?VeUd8<6hx6p2&wNH}RX^j-U4af?=|krQzv<@w zmZvAC1J%VNoaN)ek#1Gj4=bMYfS<0{-#6Uk4cl9*`tg6#8FcYef7Wk0iK$nule|$( zCsCig7*=`p(foLjpLFf-XI%M;MsdEMeN<=Q*{^hBooiOVM?~Q9nPe zYkwaX=gCQ5?GC43^!2-sVspqlZ`A4GrJg*#dWHB^T^%`8@*Dn3-2cCp``i9I@T86ZU&eml=t}Nmb)LA7_AS0xT|P1ARnPn=9`&ii zkzQ5jGpu-C&!ykjTW`4IW&NF5^-Fg2g87^esqcRASKU1N=p^Dpb@2#i`FL=oTh(0` z#7kX%@KfFX``ib9wY55L_WF~qgvU4f(0!zjFZQ@FpE{%itGxOwrj8iS>Lkx~z^seC zj^L+t<-hTtZr`ZBk47J!c(e|lz7eJ#KAl+{^{K;I-Q<}UW?jrzep=T){O#{;{CkoQ zT(2|iUwnG{?0c~}#2!ET>EY2+&%9V2KM^nUVs$!DJyBhsx){nQ^1C`uzIdBwwib0i z@9@p~bjAEkp4hth@;R@1mA7t%*_S+ZnDr_)m%4r^pXj>zNyq1x&pYBttwsIn{&CN) z@56HrSGs&ur*AGO5Ain}XZ2@!=5rlz)Xz^mzpr`44Ib1a)wkTKuRyo*v)bSE^=B;a zI@HCm%B#a!9)4ogpLuh`)DiQGpLF=W{~<>-s`}3L2L#@#`&)m7=7V_RSse9cJ>%JR z_zbH)`;ed3^ZNO_e|^W+s!j(_JX!}&-w0>*=t8>U-Njiw^TKLf`;(va-G@ETmp|A! zKB@8<$|qLOm-2@{qyMF`*XR1H_0%iWH^Q77ztWYT<&F4Db<7JZeXo=GX&ruVKe{vQ z|4Jv0%Z;?T~(a~`F>^WXZ= z^RxR&Co%QZGhfBCd^}j`jq1xMf0l<2tG>*e8|qKwD?jP* z>rwZ3U2Aba^y7&~>)`1d;jA9r5n6A=%ld4ed12~g%uo8BFF*IBpKVn2+5b<#`R)9$ zo_XwZ>m}ktb@5WH`sLwLU3}LCS%aa(72T;Qj^b@`gH`=Q^t zhL?JDQ`z`rybv>qB~w&NYQg z*BSBiI;y$p@KbEx-{$6h2dH1?xm^j*{YriP_7A?f^vNfto_gkIH7V_OTPJt)~fFJzww+8sqcQ-&+7WDD^A4U zETq3wZ?@0%z-k@mEI;YrWWC{^UU9efFW;a1$^LQu@bgPQKc?U2ai7H2h1I;fn_pFb z)aN>2HK#g1={jfL`Q}HrR`EglUjbb{m;`4={ z*0Vpizj@>LQ6G6gSHi1af9p@b^+EHAVKwh&<(IBA;=2x5&FOKSetSK5#pV6PQ9m#K zKLO`a>eJ=7dF7q&=7V^tr=I!Qm@d>uS01X*A{|)u@q?e%^Yi@oKJv8Is=oiNyAs}> z{oVcGGpziU=O-WizR#Y|i#fCr`6SC{D4$rJ-_DbdZhvfRaX%i{e;sA``h>2>k9Eyy zE^#9MW+DBhdb5452UhDi5BN#n>+f^Vx@~J!-+AM%NZ0dX>O0rz@jcIPb$a5|Q_uWt zT=mPtS-s@xuja?+13&S7-}=YzxAFhy&etdWqX#`7(wCqHs_wcVUh49NpX$!vy}xik zYgOOY|NFW%`?k_|&fw+x)`57HUiOO<>B(m-KZ~Pz=-@$lXb!&cQ=Z>9zkK8Ve(z_y z0$o0(u6?ggSFA2i52hYo=EaH0&$<{7%2!zN%nhr4zNX(dTyJ>W^Nwu)Qa|{^eMP=` zUQB&?Jm?(pxH7-GIQ7&sU&XV0JXq)UW(DkNZp~G4<3lU&XV0 zJXqI=e(-5^80@M&y9ya?^J$Ue}v}UU9`@sIxWJc(sVAbnjC|~LF6VJJPtuJ=R z)bsT>-%?lq$nTub`l+{B@v8cxKGy-OdFk*|?DL)9xYf_JR-Z@gb4FLftNeDJsN@d@y6b==9zU(?{J!+=-7)tg&u@OFKEL@&cNSAmU*<>gs4wdo zkMz~~4CNE~#ZT+)w@$p}RsFzrKW6{G=;}P7Z>|b+&drLg4`+4FS@G3j#q;weKh0r( zzU>juX|3+Z_E&TUUHsJde5nrIU;6U&VCq%*@J`Ds1p$^SW_ z|4aRb$9E+>{i)AS=ZU($oKGI&!z!;n!m2;>8IN?;T@RE`^tk7z_4xI7XY^kfRNw2k zuE@tqpDvzt-5>ha!>jbv=~PHhz0y;si>8JLuZ4boBfAR9#-|aUo9+rd}1xS4h9AkMh}{v3_$y`9!|*lb-zJ zzrFGIA%1&Z5YKsB>1JKu2vcv>*gDsAKc8W>o^vDjxAXT-Z`}CzQU_hXUjbeFmA>n< zf7Rv1)|ICRQ%?-_iAT7UPp|6Byt$$NMAw~u`@DG1{}RbAsI}G2Cbsz0#>zPO1btK|Lb@42Y`qbg9Zt`3gR8L)XeyTf9-usRIe0ePg z`nnJvT{_|l_2G*X)#<2xw*UdE$!)XfX!6V>@?J)d7*`>MXv-H(&M)D`fY)2Zuz z*~jW)`=3rC{;EjtU#p*wuv(ul{KRv9f9EF0w~^I%ytH4zNY{Pfuj|(5b&EI=ADTP= z;#E5QRQG)Oo0s3cwfa7q%f8(i?h}6MdmhF!hkfnoK!5U5Pk-jcBg}q$c_=?ZbK~iU z@`-%mCmnu#{Nj7H7W@CgeL90Ke(HKS@DWd(=Qll=dR0Do;#oc()V~z`iD%c*H^Nyx>nGCB{L*+<$GoswhaNx0-v55%eQwuU)${eY^C9(ff2*6v zKDS;XK2#Ua;;2s@&gv%5b-}EQ`NdD`^82`Z_g9AM{(o0I{q&&oh_CYcMu-pb#j`l- zQ-`y<$>TH3x|kpQv@X9+dgRfKs(x($dg@(2U#{x6xvZCHKBz8UidDZnT&j!jdZ2t_ z<+t(3wDTGbET z+LiED-QW5vtma*sr@yohf3}}rP=BKR$4~nDZvCykQ`N7$Q&+%S)o=Qc9?UtbeDX$d z>Wt!%p1SLR@`={tr`YSkGe3V!Yf;yaCmyYXr*DL*hfik~M}6vWRyTR(g;^I{ho548 z|HubVZ&dY@f4X4z#}cHXG@~S$*?We05my_{C3i@cX6r>Hq(wIvu>~Jn{3R z^9n!Fe3`e-W}|hM>W=zc2dwn@!cV&LFZubMtyP^4o_Mqlo<4kWqPo7_Mfyv1X8X(w zt99&8e$w}P@ctL~&o6zReEdmWi7uqC-{Z#qR>zmemxuW3;tEH+QGIuH%?+#dTsJ@Q z?C-lCa8w(`{V*4v*gE10^^I^=uUaSb*2{bqFXfvTR{i!NKk@pvBCoAo{ORYlk<<_W zTAvWl<0I$x_;5ef#qKAaM0}_&p2bn0I-J!_9-m>>#p?XDE?-Xk+VPF5{)-26243a2 z`$z|0ywq>=u8P)Q+CSUJM_8@ny7@`p>y!J&-_LLM_l23yy0Dseck`?2kNR8(tmgDS zNBZqNxc>>i)>_r+VE9AeqiHO z=QrKze(0~Tns;fQ{?b1D*?vAl{fX6i;_r)JdY$eVzyE4WS1^ZtTW9d+?itok#b@$5S0g`+-xTF>`| zUAgzh_rH&PMpwccohNwKH;+E`M0}_&UW!$}JY1@a@48Z_imUy7-N#-t@1K~XLi4*G zJn;x;`N_+8w2r!Yp?o6!^xL_9`WMb@E$aI9iAU?;=^Np!9^FLxnV*f9_L~=u`uU0H z=b)Q>Xume1`kQ{QPmJeW$m?*w^7+y{e*P5GgQ>S!@v8cxKGy-OdHKapy8PO)|H-XY zefIZH=<=Jcc`~mKt9e(=uc|xS=el4ur#e6Bd!2Xm_9txc`-=Yc)VsbfjBoVyyI<<^ z(7Iwg`P9RchgH6+CtnQpC1yQibHnTx>r20#C%^Mohc&8te!q|VLJy|j^5$`#SU(XT zs*6WB%g2Kw-Ky@oV8!#e=cjf3e0hg+`u}(2@9U3W|9jWUPwVhA_pR9dG#|d04%8<$ zrx<3QZsx^wb6v4Iezguh#IKN_>ij(In7)GhaoD$eOxHX)2S2keR<|BKn0n$w{j>Sx zjpA8-bHhs4{pTlr`+KjW`v>ysdp)Qt;dy+dK0o=ZZXS6$iTF@mJd2|~bvUb=Jl6%Y zF18Lo#rF5zKl!poRnI>0XdOKIBb?QvTcLHT`Y1o!Z(dmG+MoQS&+mJG^M$QdoerLO zv<{xW5zgw-9ijC`ysXdmnHQ#xn6Lb#@B1O{c<+rrUmp9K-ihaa(SxJ&M4$QSK{}g_ ztLl&XTnDV?uKf1?;Cmi=Kx=hB{C}g?=UeL2bxx~`-48m6_^Tqlf31E#!)kqbeu};S z{pEMutc|RG_yhYZZn~at^SaV+eD_D)dg|iTQ_p-A(}5L_F06Q?yncwE>)XftRQG=D z|8wiRwpR6rKD;a8@sB>_r~NE%9(g*6_)uNE6s!KsXFR)(>w=>`ep=7p7r*bsd$v|} zI(XvQb@YvJR?qq)wBCrPzLalXSoQmPoS%4pAL57pqW^uG&o_PlH=cfTLXR8!SYE6z z=c9`ctGxOwrp_py)pZ@P>T})vw7$QWy7bxmwpRCJpF_G5p8nLY&J%s+g7Pr+HY;9L zf7ItX;Amcc((Nv zUB*xA^-mer*8b?3Pid{*U%B9hoq<>FZ@THLSe;H{&a0mJDxT%z!AftG=QFJMUeEE< zy7u=KuX}N8bwBbvsrL7t^W+CR?tbu--`=k}_MP21_1W+1weRg`y0e(m>dX8n9`$8C z~P5J4BbC^!ev}R+krhK9#2jQx8ubR(W+;@#G=?2$OGa zSoQOppLDz)eDwQ{ZLRkAm;a(G<^H74Py3jjJapcunEP4R zC!hUQULQZ{`FVcN|L@wJ;|m`X`3s+HX0BxH?a)myZv6 zIWN6L^H(^_AMwz7X))stVv zsZ*^l4=X*_$xr&8FW>O^?z}o3JkNXbF!#5-K4?BM#J{F+={h66>wwkV_Ax){`n=&A zAN|SJs{Vn`bS1j@sbBeR|GQ6g5>rn-^GoB@5xWk!sxG~%pD+Bhp7+1^f4~b{tGeI! zp^KmT?jwKI#m*c3^dLU0^6H7Rd^~f&)EmWgAb#rcg`evFzoYBC>kh5e^XUaAbR|6d zls@Dqf93U=3(74_X&iJbZ`;->JgksJNK=w~_u&+{!kSnY3qdtA{;qyyE( zBb?U@UsiS*NN_u+X*Y<$1!>f3e&efDYU@{_;n`pgC8A^v9Lto|&|e69nI z`qOXc$uC{~>L#gv@)NoeUHjO&(0xe1#qNi>=!xk-b#aBWJpK4kektmQ`m5{Vr@HfG z$D^LwTGfw#OIO0;zM=uQmp#r;Zj|E*9GMh`NB`@+J8Ut#j{$= z8zt5S@aT=!!P7Uw)WfG!;Ve&o#jo=E63q+MQ^)?}r@HqCUv-y_zwdL@2Rj3=dcJgf z=vyD(eWsI$57osZoaN)ek#1GzGpu;7KliuK(>`)ycTAm5&No^o`|u$>D6em`kp5D= zQJ;BXr9blfD{t(7P|WZ59@mwuuRrze-`wB&%q31tJ@w3w;!$7LGal)yyB;W?$S;0c z&;EYP15ax$>Q~*bE8yu*-ReBiXD)GK>Zxab6p#AU;YhElyB=8atiw;S*ByIae^<_J z%<~!YaU<*dQTpxo2QU3veBw6z{q}zBV?MK`wR-<>zyH`f z_4A27XLRbieEydGpxM)=R|SU8K9J&TPNyg4KF_;U|6X zSAFp{=d@P!S-;({&GOq^)pcZkmX8NhXEx?D)R(%I-#%}6`;)r@zw`5*YJXRLTi;yb zMDs#*@hpz|vYzqmI<5y+eU;xH@Aurs1UN2t#8(j%+>Hap4`$;D;=T*=AC?55x!;xN9cU`dJ@r$3<^?BOu zxA*T;^7n#=ZR?73@l&5}p5OBBvwnIo^{RaG#It-nsDCNyhgHAV+5B|<-mhAFQ$H(wW87sru|ge&YH2v^QMqo-O71bmsw` zVJ`mZgZ3}q<@LdwQyzb_aaH|MpX-35x%o-g-;?~$&vXU%q@_^KRQn;y(*vUW<2UshuJ?H>xcR>c8=P=eBt-UPJeo9RX=&Xfet=>NY_4= z$A@$h@i!Z-vs8D~rw%LqYJc0GPd>c=hur*r>#e#HzwFo4pY@wgV(RT~On<4)Y#*Ot zwNB-?=gUi?&IkNn0nga7at~X6w`qf-{YR2*7JGVDF>a_TGi>`*{||2{g&4^ zLi|;c{;K`@W_9=s^(We&{G{)k+4KK-rDH!h<}>`ktnX*@|MT+t`-)5Z?`wL0x}s7uBV!tAN<7g{`W^7v+?tWW3K9q>#Y;5qtANw zJ$-qxx_;*x9zAt^;t?hm+NLtj3A z%Y9ag&b8$ZqAT>I7$4{xpR$7k=-mGJz$l=@zuq~H3?1?6GtRr%zN z;?x<%BRzH30p%0<#ZT+|JnfJF&(_wWe(2F%fi8aPR{I-Ye}$>1FY~jpeyDG0tS>RI zBkN-8@l)*klz#ZL$2F?@(!b}X2R)wL5BWs%L3Qy`tor5QQeAw0k91r&Kk?^5iAR|HjP=Vyd06Gup}C=aqW#HF`d%MD_Bs9MOZ9C>bp<^AsayH2&s+{rlFeEh zYfGqqWous%k8e4Le){VAMu@*zXuYL+vwi%A)jIs*Cw=G4Cw4rrwW=T5KhE6sc|-bX zo$CBHk3MlC9cV7`EM{L7XFl~u_0jzF>_dLi<@cvvcW|SsA9X_Sq|Y~c*d}>lRX*z#FY}`~b;ag}@)h#a zdfp#=;^@sd*?T*`Um>2x9xhqtn~GplP-RhS2v$Nn0ax9*_S+ZnDr`7zPVx5C(lnh&VxU_ za^v4iUD5yCVVB>0qoY6l&bqnq#W4Be3hAs`U$xJ==7#dA>v5cZJ5Mg((cdspfA7n> z0-x#AwT|-wPafa%hCH5n>fy;Js>7;Y<(Kx+(PwTbpFF0twnvuOS?j) zOP?+u<@NDfoQMz2C0-TjB+`=?!z!;nnjeoJ{G{vmAs%+%0gdYY!^`g4JN4nygR|#} zIq8Y%EX8UbI8Q<$xl4{`-|_~-l*<}|G$AQq^DnA46SGX%j3h;!;^f#a3^6}tE zx2oGGu;NwEm(G))fAr&8tNnlY*SZqjYEJjjezv~Yc|#`=f3wh>a8?%|R(-Scd)>!R zx_-avI-j^*Tf+W+))V`Rc(eQ4oaW*?okV=7E*{}59}kXntGerg70*87r*-*#-Z?L9 zt?HL;=}LI^aq2rS_^PfCnokVzHyc;gAN9EoSj}CXC;I;KruS;C{J!edy~Fym`#bsW zhkRnrtDgBPp5^1gN^g|s7p(aF;-_`_@v%po)LPZ|>;FB>uAeU}ef_z=@vUn;d6@I! zWquTAUlnIQbw_n`!|WGV``i12&pq`Sjmq!y_w6g|tLES*zs(_bj_Id|XFjM;JVN|R zPk-{vlZaoTb)f#_%kxw0d34YJCyR$@jd*Pj%<_ou74hYgK>FeYz4|eEN{??)puy zT1UM?b5`}#9mSQ7bzsG-_P5v7-?(4@LI{4Jy}c{ZO+T%Ze!GtB6DQKyU9@iMRC&Ct zXN(7P{)#WpPw@@bg}!vek?mjVSAM4#@Z1;cLiaK6hgh9XA|0qMUW!$}JY1@apMJ|H z@{6C=b)MYzQ5!#>zvvcS0nh8c)E%AQ`YTL5yv$eeEWhGa`D(q&?@A~A_WI=fqo3QB zP`4f(_e&n;d14-YBc!vtXq{DcX8T5`BGjV zzcZins%PGM5Kjz8ed_w5{t@ESf%1@!IzQF@J>XThd~BntZ~3F%iD&=PgZ4L`bt=E{ zARSoc)hitJsl!TVly`k29lr1r-{+SP`|3GuDfRsQ@5=Ao-{z%1Lj27_>n+ur?c+DB z)^Tp|lfKu3ryp{!)~f!T^$+gxoC~S%KHAUf`bLNk@x>#YI!tK{Y^Lh#&kDspB``_RAX#a&+^$&ceE78SI{q$R%{~i}~ z67iwBcvVatvFm_K_36N>pD+Bhp4TTocaM$V@3Z5Lok7<=O?{6K{;F5!H$Cg)Lv`^8 zXZd(=q+8YX!-^-Le%pWNpKxZQs9*Z2?g(G-=s>>WW!`;OPsE4n;t|gB@!&|es`GiI zV}J4!-?{wv|MY^kg!<0?dj}q0a}Ivm-^o{(PfR`a%+JPEzdW4POCG;r*2Vnbr*++j z-~Qe&H>&yvuGbmr_^IbU&^L$J{nSqnkDhww#p?Kpc$pWg(}C)V>iX2hP(IN)%TIdz zc-iyMZ!PMx{~x-3{#sY;9I{>_K2#TvaF&k;N4iy=&#>aj^V7OspZv+6{cNMCUwu$# z(8Z?%`N>~-vHPr_9>j-LUcJImpE|5`MtS|P;=6u+x_;;D$6ob|tyTT(TX!Y8kiLHB z1AX}l>A=*}Cl9Oq()vh;9!$QuAzq?Beu{nn;J5C&wY7M^_NxBhmF)UGA^SLW`JC(1 zhw5UOdYcumsz2(>>!{|X!%sY)@BHSu{SUa^j}M*R8Tu-}JwEu3Z!USUIy48Y^6Img zI$}7hlRVb}vo7|y=cjf3{mxJQK|es$FJJ$BM;|^t>$wknRTr!4r>7rZy~?X6KjY-7 z)6KdV;zNBIXI_6IKjpn2`?O=XHc9o1-q{&+@vRH_$zMFL6YYO_dXNsR^6C|i`qa%q zCu8{v=|H^HasB*M_kGiMxY3ENRsH1sI|ENYeJG!Pi@jc?lSl`ui$^%i$Acr?s?KLv z@%X||>)PLs{?(29`;r}92~U6O+rRw9H;+E~#ME0F(_7k?{8=5>1yd*E^xNz2`)zr6 zqpBZv$KGiU=SJ#zeVu;mGnY6q_0%&z8(00A&v;hf^}wo+U;MP5{r#CE`Y*7lpK(lA zqRY?J_xzW2`ua1LPrWLiJbJTvy82-Dk77D7`E)(5`6=%_IQgu;Q`NVxzcA+~K7H#s zpDVxhL3;Y>sb`#db(rhQFU8ccE~IB}D4(d#PdZ)?KJQVFYAx#e@x*k*73v#d>fu+q zqkPrBG+(V_UWk|a?jJwZeco`!;~&{t)hnG`2QQKS2xs+DUrb-TD$eSd7gp<4emhUD z{eTCwR(@Y{X;;#h^N92L()#9ce&Ewn$5*fNc!~PtGnS{T4%I769vxWqRp+_^68^F%~i#zhoAY3>A|dLOgH%%XJ5v2pnAsg{IrhuPyXx=9^EMR z_n|lK3V2@M(1G?h9&~>_e&iGBKy~pfj{4N$tZwr7I?}QK_=(T&t%q)WzC3hmUx~hR zA=l?8eYzFWQ%}8BV|s9@j=4sCc(CI0BmH(B?0ndLTdR8hd^y_R^sR3m`KA^lE7-UAbj8jYG1O<>%;&u7@?z_aP+!GUrwc27RZpE!KR@Z&-@o{$ z8-M@%L$~Y8|C>7@l#iwpXy%Ez2~6^v{v_H`;T=7 zp8G`~@{`~4;@sc#ApWYjyE;qz`3S3id47uhJpX{7dQ7`yb=Qw4wvM<$eIuOJtJaaX zj(8TA_L&z}{q`9@@$An%@5j#FAwI+G;;Zu@{q8@-udV&}zkY3Nb>8H8;+#%>_tE*G zE><^3B0f|X&*G?09nR_|kIyjcV*8k%)|J1>hr09b$D4kycjCEU^q_wJ%A3dQMKL{y zzuCB|{;1D&z-n%O@sqC4(?0j4jla)!dH;BA*YEr2Pkqm~SvQaSV7sL9h@}Gg)qNgHnQF0-?E?=0vBP z5)}!H2+NkZlyaFSqglg;{B4erQ^tXzMthd;BKHI1jJx`x)<;uGX>U zU+288>%7kUdd}w__jum-9q&6~wNCYX;yl@ULEDM^KJs}@$sBY2Hjn)+rU$EeH=18n zzt`tFU^S<&JNQZ0bIX@5dd1ZGrbS}&nN1PT`)cav{>FEI)Xk?3Q*Yhz+;w_<*8!_} z`NdDV{QkEeKV)ikKQ8=-rlgKf-@c_Q#uKZ{d!6OH>iVpMr!EiO*Q$<37s^BHi0Nfs z4Dl1~Gk(%rJZ5it@wsPAE$WAz+Z6CzSL*T)51L1xd}8XUXTBHr`m&yJPhZ`2_4=#x zME(z-z5My_lYY3ZK$maUh3;eS8+|b6l&7<9Tvfl<=Q?0Dw|!{;(sf?^{<&MHR`nzH zZc2C?^_xy&HSel<`g8k7`}hp2b)2L8r0?tbW8b^{_wcSfy(!VfPkr}M9Ut2F;>6Tj zS8ScRdc8h%*IVg(-KXEq@9WN8{`}xIKiU-O&fVX9_WGfdSk1d?p8nka(LR2|Y900T z+viWdde!pzy=`;r;Bz%k*7+>&KI@k^PtL19^BL2tF!^+oU(J#IV*J#}7(e~C|L*$L z_Wcu|pT(ymww|~`efZ);bvo+e3P*YRD}I&NmuOz7o;uE1eyTgyfAJ50d}?(+DxF*h zFOhx^Qx8A&G9KmWOMb?9Jv1+@_+H2SwBDT-4JTjLzLVnT$$RbB3Uu*P-}9u`t2#cU zlZd}=w9Z`JUY|Ow^zCDQ^1=D~g=f8JYIQ&8;JIJ&aMW))iF8&)df%;nKErB#b$;Uc z`TQTh@SfAi>L-4nG4wf)^19qd`-B#5 zk$syp>W7@wl<@eM`uh2;ZXSJf67g3>daL&98`W`LP=BK9<|lp6gO?t*{PUeVw_jH) z|Gy*pJ-^)t>zNbMfmL2Tk*<8k@^rJFF@A+~AYST-E5D!nr+1lJ)lYm_Q{q>p?>^$? z`uePoC!c&gd6@Z(D_)ha`tcziG&ht_zU$;Cp7ZrlFKZ{U`t_GI1w6jeh5GTJd7NWn zbx3DN;oNn4{Jf58ZajX{b)Nj=51%x(`u_NZKiZV^xnK14_nuGmVCX?@rSDM^wWdY`K_;qbRfQX6nlN@a8x&We1=)i zn4i{l9{kdWmOo$h{#ypr16DlOUHN_GeUF`5)#>DXy>+q=AJT*J`qqW?=j!$P%nK|1%5TpXU;pjp zuP=9PKYrN$`wjY2zuMpCvCqY>2d3V-;#Ku~eXavm^YYdHrR(|mV_UaPt?K9By(!^U ze)qnfF&Cry`)%s$cR$tXTNlbhd|2hxM=^E8a8xIGt^;OW zte$>5XI}H;&zz{gA+c!8`RLHCkpJ!noy62ruXKC)s$U-VbgH^}VZ~!3KdtNk^ZLhA zmw&#q%fp%?-zt6QI-d2t9=xAg53kZwr&A$4^-52jE?)NI^-w>=rw3hk?r+~edHMMV zOs(o)*t;p|=O2COKDr)xeLcj7_~KFQ^<_Qd(RKI?t3J;W{Is6?aNcX%d7{48;y-@H ztNhmA^V@v%U^VYX^Q-Fj`dkOB=Ir_X?i-Bh9AMGvf}U4)@1- zqfQSm_0%(8#dOtsx^wmELi)L`Iho9n@Wj>=SE#RtsfXXwt?E^t zF06RHym=vh>e_GoRQG&*({V4GTHTLIC)dGCq!0Cpp?vmlG^RgSr`Km*Sm~?tldkW_ zKILBbnOfD)`E={R^PHXf&I_*}b@x}DP9i>37ms4EPaTfxCXbIW>tcTK)4I-+_kZF+ z6V=Z*_2G$o>)`3@Vd~-2sc@91A0NukMe{=asjJRUb$-9{q+_R6_v4KA=astOKe3PP zZ@Sh~mrpb=R2Pq8uTLG0>L$;1!K{m2H$ScG`Q%p~_}Yo8e(jBoLDxA?57JG)h_Fhw~`mRSeCA#>j&rg1<wf1_wUIvLAX zhz~1XUPr~_3qR@d`&N7Y%GB!f%6tAxQ=(t_xxIg}FkO7F3we6byy`IX;tI1bdFn9h z8CN`W!|W6Dg`e~m-zM4gmDfCVqN+bMzJKEVmLB9Mf92^z^NAt;x^Y$gUZ3lL)!e=g zn8WNdE{MhV$P|a`Ci=X%X-E=eRbCb8|O)Nepmb3`sRZ2(0s7UtB+#p^x{!n*8!_Oex={`_gT+h z{{LOSa6(fur~cH>^IP3K->vh+y5@T^FqSyU46x-a6T* zzrs;Hx)su|>b?AEzjhkd{E?+;eaE$8_Rm#a5N&e`uM&6WY%sp7D$} z0bX@}%k$UV=8+fEgZQw@tB+#ph~cPC@>~bZddB>;uKzD%mvh_mx9_hU_}JD-*FH@> z`<}0K^;LPi)YF&wj7NF;lAkeN5B0%{ug*``>+8!ip83Y9)&9SJ;Wr*%Q{U@`ujSGgZdG?(5HEGB^P7*i__@1Ht^BTZavi)xyq@29`g3mi3P<_L%ed0d zym?{P#h%;vX}xxeY}&NXdCPwv`Q1l1hI0g;o^#uMOkc(Jzkd9jS6v>;Tephw@e9Wx9?C;`NZ0kJ-=4pJ=LHX$sOs-;e~xhb-{VLXD z9?ZPBhvtU#VD|G<-Otm$=h#Chs`Gu9t*sN!xs!VGJ-yGEH)8o4iSk22Xe$s8bdef#q`==A9R`-Jrp18LT zp1vNA>d{T4pZU4*sE&DIwT^wrPx^lU@-x@94Xu93Kem$sZ}dD#zIEjjb6)k#_u^ik zI_&9Hbw0z2=RD=7b?y6`o_6fis_y%-`XGJ%p7*Lc9>mLe)#+E*<7GYLo^HjLH#d|| zUDwG^JU?%E<>6;eExz9O_kHNo!dxw?FX^kC{`pL&J(Rb4)L8RNmKZf=O5Xdm*E zp4a=%zZcdV<4gJ{=JO~o_qY3Sm$x4{NvfZ=xvkLCcfag!bD0OyNyJ|_T4%0quTLFT z`uyN0AAG&@pf{c}wR(Q>|Dn^xPkqnd>eli8>VD8kOg%c8AC2`xePSpNtGqfie~*vH zPketL`M`5tI!UUZ_msxa=bW}K51vUDzBbMFY_7Gg;hQC8Cwr#U2GkG ziaigW{@(U}seb-Xwijg1=aYwg z&tX%m^X96>7eaXaOMQN(-}-u(dK-OEeeEdg`q!w$5C=USD2ErSJUZCtc^k z2jAR&e)*e=#M2fJB=jNu?025XS4an@o;Z=-Xg+zpcvRoqu+rr#Kk0kFyzZLz3UEKR zzM(PbdVQq6`UwEixwZ0 zz~f))^OL^1z8>Ol6s@zW&S*cMVYMDTeu_Px9CuOsWQ6zApFX=W_*i|t<6Ot1EB3m| zyg298m-&ocZ>5ubx^w$dM?cKEm@oV^r*qNt<(AzgZN zk3?Q-vLvL`Yqqz6!0p)^E|OGUFQ&;M0}_&o{Lq#Je;eG z@4BFTV&%8j`^W#b{el#~KegESbgMZlzwsfR#GG^8vGqoEdwg|R>Dr(CZxbG7x((qVNb8ByFOU)>|=gf*EzG>n;tc_s*iqNQ$N40FLpnymxvG5#TAb7 z^y5SMxyWazKXqL{Kh=GI@FO?1Um#K6wY`0A|NSk#(TA1a^vwn3A)OtCbJywdT?ee@ z_WI^0UC$5aAMuk@tNNZNHzm6GsbBf6&wUmrre3v9@~T)r#G4!IOYF^$-#bsf_C=A71}TOl65=_KOOQ5TP5uTLG0>L$;;FzaGO4zH#e+!)^lFqIlnJFX!$((Z~wh@@DZO5bgtu>L#(b} z9*+*h6IWRAdb-&shShrV^kMditMkOy^RIou^CwC7bdP?@bidw-mfX(dB06v_c8s}XI*h(>ZxbG z7x((Io^elK-St5EM1Jtodi?s-$M>09+>fgk@1J;;-`-y7pXeMKOH!=x*ylxwJG6w&Pe^y=Rxz)gQ>Ufc-DL_O5gtECtrO3iou6 zr<0g^>Y1WJxI?#By1*cf#2>A}kHYJby%bYPWNudvsr z?mA$lSLNxzir3rUN1oe$UVPEwp#Rd7n-bk>f77*(@u9rylNaZ_>Y2}Yl&3HG8RPX( zAFTM^FZtqEw8VK_^TrQ z?^ZvbVQ+nY;(5Q@|DT^ajjaCRhqf!g^PHa7rN8n!=b{HwugWJ+JjzdA#`JpVdSH*w zPwP1k?)t?oQ>*$r+mAE5pNG(2`E4EZiDAxL<&)QoQ>PdA^weDkluzUbKdtZlzQr#b zJ+-L&{s~>rA*t(}p^p#sSx;V^dg__4Vmh$m(S;SSm)8#~zJ16~^YQ!JcRPA&RrmLO z@c5eg>ps7&m+SQ6UY|Pb=~Z<;!-^-*PwVpQi+5Q5eC+1--vxHhlivAloeIshD$@OK z_4654>+>c3_Iz^VtJ_bG)wf;P6zNs_+xsY>*ZZvPscgR zPkg_Bx!dUnPD}ZD@`Z12$~}F$^kDi;C$V>ZbL-ZJqy6}>>N9_j=ltd;zVrJLZ(siX z;^V&DR_y89zuqs+DYmZl=s|o~<<%2M`FJpOMq~Z3>Qm4C?eE(^@APw~R`=u5r#2-% z*{`Y3PyVV~Po7R9{;EiC)qZ`WI<5=qPjt@mlfLhtymz;kO)c)n)tj3FUgdYSzx6@$ ziDAyUu6R}bUZ3lL)x7-TCtZCz|K9qtb9~_wl!x+=ir01eeftIB2S0ht^e^=r77s3T z+%J02^CW$FeLckAC|YM#ozZ?i!)iURdw$aQ`Pk>~{?w^e{WV86CA?MrHW!`5YTi}z z^yl`E_PHKdt>b--pY(lRbGK_-r}KT=qnjdK{M2_Jk|*yzt0$)3x?=0h)$8@CSAOJr z{KWV72XFOXA2(6m51-d`PT4);k^L)Zj zb^Gu7Jzp}ls^{-t>ZcF&^HH8YG@lsa?i_(|9E;MSvmY-&~C^9gNd z;#Gb-r>u`}-&(hsL zVu-(^aPB%izPjtF=2qt?p1*(chtK{`Q%e1$A8ri#>8F1C_dnWsVs3MeVy};nu+pve zHy`)8^yyP8zjxW(l&oK!Cp-QA&W`hypM3QFsv|$v{*sl?7w&p=({Ug1Q{DM(f8)XI z7sJ%6^2y8CdNAv$n{o21SRbr-=HRE;zW=}ohz?E03bgjelv_c8r84}Cg`Ij?%= zN8_qr9**iI&vn79i><>?vHiXC_jfq;?<1dK#kSAVZ$IDp;+L1tZ#wRaxVH|Tz8;S1 z(XG%rRlS!V?Kdy1biMA=Z|CoO&Odr;Ri~44^w!Bf{S}Vt(S`KH8^uvQ^TKLf=PN(y zJC~pQ{^g(VT-AQPWc$yH-LKU5e3Etg=7RDt^{RaGdU5KAVXwccyAD|K_>zA6e(KTh zx&PFvo`3%&{j`qPhx;k-y38d`#9tTEpQ|_8=XzkZPUW}v%isRRpPO3weem&3$sAq> zsZV!Zep@fs>BYUis$S)*_4o|&QimV>ROk0&zWLCpRsH(KH^%X*{oV6he}(3Sc;X(8 z@{^ZwZyj|$L-|CndwyEa&l^7fpZA_x)b-QVFAtqZ?x#HcM0)c0P+eT%C{I5=l%I?G zVb$+E;HT^5*SEgB{Q0W8-MJ~@Rr^~WT3;VDpBUorD4e@akMBBQHFxE=^WdiQ+R5U6 z9DPw!vcCS**KdF0o5yp%d}8XUXMQxU`ZJ&LsJ`ohRiEqTr}doQyMN;srdIV2-=itv z<^Hu!`Yo@|T;fE0s4gDGUSCzO^3}So2jZoUIzQE&%Rl<&yG^a?58bsf{C*}reg4DT z-(vdu-FNfQQ`e^s@u0l*^{Xd(ed1-GygqYe-ntMUPu_L%Q{DH!?{Rhe{-FASd$dkG z^U;USe>{k9|H~)Rf$HKOj`HzfPq(V`b5w_)_&%@s#)q|??B~g!*}ttw-#*Pb>;pVV zKQZg6r=Iy<-0RDF#yx#?eXdJB(LUp+y63_F@_Ba~q?l?>_L|9Qy2YaiTe( ze(_w?5B1ND^(FS^Pd-1b&!-zU?>kY|Pr9&m;_)r@OI@rEQ*T}Is`|Y?*8!_} ztMl9be8)-4zYq9@EltV#d`o@({8l%QK01l`P+dHVy*_m~s+&C51+y-${N8z1m- z1D_#(D|BAaw?EJP<@Ug>e$}B(fi8aPdS9@=@u2<+Q*T|d_2%mJ`qW{i&li63!~VYW z8=ArD+m2~Uc%ENVpDw@E^_feYh!54pJsjoZ!JckacRjG;@r$3<_4Bk3eDYVOR`rkn zVRMkK^Csi&U#Ufka{|DcJYKKlD4{LKAr z9(n5}nhUCnN3qwJ^^8Z?ab2+L;|o8n=j)wYp51=o((`%#|5AKQefvJ^^vzXa>gmgT zFV4QXG5ze5hj|_HiTvg#UFXbC?0)jp;(qAI6I)MQp}rob9)6{p`B8qwtMa*y*t}3a zb$p+LpX$!r&=d{&yDn;`^ZPSV(ZG&6Vrj};tEH3`thv`)kl#ItomFxKdtAS zdDKIjbMD8MqnZ+){?vCL_^YnZT;fE0s4gDGUSHNT9$m-vz^acvKgIUvSr2^f)avhB z(ZLh<*1^-)!_>p4Gm5=FbvUY<>?v7hhU`*H20bw5siLff%)?PGe-`JMY) z-s{JD^u%+0$7g`ae6=i7Yv^;0VAscSv` z73#wmC#us?7x!?KU-7DZwXS&~Uh43bpX$Cpc+Ixu=aY9Y-Y?y!O5eW6ql@qLBaf$^ z^Ww=Ts>7;Y<>&U%(PwTbpFF;#-_G@CocPmIi~3t$))adFI4_cCp1FRT8|L-MC*r}Z z_hR!VFZJ+X^2L1NCmrYdM^9{ysq4cN_twGFhY#hUyuNiI{keL*KJ&s#-#+6fU0+{b z{N)3tR`qj!rYYfheWbqoWq+%iN1je1K2#U?aFmY+d%9KKb-{`!&rj>x_oqJd2@^#f zA5T2Gj=mm_>RCULe&*-KqdMk=)jIYWKk55>1-stm#Hm%C4xYHT4xYXqj_T1(q@Ve@ z@u-e@VYN>6d?NqjcRF)w<@fRTYf9#D9;Lp1_fuV;xx|V1P+dHVy*_m~s+&C51G6sn z9KuiQ+TS-Gy8QPcu5G`rw*B`3tMhx*Z}V9fnh#ca^-)ZnUOcMnI$+huuk`x|7Y%#9 z<$=?`)Gs`&O=wR2sV{GTA=*h^2y7%*QXA%e>Bz)t3LaTpRV86m+$=N_8-8gU-G=B zgvY#&EMnsI*_0E&XXryux(n}d2;#U7p3sLFXnvqFMshzG4P;`0{^j#m?`Ye-`}L)BhCW^MsEgHM&RgY^ z*Nao97x(nkT?dp;be{0j`ra>Zz3+Zgi}%w#pVt`aBVGM;#ZaG@OXhQ4^(t@O3bQYH z>M-k7Y%X>EnO9HbC%)&F-LGi>lY{f-k`J~{Jo_~D_|0E+bNTvHOb_C(8&}ou^|=mM z&CM@<(rv%Ry=l`^-f`clRsD(WB5TF%bOb} z-#qp)KjrPu$Gz{Msa5^peVP)U`<421^E&kPF!ffAt+S*1`3$S|>_dK<&-37=A9&%^ zs($&lrbHJ%_1&-ZTc7)+Umm6&UgmporKhjbm52BhS_k4~zkSG0b$%W44{bxMU$}Qu z!mGZ%q?>;0gXR-M^X@2|yH1bqI$$+-wZFZe{eLfL|KTOS>E!yob+S*tJ~5Qnw=SeV zSFhJ+URde#m7jF&&l7fS&nNz#?{#l)N_4CJ>po^(pLMM#56!DCPTXic^~7^^%?+!5 z*Ue8nKX3TxN0xtndC6@XgKqk1UgxuV^4(ACsY5!j%Bw?qP`*OCu;TUd`eDU)PUrsi z^UMEoOmj^A$j3J&y47{@li$|YCofLKht)djJ=72J#ZVqrd3D&EAJ6{gCw=$fmbGDZ_gL}w63q`kKJSW=kvQfx~+)k+{iiHFZ&qZ zT$xvgsaNHbmvOI89cKS%?7Cpp$FIuo4<6aRbLH#(BOcl&OTXxwJNLIa^m#wUm&Z%J zDvwuT_9agpX1$8dt*#%+C+g#;c=rYI^AEV^^e^AfI{AyOfamp*y7GL)7rP(wiK(}0 zOm|23^BGp_nTMZZzt8>AC$~RXW&c0;Yfb6<7J~Tnt;bLP%8S+Ydmq80r>;-j<0W4o zU1*Np`uKFkbj<_hVfOJ;-Ru3_H#G(I1NUhw;W>}25A*z%*9WWl@YjvjovYjHa~-g! z&riDEFJJiiBc@h$e@_W-Rlm(eCo$($&wMZL^;PvMU#;hQAYSU&hx}CM_t#!?(A27a z@F`6RZ?50w@%o~ZXkMr;o{Lq#Je;eG@4BFTqH}|v*7fl&2pb%FjhUL;b1i`uVADe_wFX=BZWv>Ss44y7=^=*A0Dnee&W& z{B@&s=IZwP)M2IXeVL#9^S=F>Z!dp-@SMMIN_6$7e)>(9&$++p;iaB><})7U>BG-; zGsdrw4#cZ;(r^3wvS+_+YE@tN^ND#9&ATq7KUZ(GkI%4LN1dPa?axo0{_?3+{rvXl z@wR^+qB_6rdwS4%`o!w6ns=l5RrPy)c^%c9c>JX6{qn7sFaQ4yNB?AF;PEZ>-7n_{ zzS#Szd}8XUXTBHr`qW`hud4GARy^yZ-~4#oxWh!PJwdQ{gC2 zf5orz`V!3x)l)~EpX$E<{g(FUkvFNI_syoH51&5FIv&5hALAv`f$HM9XnlR+O1H}E zPoxLcD;<8~`TWUi?{>(P%6jT%OeYa<6tjQT*m`sONBj5+t96{e{G{*uRa<_w?L>7t zc>3jGp5OBNdWb(4>CWw|_@jD!hCMxg()afV-~5I4^9J8P-0O^{L>E8xo&U}cJbm)w z#MIkpY<)PYhYzd1(fRTCN!R}V#62G|E#ZEQ{{2DkhkP|Bed~%7@u9kS6nlMDy~&3mktY_TQS2r(|PxN&OKdtBdy~{7Z zbZSxGc12TgUf|Ppo_qet{cRrW>KCiiQ3tc!bk>ODRl#IKN_=JWOZ#M|G3ux?tAD{Nkr|eSLYymmD-v)&HY^ zKh||Y*CS3;hglzu`3$Q*uVa2%*Z$n&(e3x={XF@qD;k5Y{?yl>``GQ(vV zRWbdn(}n68%U4JrR{hRle&Wer_TKjQsNIj7_HHZU@h|7d=M((IoKHRV))lX+-|KT7 zu$ot$pLCty*I#`0)T;jOhczX<%5VGE{xvWcp}Ke!dwp5Ycyt}t1FJrM@zZ*q z2Y=*4FP~b~54@x)(e)gX`sp`aea@lGi&IZu=Bs#=j|VHgUS2<}`0D(0z5M=z%Nj%d zstcMDp8J*h^7c2r_0;7PQ%^nfRXobagOy${@48^cSLdg7`F+Z*A3U|Hzw@rmDSh^7 z>f879@cC^2XI-3n`ZM2)dwuG#r&rbWSM%$0{rtq|_f;QjOz)?6e{fT#U->Oh58rzK zxBBflf}i}6zuga?G#T!GT=);oe?0!Ve~_Pi#&^EZ&pIBRb>ph~y}rDTYHrVc{G_|x zuiyCDsa5@m_Uk3ve}2ig)Ys2nb^0*pl*eB;j_Qx{%;!2_)nEDT_W^f4pK$cwM?S-f zt#1FiFaDnCw+>wXy!~sQ)0FUf=Sl8w>&eqetmd7Yr#rU~f3)9qLH&u&gY?_`-R+KP zKfqMq{T@xh9CQ8FXD%oYbIx_etLpdqTnDV?wa@rT*Zw}>t?esK^+Rsg784(lPo8*`j|cV7Mg6eq=L#+r%bKtcR!*jny)%fJm1;h$#-7KC+58BneWBDK6TjB ztLl7)6^~#1w66Vq_{Hr9XzKgkzA@HU!ERJJu%cL?%`ZMy{a$s=7#zc zT{l1J*!ORH-}3L1=8*`Tt8*&MoKp4411n zNA_QyC*CiA;;pAlWc980Zg<3ev5#+7Z2xk<@YJp6yi!lZhw9={?DeU`QQhS6xu@g$ z`HAoInlC+k`R~X6{6%dgJm*5L&rkaJ=JL9dPfR`a%vbR!9}iY~y}av!6~B7Fv_Jp; zA?*uz_v5t9P04({{mpOd>x1SKL;Q8)s`|Y?*8!`!`N2=R_UC@Te#fa*{ptraCA#>j z?|#u&$JbvW{<_h6b9H-t>af!12S54IT;80TlM2PNBN2GdGIl}dCWxh{IaF}_+gVie0uJmx>(&@c|Y*g@##Z# zF&rv^iTvOveSZ(^hRyp59uD{Tf=<<`ke&>JI_3;~D9;TkSLOQF~SM9T|xuJZe z%TGL?ulm>vj+k21&v|-d@DZP`IQ^E_r=EH9rXC(tuP}8g9zB@6%x8>mJt&{(y7@`Z z&*wk${PqDK_569m=<^`mQOr5@Wxk3>`FOC>>*e_jE53F3DfagvcK$u;%DLq{pW$-# z=E!q$`fcBy{^{FKWc4pRtM$@#{@<+F{zcdMp{`F}4Bc=1b)$9W>h}87VWnT4C-(O* zfAS$yE5A>ANmJq%q;Ea*%wKh>1*_?G&&s`|Y?*8!`!)%i)+`{j#1(f+?W^#k{49sGi+ z@0?M$j{D=hv0n00Pd)QhY(4rFk1niuy}b1xe(HMN^V59JlY8CRzS2}baZ$k2PapC# z*V6~hCx&!(6wY0z$9EmDnwuZ|r0YC*^4ZJZk3Hn9rbO31PJQnS^y%ue&oeJhy(*tP zF+P+}REJeP^BKD?m~}Bf(r-)-)8prLRCD6-ldkv6t?zDs zP*eSgpJ+<@_?G(aqt}l*ef?F85AnsLNFQcj>Smn$D%J-pp7%R`n#=ip>G^k_TGe;{ zeCInm$!GYTS(`T5hy3KX{K0?y@~QPAiG>gz-QGIMv+wC7rk*;T9***pmvL_$b@M{` zMCUX=t>@?S$DeWh)Z%^|z4&}AUHsJb`rxZNej+`H539U-g}uJ4XI$xKULVZ5m>>M) z1HayR|KldA`sP1top{b8deD8Ok1y6IpGXI)i$}57rw&JTlgDS6b+PN_r*--Lyd#$X zztO92(HMC8=|O(uL4MOWpE{%itGxOsrj8hn>Lf3(BkLKb-;V1i-|gWORo(N6`FhWT z{I#CGoKGI&uM1bz9qn^Hu$r4+{G{*aXf3ZL@F0}9YD{mfob0y->MLKi)l0R3+b-}9N z_xJdT$M08v-}3prt;?z^md==Ay6^|~gc)h%SSn=)8^xNkL zFMa!qrdIXb-~3B`_c8aec{0xzn0l+m8`T@_cU`cWmmWXG?!(UiU#dCBm&!*zkMh#* zJ1vAi{p>@hfBE^(6Q8uazWb7Mct5m{)y*X@HYdb~RbG7*Q>Pb?>beft>*J^OJr8bv zo-gvfaoiTmRm(@zhk zPgOp78Tb0sVfK&4`eD__4}Q9S=l9lkK6q;N{qbD}fB&+#zs*%)HSb3A)|;!->r;o7 zK419BZ|C=J2R(CYwf|53P*b7{Q{Vk^Kh^Q=f9uIZd|2hxE9~{D!%C-@*AFYc>*c3- z@nf@1ciejJ)T+Mg-i?9hd`NxgMfxpvpXnr~-l{R(9o^4oSgq&!`Ds4Sd5^gJi>Frm zpAMe5w+@~@d~u?B_K8Q4ZpBxJ6|a{!FRb|X89%M-_Zu!g{H{~0`?2cpr&*sqG@lsa z!z!;nim4-pqdLiR9Wd)+e)H41zP|k0SDrdi)vrFFy&~x9rw8S|e(;??&Kv8gi|Igh zaSuoNc(A8i)%C-Q#}|IOUf)02?di+E?>hSb)zRg*^~KIB>m}ktb@3?n`qbg5Zu0mH zvo5wy`tAAT)SI3=QPnRxyLHg@oIww!-}wCY{9=7|F&(Hb?%^mO5B7Acx_(&kyuSJA zdfQhyn>HPGRQpQP^Z9XKZc23VQ=gyw#S^<9^7LTpRr%zJNBMYA|6J4$tA6{FpRV8M ztF|4t{QcO`zc-V9Tc5tUD#TwmT5qmyuTLFT`p(hZ-=4qk{XOlSP5q=FZc2Rf`bd3w z`&nII5Ah+sxWZAMetalD7x@gUetz)Ny1rlanU~#rYE|FzzNSRi>m&86{hfW*lNYC+ zdge3M53`;z-Q;JSeHqh%>KRvlJ6}Kkx9tP={QlJ9jg(F`m*+kD*2jl*67hEwT5nXp z$5)4yuGc+3`QUl*1CMM!zf`A#C+@9-r>}>jdUPwaPF3&aNBhkSD_!RSKj}LU9{l)+ zOs&5Dq=P5!t%IizU!16(ec}q~RQ1f`!>o_S=7suFSIkeb=kHU#|J75g=a=J7YD#qR z=|lG`&l7#}`sE=VSmo7wSoO=po=#QQ4=bK?nxC%U-&1<{9qu=^+W%+1w=w8?eWX5J zJay-n^WA!QVtlABu5gs6A3xX47{9_wM_!+ppVqOzKlcOeJ1Oc{EgJDWPtt?wC;lkX zf%?RAvFewHb9M3g*we8O`HAoI-~Z!2KR!vS`+Yz>`_;OT-_8wreK6;g$KO#ncby*J zb--%w%5Tpn|N8C2rdEC*@-t15ZZ(HqZ^l{c%6!6H|{)=I6%N(MK1`C-&-edpxgue&Tz-JnEGFr={JG3r}m4(AS^y@jLgo zK6&fOi&IZM^BL=hSw^`~b@S7lzJIdw?{QSlE$8_Rm#a5Np6AkU&)?hj zI(Z_iZ$6>*x-ZU$n-$x?%?=vQqSvykLvn*h`&*^&Z;`2 z{d|Vidi>xgeSY2b>Fs~^Qor)Yni5`he&_zSzCLq7d5FJmTvfl<=Q?0DH(&WlmtR*r z`}I?+IvqT5Zyh{+Jxo1(Iu(xc^d~=KydIhtR($)DpVsq!chhk%n_AWB;E8+d;OT?< z#86(}x{&@{y+a(t?CDp2zvuj;r&e`3 zIrr!~`g%C3XC6pbyipw0GcT;xt==#F{PLrRA3nA6`>>0fk~y45sV|SGZXVBr>WTPJ zUEIS_J|67pR(03a)A7E@PkhgV*SzU~X$kd99@JK(&$pbzdE|c5h5D;F^)?z?Z?4X0 zA0DjM@%+qB*YE53Pk-r&Q>*$JKhPL>`cvQgEgs~5RmV%cDxW;@C?5~%pNo7R)!`?; z{rxvL9yl$nez)&w48Gx;Pn`SPJo?gadA!uC@^}?yU-Hyp)-!f}nb!~1lW%|WQ{6f9 zFUK!GpKLj!DbeKV(~Wj$j&nDbYB`ShD#Uq9wGlcf4#hqaE} zpSiAc-TknR*!`5pH#a@?%x8=bt<&SFTL-3&80yP7^XibV{Is6@O;0;^qN@A;pzDG3 z^z*Z-<3YTfSDk)^Jzmx`?&(&1d2>Vg)ODTw#Pj*8PkgZb2fFGv+`TE_dG1SH_e&ig z=3MeH^{RaGGVb-M!|Wf8T@S4K_`*-?dOrE>U;MeLRsG=On-ZS$KlSOlpX&O|B~HYL z>f%xC^;PvMU#;tUAYSV5gP-co=Lg@=&J*9St#oo7yhQr=P#(%>|GHx9jOz9H=7p86 z{mD zh%40B!_>pCbmd2R$)DS&ADS1+SGxRE_x-9bzVxM2tMjJP(WhP^p1GiWVx_auymdx( zdVJRbD_yU9e$w@RdDP!+om$!|Y?$|riiL2;Vsnzd4eCaouBHd~Zx_B`6w|rtX??&_1nXA+5Q?LB69$)y0 zZ+{tX8QSGw|}yox`!pN@H zDd5?!sq22xS2vG5okV=7F7DwdKY1DV)=_s|P(G1g{Is6mPy3&Lw7kF1`I)9b*ZXay zo4)F+^43qiDxW+tK9o<)dN0m?vFn2JiTuv}?VQ>DiuRYR+>c8>*c8mAKXpBS@KxPB z?vt1v#9ueAs^9B#9k804U;L!&`>|(Sy8QoNzWb$33D4Ixso(S4x^xm#Pd)R!xYwr+ zdwNx!&#>b0EB)SM(XegHe$&6~|NQw*HD~2FJ~W@#fjH;I&wMY=zSLKTsV5J!E~X3R z6Y20%Y=7VKv)iT?_v6a;?W^s-FJ+&muJeeG>gJIbTNmQ58&}ou^|=mM&8^N)y8Qmg z9e;IdRp0fn*1<35aO%tBS;zgc|E-57PCfO^S1}#yRy?|R`YL{v*AFXRb)NWpc)#$jQ`UHdroE5G%ryFPhw>ZxZw<58YI{9HF<{0iwnywvIW{gzA4nOg1d)UjXX zq5W(BSa%fZtcuq8ZuRpSR_pVHpLl+s`?Ggi{`veF4{uk1=YHjN(dDx#i^&B`6?dey~_BcG_>T#3o23$soy^BLp8 zs%{>BitWGWz3bem^o_S=7suFmyP^Xm;cOP z9x}D6S30>4ULyS-rXGIkWjxB$SMjU7zC`mv_0$pbQ|#wyd;RQFr&jwvb*kqR@1yxT zrQbvAKz#8i_WIP}sBZH346`og7eB4*=V^Dn=|K}!{r~j;AJFx={!#4p@flXS_8&j# zdY(J(kC*>mfzN;AxnHTTpFX~MQo0=vIFA{I(9H z2hF8k#qt$qf5ppu)mP=s4J)2?_=(5w54~df```Kd^P|rv)}h}+{B@!A=IV|1@flX@ z@GJfH{lODXTK1bx&e2;Z`}9{hsz*1GZszC4qdMk=)jIUjZ|C>>zjyiP#aI1aQ_|PP#5|ru#q?n6Rr%!g;?xntUVl}09kAm0`Aqul=R2SJqxP2{)lYhTo6LM(AE}>y zCtqDYG4<3l--~;F>aeF*)%gr7p6lkPb$$Qj#Jm3J)T(~k;{Pwk8}-}z`phLx#9tTE zpQ|_8=XzkZjygZ-`@H4}|Mcjo)z9w_`GfXJH|GI9eR@z`?EbjV^7O>?^h5dN$tU*u zvR=iR&-K+|&LQ?Z$4_~FeD*f?oKo(`=>J!zpFUmln72awb))s>>h}87VWsc6kDvVT zeDY_v*>`GHr-LW%t%IizAId{{ed|K{bM<xgma{ea26E&XZfe;a*dV`l*j?47%=1>dNyQUu^$dPhFgPRleeF zlt0>Uee=Vr-}@Rr@jMUS=d|U&SMZJpGzMM1S{Kr#2Uqpmdi3XF)h`d{>f-Bno$`tH zS^Dks_9uS(C#M$AFTdOV+|TyUYtH@p(mXKdl+QWW9nW2-$9EmDn%92gCtdg9m>0ij zYIQ&8;E8+d;OXmO>fzI=aFnMXAIi@~^FsZp>vhjhb@}f*s_pcP7B}G3BO8OR`<43M zht=td-A{RXF!k`{VU?d-@9EHk$u~E|OXMp*>G*xAN1pJksm1-cuDyM3KTo{hrY=AE zt4`lsP#)s18&}ou^|=mM&F%e;pLE+_q29FV_Nk9~Dbd%T`qlon-`!{H(G#bhdggmE zJ(zsDFzchSepvOnZho4}{(IIf+UFtE>CmnG*3Vyk^b_gHbfew^*LAc%fp-#FY_7W!K`PD2h}r{uh863Kg>RUs{8!+ zJ@!3)qN-ogwtM;WV!H0v-2H7{IUlYX7>A>U724 zPvz-B^QuF6Smo7W#gm8lJxsp2Vb!nBPdbZZW7BRQd*sxrP6tm+M_i%49`^L~SL^KP ze6DX^Sgmir@so~o;zPIEI<=}FeNj_NUg}rpiFv%P=p?3|dgiNml#d51yed>H1*hOXhn%s>AFTtMk(w_TQ<`e8NOk*M}#jBd$s0m8yg7S( z^TLYn_0CWF{Jzbr&zf4*ZyNmnN%YgVzIo=J-}*<9?>My^Q4(@nF`)bLZzLT|b}ywL>2`Nviw*cj)6=7ka))zs2^yetM7& ztn%uKqkKGbz|`x-bYR7I-Tbtk-=F{5SDrexs$YFTbBwP3)R*TkzH{CFx1PE<_0%(8 z#iM*YSn2ih`eDWA3qM^iKeoR29#gCOi63c7borV3`TT(gonzLc2U8C(^HrSujP=RG z%!_+yZaRtj`NB^;zkhk`^BYtB+WWT{&wix`-7o&i>oXsehxqHpRrPy)t^-zc^M#*u zoxeMOzqmQam&!*z!;072-#b4ayI#NTpe&zkt?>xbG z{;1nG>f+R^^2rm^6IYnJVp#QM-g>a==Qls``0=45o;OLmA16Mnt%S$FTwg!G)%E3E z@(>?ZdG#Jv{h80Wr>pLIpnM{~`Ds1-_QbCqH?^prxp!mWRetN|H@?_&k$hs#xzU*Z zT%FNAKErAq*UeA*K7aDJ$J}*lRsZw$zl`YQcuK(>f*Ur^~=M#y7;cE zr{g)8pZNCY3;(`70IFZ~q{is!^V@!=EB3mQrw3gJtn%t`l$ZRuefpt(D4)nze!6~s z+~w^DPA%%EZEgy5@l!YbR>y~Q67koK)|so@>r;o7zWv5eKJfd-&pu;nRi}d|?yZBT zuZO9JPiGW+ed=&jH+klTSrO@uFYrocs=RBeZ^}C<)`k?v55P#jc zs(!D}b->=-{G{vqvA=Z1{!^>^JMY(&=%%06@%qR*ef<^ESrx65{Z&4B=E{5(<0mHH zKIEr){CwwE&u%}T|BZ!#KmFvUKo>uCy)W<^PhXW+PrWLiJn<+W59*(b`eD`YJmDu_ zeE$2h-*eE^>VCZN{JbnG@F!ic@ z^2DQjJg9#z>W5Xo*LC{s{qo5#J8)`MfB%_nN7Ln3>hp7_-|0+?`N=;&-+A4YZ(Qyx*hyci#GD-w?74_eB0f|X&&8@=9?sRp=QET~ zblv>4uJ_CPe7b#}&DTR`9^Dk^@-KCrGwS&Ct5`nus{E=veRRzwpRs&}bRb^F{NSf` z+=p|2_7|pB_v6C$=h|lfyn!w>hd9x^P+dG1tA2SnR~J98M?R6S{Iss;lYjcxGo}{x z^WWMOdiuUE<9^cB=e)|idd^wplb3O?Pu(0ab$T%!Sn-{|{Injw-hNa2`zPu*?AsW4 z&W+S}AJcEKI-SJSQ_uWpT=mPtQN85x8D?EU47b-a6Tb59vX9ed|K{bM<?ljV(ZdLOg;6?_u^ikI_&9Hbw0z2=eqf6UFXSdUipZrRsDw+4B5{ZV>(b>JQu5e zc{o=WUq6&jbp8Ak+uyHv&hq}g;%}M)U6{J|Z~85E?pu!zOuZ_fJbI&fx-k1jWBoAu z%;lU;zkUDvgcod`TGcP#w<+njk5fPWR@c`<{Eeb@R@E8p=QFIvJ8j(zg%!N!NLD(i4uHTGbzV@1}&e>i#wtoy441 zJ@cb+)h`c6^^)g$VAjRXQ+`^P-*5cmCr(s#eR!4M-beA~qIIBtaSuoNc(A8i)%o1h zsm^cjXZ!rYL#HM9ecn@=Vo%@e#{D#>*t+JU2k~K*SBIm#ia)nMb$b1-o1dyZ*_e=#NQ}dXRglNK0d>$pC9SBzgKYC&$mg{x4x^zbUokX_1M4k)w9oB z@?w0bF76>6Sn=uN!HQqy>A;F-AM(?B-Y-9Q)zhX{^;`dS+tGZ)Pkp+%zSo2Mp`Tvz z=~j7lNKXu_etCURU&iu@sgrd&c>I*-_mAE0u&Gsjm(5KHkAJDpZ$7H)>mmL|(K>T= z=JxRsR{eb8C!U|Dee}fUn7X;~#MTj4s1F~?L-|T))ja*V{k=Z(!b;!%+8c8C#q+k zxQFycd87PF&%6*Xb-i!#Q{DOf_G^xsTKzqPtH0S8&Sm`6_uPVK4zDNc>Q7$k>Ce2l zhuM!W59NDkZan=^K9Mi{q+>h3?S1V(II{oq?+^MqGzPiGW+ed=&jH+klT>Xj}()$Q;7|Fxap z>iKyH`tn}?qe)7fp<+1nMHnqARTiV-a_w&ef`D=ap<`O63Lv`^e z_WH7(@#s3P2UdOb`6;%)cb?ysbIW-?Lsfo?VdeKTe(1gvMg79PTL&HI0v*^pzxA6> z4C(ABoV!ks?>b;LH^2Bv*WXim%Dt9U@S3k6-CGKi>S&eWq6RlNSZ^@h|oDJHPQqG4(bYTX(L`Xdjd$<}qwDY)R(+M< zo(JFa;`aA7`Mt}TP03u9-*odlF;C{{!D`;Q`Hkw0_PZ`v%~_o%^3Qq7;yWx3E-zhE57~4PwV>n@|L^berk0;F8b%jpzG%q zsqb8;PuKl%Kji6&Q%^nfRZIs~Ji4&r_44{*#peq@&Bu@ZE;(Upbw93ce=d0w-|*>M zPd~ra@e}FkulVxGlZUAk~tH^FaAT=LtXQ+-X64_g_40`q!^54&3WbZUsE| zYeC-*&gb;oJn~|CaJ$f&|FNomug`VBYHZidPrCN!?hji2|I;=f*BEs1Q(r%QJhA&B zPYxWgJdhT!g{_}sm@6@XPj;-wq^ug5E?|R{P%kOHgoD&ab zeKgkZI`w62AM#VZeHFiH(}Ui4%G7HAZ@WcvtkSoR_fdL$c1~NDP9hyhM?4qxL;YeX z539U7G=Gne$4`9c@_qK(GD*507ks@rrq4dMF61jcD9>+sdSW`T%BxpMx2n^H$*c0o zv(6}=pZK1?FZ;W_Cu#Mq-)bw-w~upu_rbna7pq&3o){mhi+ecA$IEpy9$k+fe#YLn z_-P%V*ZkJ*$4#y3+g{z2=-|_bULX9G*Jq!L6Y*iSj`}FlNz^YdhE-m@H$NUf_(_-F z&%5>V-V1)%ouCN$lTh z4)syLolCuWtp}^?Qm?S;%e?t}I_8F{Bjz_h>Du4tJ@(v*>VBO1;MR#(?QiFld9A0< zTu>h3uNzm@@AbJ3Sj|m`pJMmnf-n8@)T(|_`*Hn-zn(Xj_1rgk^Qwzs^5mgDC_fjg zbuur8)jHA&+T_TuJ4_>}U$qzruje=3Ty)(~zj!WI{qk_GE`HBvyvlF++a1&XfGNLszenpdpL2j7 z%>6B|&wNlG;;$Q5)$jGW4p`0Y_0CVazW=@R?{QSlE$8_Rm#a5Np2N~_`}6I8e&|G2 z|HR^;abH&5-}=obhBr^R-hZvC-|KT7u$tO)8$aoK9^CJ9%YQHI=*8b3#H;+K>-A%O zeddDl5PwHuZ=GH}=W-pe>bH;6Z=Z+w{N?Qr+WYyBzei2C^3#35GoLx+GnP+2UgpIx zdGZw|KjYCpbNBkaFY*)L{(SszwL9W|T({Wxczn&d+{fyE=&vyKHX2)JuFhy5zhSiw zzxYX?-=BL_`w!?Zl2{1g(e15+r>}>phfik|dwp5Ycyt}}!m7{y=BM?1KK5TOK5c4s zKi+v+W8hWK-}W^gbbsxC`NW)8J@dV|*QXA9dR3jzJssPYpY;4aj*G8Y{ydv{>RL~| zLVZ0P)ysA0W`5OpRL{JyT8|#T#rEIl4qE>I8@3$L7`cDx>-V}zp4ffHm(O|CtGsn9 z%)aEQ!>m`axzzPT`9#;9ez)&>Z`${XGkMi{4L;1OAUZ_8H?Qedn`+EMHH#~D{bw4VdTn8@^&vT*m67lq_ zi$}57m-UQC*D)`w`l|0&`Fj5KfBKZEbzOeDo*wq*uC6EZ*2(;6JlbzwSm~?tlfKVa zUH_Qoy!X>1-q$+xxnHU8{P+HdZ(n;|Sx;S@dg__4;!!>xtn_+${jlQmi=VEyed>GD zrYoMb|J3S!{O&1DiLU32)aNI^)%D4Xq4n`$l~=E@>XT2LtAlTTC{JIk&QEpkmzQ1L zoKwHL{de@+|GqswJy`jTZ`}&%tQ)NlNA=BF@zr6E$4_%Ozdw7>^6$^n!EAN=0oZ+-5E>ysC!UX@Rt7$3?fs>7hg(npt^W2R{fdJcyt{;!>Z3d z+2!@y3l%a^+x;n46Ak2`AOgRzyI^K%fDZI z;66-{AK$HgC&l*<>EMZb>)`3@Vd~-2sc@91A0NukMe{=asq1}_ zpX&bqoe$jmfT`8}xca`0;dOyeAG(j$$J6J$QI{9ff%?Uxn0=|Q4lCX4OP+d9$9cd{ z{M{CUpZ?PF{|9r%4>Sgz*FmoDKH@>XR&~78tMbVckMi-L{<%2nH{JBx_hWy5kJnEW zb^M&qdg>MG>tX8QSGrZc*O&E-E8WbS7iL}Txs9LJ^LdEN|Dl~M>iY1iSUyqTC}#iM z*gA9j@JIXk4E0yYPx`)p^6?k96Gwg9)}~0;d6c@&3+D&EbKQNmp1L^os(kXq^u!g? zg~_Y($+I5Jy4d>`Kk0iO{MSp~Fj3VnTKwQ1kAL(aKlv@MuZQ>>MeD4pGuqE*Sgl8o zpJMmnntwcSYW4FrI(XvVI(Yhen0okhDjenMulQA7U!r-TdZoipb?=v--1exc)%`f| zu}z6??qBOv`#bx@iFBa3#G{yfRh;?M@6~(r)3cBHN!QQQ{^wV>O;kVsIdEG$LFhZD z=|Sg=`++a-eppXkOb4oqdpOF+gFW4LqDYh##OKE;FZ`uxN%fz2SYxUfFtVb=4ytOKh)*Ue8n-~ayPFSpOztKYD1V~pz3!#AgOvyPY7SLKr@rVHg0>B6j! z#`=5fdB5W)zMrT4{9i5my>0RLX7HR3IhWT5f0M5+pO|{;nXlqeJ|3*}dU<}sitlxt ze%pUL_jl#oa-Pq<`&R95`9ojSK5*iGjQ;&t``7+9oq2kQzb>@iT)ojge#726{G`wC zFK=1?dS}y%ni3x&ef?fHSvQY!2p>;9^)jDzF{BIS6Y1i?a!j{#lBy4;K0mFu$D-kJuWi5I;Ooy1w=aLT|Nc&I zf2ZH(k+)uA&a0mJUfk=;dd59{b=L*u6J0kyt!IBf=z>>GE$+u&`!xl;@7Dg#xkqDj zj`EW?+Rta0I%3a#{G{*e`G2`@`-X}7X`5Ro-bVd4A3bQkb>ph~y*}3gtGVg$Q|vz6 z_DSuN;Of`!vtGZgmsriaYM%bw{?R@@!`?dlr0?tb;}`#bfO`J@*h=5~B|SW^hpLX3 z^H%xfSx-J=c}N!zs*hsoWFKA6$^4Y}JouS=FaLkK58bCJ_w@Pd{Q+NoNBQkKttYM! z4_3ThUO%k(o-fjG&*hIi;aO9w`ntdWZ9exIR`YH&zp8$(&vn3RPQLP!uKj(;KePi| zef0lhq-%d$pFT977~=0JoV!ks?>b;LxA#SU()B#}oWEKA{o66E`I9w_P72DQ*T|ddEuz8IV-+8ta$btKh5EJ z@J>JZw5e5nmyb0Dp7-0-ul)9UP^Xiadg_@UjjMinII5RCKEteM%unljzkJLmwoX*_ zy&l&(>AGL3$Iq;1pY`Nn>fvR+ij$wQK6xk)tGqfiHb_yt=gs`}{f?^OGnehlyd-)%gtN6ZyqY>p4$e z_k!i;?~{M5Dd2g1q^|pz>+6$;@-X$*6|bt_>&xq?=H*NJ?fr89-Ci=as^|A(E5CDp zTi@I;=ak3aQ8;&<9^ZAqYHm9G6!Ys5@A<#;@6GU`@-6dxhE+db({In;hktYHB&~ka zGnp=X}bsq3j-Fz$&kvILpU_sWTh%8R|=2b$+UQ zeR9FO_iL@{Z+~i6!s9D_=zj56-g@$M67iwBcos)}>Tp&!d9Dj)UF=-or**ww^*z6G zP@}4^eXe)_+mOxUscD0_^t=aCyy?ky1YKII+Taf%`(^{K;I z-Q>A0n02w&W&E_R*Wcgz1YcAU>?}>X2R)%R{r>lx3k!*5vi*=PK;-tOxSzxIZc+yAM1{Y@7? z^}X(?{LX$cOucHIp=NL`;(va`L*lc6G+FtbIfOm#ZNIzzr7xO z$4l*eTE~W$3#Uq^M-G1)U;4gn ztyTTReY+AK|5Bfyc^&#jn0h-KTW_h(Y#*OtwGKb{N#F19eCpt5wpQ=29Cx$sFkSa6 z^}QcJ54u0@hde!T>ZxbGif8$Fu+kgl^;h%jSLdhL_u1b2tn(Yy{m8y5mQSP)b541f z{hJjp)gSR)2dw6_-}p(F-}nFg#@{EQgIC4!iTa@V#EIFzvoZapI-@?<0Z01jx1V>O z_1ukr|C^5Wy^fKG&I{+2yuK0Q?<`tpsm{_qKEqLe`t9?ZpMA~~+NkR0rnj?x=X`Xn zb7SGsbw+&G0js(B#ZS7P@3wz!=@&&e`WhZS#>cRjG;+sElQzrOIOC$?69&*1!{`$<8!nlsN6eE!qduMTrwyv&c{N>3kM zC=aW=I39s_or>lx3k<9cA#$1i?b&-s1l&%C^~s{ht! zyAs{XPy1J$u2@~39yG5y%)B@;`B@j^LHPgmt?C?55x!;xN9*I&)AkFWg1cOMRTbN@{q_5bMq-Y8#O|16IB z)M2IT@y$SsTsE78SIefNvLI==o2@u5EPQq&LiFOBsj=5=IU>~YOc z>pLfQ{d=RF`nPe`@9I2}zxw2-wT0Y|Jzm|{rJwuPRPJNe^*P7IiF7s#?IU^_%O~Q& ztcz#o_qgXLUEhb;{_OsSpt^t0i$1<}Vfu~7e~%w|dXNsR^6H7Rd_0&sv$1|y_3?$D zuHU~eb@O8mYOU%!ex@toc^srZUH+<@$Nr|1h!54pBb?>q!I5rNcU`dJ*=PK;E+21k z!o{ssoerLOv<{xW5vCqKoe|FRlb7*m9d+|U`9%8s6#ISduive|Qc>UX=&pe0zNGGG zf7|!wqX$#3$|r9&rk6O|pFGzAvo5aAZ$94c73Vf8zxSB@{Ic?!&mLEKy<+q2Ebgq{ zY(Jl2HMi^MC;dCFH{Ab-en-5&vfGwkpsPRi-N&rchjbECZ)aoc(aBgo5f5fvJUc%f zeu{nH=lNIm1H}C}tN(MDi~k-TU31;o&zF3F))zy0D8Ce47p(d{?)hn5=g9>p-Kn*z z|JL(5gRXNS_4EA`y7s&MFHcXLdg__4;#oc(tn@~C{nh;X`NdCsufK2lxgCwF{*hyQ zC!T#u57OnYyt(AX^dSCbcWZ?@Dy}mio?svRh-o^FRa$1m;2lPd-fk3 z)moi5bo7Zw>m<*9rjwX@>U3st)Ta(-b(3dan02x9grC;+_h#PozRq+%KC}MwC0&mL zdeHq!U&Z>YM^8)#s*7iF)Tf^7W;|Mt4#ZC#ue157&adnKsNWI)zHz0K>)<8QA7Sd@ zr(VXhJbn03ekqz4>Q7zgH$T|=gffBkDzYcDwA)YhuL;}%_cq@S+`>89WE^kB|g<&&53s81bc|7@%u zR(*Wor|ZAfdc(1A>)-rQKlEe0fXBbor|Z1N7dyw~6H`w;^Hn^{$AgvLD9>kD@vHsq z^ZB=J=^x1Rd)r;RlKCpXofrJIzP=IS?<`tpRh`*>KErA~e(;k%zdwHRPq$X}6W5KT zi=X3Te<%hMC5 zo_gj-@u*MTx^SeMbviKXV%N=2>p8zKywACfs;&>uIV=z9di==i8zKIxNdLRl&u2JV zpPzVlSReGt!(ZS2&(Hg(t^Z;@p8cBFHS2fIM>q90D_&K9)aN>2HLvr8pLFG4`u}Wg zt?tKX_wGt~JG;NlM-Nu>ZdQKjIwQX8fYqG#AwTKL-{i4}v{v?t_sWt)5z-EkLtbdi~V|oV*fvn504*pedZD;;zM23*{3>b-FP7#nt}y`+z@tQUB(T*NdO{ z-oCPPDCe-Aea~O(i9H|d&+{H%U7TpW%!{+0F!`avD4)m=ep=5taoLfbq5h!by8>PO)O9}7SJx*mPQ-`RI_e|T5AnrN9#(mEIGP{N z^Atbn+ux7=Kl=Ms>R()M#B*Qh!Q9{S`bLNk@x>#Ygl)dv;E3l?%GlhJj8e2 z+yD6LF!k`{6RUca*RKxcXHlQIp?vDNetxPuPk!&`I>Y_=zGFIrUUi=Md4}I~_2pdh z5Fb`~bvVmQ{?b1E(Dgw13i)YW@1K0;{Tp9@U-6!@9$}g>tbm+n4 zn;YUK>f@)__rL%2UH5D)>Nh{KE8sbgQrCUtx4J%ai4*a67U?e4S=#4%VAb#Zg4t4PxQFwCw-sKU;6%YT8sL{ z_vsA!FmYrngn@^oP8;bp#xE1l%ig;hQC8C$Q?Nj^XE{r>zHAM&G(s($`w zJA*!7Q_ue9FTV3f-FoWc)SHd4#a**!-~SbN&3p^L+V-pL|`DR{!|#bqDEs zeB}E4+$_JXm+Oq;QJ*>-=~Z?8u;R({li%JS{M*YP*(mC7IH)t|@{^8!=Y_hwSY4hT zOg%h#Sml@2M>_Oi^34tL607~~oVe#@{gpVsPyDH_U_JenE*>-w-{liiPd)Rqan&ym zXZ4cjx<)#DJX!}&-w0C=pH79dJpC2F%IixsFH}z*k7ItS+utYOwL9;AeBxKT z5?-~x-7h?79_xzLp?NnOSJfZ&<#kkZdwlbguE+axUwK$-b>1BN#IA&=KlSx{{HU8t zUQ7?-Z#J%~Kk9QGu$r4M{G{vss{6frM{8Ao#h1Dg-e`ZPuVUvroy441J@ZvO%g2M2 z-YCy!Sn(^rod-YkeUE6Zmq@G&<$R-ctgjDWoT#3C;-yGG`!b(#@~haqu;THXpVsqw z@Dqo;skPeQm;O{|*r)iZ@0`K&I1syi`sJ&6)pMOuJnB=ou6ahfS=SHMQ;#qFRM&UZ z9s4Jl>U;Ko&S6cTdFhMYN3nY9i|JW6`FQepnOBFa)<=DLUFLz+`gHjz_WsGfFWC70 z$)}F#N_c#t57TdaXf7xZ>A)(lK8vX{if46Q2dw(k({FxV{C}R_TGbCdsVlj^)x7RQ z@~oG6vATKb%Zt@3#Df(tbw~WFP6t-&xlVr4bH48VweGjSXFvx}JX!}&AHFzIJ^REJ z(y8j1ulQ9y`^Dyk@~PwJ4t}b8etX$}KC!jBAN&4AS4tjz=<(rx%8TjKNyLZh;-y&i z%fqF*_&YE_0%(8#dKiBqYEqE zD6fCC9>4gB@B1frJNwmbDfO$r*;mHnU(Ul%`&nIVT{?;QP+dI2Sw0>d=~i`q!;0tn z`DtB#eD>-619|lmAJ~=nte?L1(r>yDPfSmqo_gl9jt8?YPONnCQZMuR6Y=EfcwXdZ z<}bQrOKVlX^3<-BI`m=sEw9gg5+~wsHd<$??x;^4R{EYV`N?m7-S(tITdVr^Kk7<& z_GRjOeDGCW{M~xKG^h0w)nV3WV?M*G&pP}R`+WXmk2s{YdcXFPle!XJzNS7u`HK(b zttT%|J@w3IJj>IU{EYENs1H_r`;(up*Xxrz-TbiD>VDYYbeHtTK6U?-TB|x8Jif|9_mRKy`bLPqD$-xIU*D__zoGub>iJUN z>(ANv{S*B;2fliR`bL;~_?7M`U-f4`B-@{>aew&XTOufyHm##D7yAD{*%U6EV_4{eN zox6K$_4)nQCv+vc`cvP1;-MXlBS z*!Iw_q|fth>gVedx=??jdg^UfY`vv=qds+5>D!0=biIDwdGcd7et+CP&XVzU;BA&TZ zPn>=7Bh)wIWj*`E5Z~NTK2aY(#oj;p(y7~9i~64L?hLxs{+8z>-Q3^u^u#%*dgiN` z4y<@|VZ|He^}~v<&QJ5%pKm_+X{}ZL?N9AWc>GI!=M=xyttU?>5g)3HXK~b*^^9lN zab2+L^ZJ6H)?5FxrE7;>*5B~^m;#&{z%FRb|X8$YdgtM!JXj_kh~ zsP6Z<@v8I0c|_m(`bLNk@x`+^>Qjfay2;}+%(|E#{Issu-|xNT#70#=?moQ}Z}vQ~ zjy`jV6V10-NPnr`Y@h3a)jIYkKk0jYa`OAOwN`aHc$MGo7oW3Fe}(u1k2e1ti#`>39n&zI`vNxdqcJoC$s`qZJm)RC`HKg@nH zU-&8S=gZGs*PT&6=xcpNylQ_tzwsbn({FW{*Hz_{m+`1i9cKS*T=|^+elFpsb@~1M zf6@N{p8D+Hw|5`;OE)p+RL?m#D_&K9)aN>2HLrcfPrA&0&wx7?iT901(?e*Y;|MniORsA>a+m-0*PkrY}<+uI{Q?FVld83$4 zg;hU(RnP0Fc=jhh>H2+=hkWWatyP^4p4fWg3iXX}RUFLm#a2skd3Nb(iXm`dkOB^!dV1x?T@{==g`VR`+A} z{i^CbF^~ITy~LbXJ@cb@)R*;)NBZin3(6<*g`d{*^Um*n`X#MJJzo#HU#aW)mapn! z&!^T)#E0tQSseAL!&%+r@fl`atj8Zm}UL9tiJXDABBg{F)aMaIF>)M}Jedgt@RsHI-yAmGXQeQtG z)y<=iP9i>37ti9TPaV$cCeL-jtY^$m>w0~1kK6Z8>ivFwe%~HH_4vteb3l3h@-X$P zeDX4;53^2J9VTxS(}5M=d$<} zv-*67RiAU3pVss9&Tb#rzqP9GxxFjV#ZP_D!__{Pr<0g^n-yDUsotnh9aj34-+o?s z&4ZuOT77?(PWn7rM<1W9OD8e))afk6s$U*1)x|e2luzUKoy#UbT+Ab;L_?w%@$4TE{-)Cw-q^UUSIZ zTC4is_m6wl@T&8~c|_m(`phLx#E0tQSse9M^(tSj>v|ww>UiAqQ{CrjZ#&@8tyTTn zyLTnL@0Q=@Uy9~j+J`^e&u6GVksd$A?!&HspCldYF`rcV4CNE`RenF^&RbfG-}k=u z<^3m8=Y(@Ub@MzihsT4u_34RIZ#K5h&gxjtT;d9?3o9Nyewx$y{ZqetMr-wX+l5c; ze$&NIeSXr%OTXzPrk;A{XJhL_eRQGxQq&KteszADhu;ssVry%)|L@U1UR&d%{?zw6 z6A$9^-FjJ1J-p0Uaq`7bpBTzRc_^Rgx?tAD_8~v%IwyYfyzZR3K0NVg9Xx#_oYkXS zp>?YIC_me8URde&(_w9mpY*+7wd?cC%DIhsKEsXG8zPVE^xNx`e|+2NjjX=MlY6iG z;(pzr*#FPt#yO#G9(gf6h`-sms{W|Yb--$FzVef<^W@jB?9QqC_qplfr+(!(zWxeR zZ?j_S!&zN(R(y3h;_=fQ{kOQ+)_!yS_lecFAJvucX7@KfG?zHBns+u&Zbu+n#)@RQ%pgL`~& z73Hj+clJvo2PL@-Xw&`tneJBENHgJ11^&LjQt~ zy8hWY@S(mD=G^#nMmWn)UdE$!)XfX!6Y29)?ER`2pZu8CqW<=6T>;O&Ox@hy>iWzj zPQ-`m;t|gB@!&|es=FRI;_=hE&h^(Fd`@dsf7PFLB|MLV)c3e?Zm5g((MiOI>f)tX z^~=Mhy7+vC@`JG`#7dVPL}f9gtfoeT8!r{D7U^yTTn)WgfXxWep9 zo;u8W#ud-pF#E)O;U_)se}8oA#^2}O<1L**7t(|FF@N#otNo2f2UdCY#92NbOr6zmm zdDRoGS7GW@ypgWD{*2|V2jvr8cls^A`@Q-vH28c|f37=PC;RkQn0k10DxBr%ulQA7 zU!r-Tdg}0%pX&18_m&GhjbG0Hw&#tFJt*cJeYOy()syG zmtV*H%Er%ku3q<>kNDOV>&MGF9>gsG8o-c2+PPpHv z&TIeYe&|a*>!??#Z-ldYxendTFO8S#m={+4_8~v3i>{d`r|2hAsj_?wNZ z>W}(d2dw7i7eDECm)F+*;O1wwR`o01)0Ob_r+($PdE93)J(zk`K6#@!b;NMgU)5a) ztayH|=BIV-%cE|3m)5F&-ML+fE=>KA-_9ZZiK%C<%#Y&iTN=~PK6yBrACE8mr0eJT zuU^%^fUJJ#L%Q-vpP%j`No{^s}o*F3wmdVPMy zUArP({i)Aaeyht@e&_Y-vz~Z_vwS=_(yi+HVa0R)^3(O+e!byaS3RTspZBw_{@(5& zAMsP4pLpt(-+0x$Ri17lz07Bvx~?ZN`!c2j)#-@M#ZPtncHe(KwNafXhh5S;@%Tp% z`uUW;ym>rd(n-XJ>f%`(^{K;I-Q>A0m~}B<`Dxw$S9;gh&b+vPfTsT0b9yHq@#%@( zhvb<@9Uo6U^~8z#GoNwlX1$85zFZd%R(ksQDfavGpTFX1tyTSd{;E60AN1c&08GPTGjLU((9Ylcb+&W=$hxo ze!jF`&XqA9%=&EXx}d&Fm!InVe$d}Nq_wL1`zLhynfjiGtGaa{J(zl8s83wsEN^Ll z>Y5u?dVJ+4o%KHpw)Vjnp50p2x9-=K@I3#eKHXWr>A}?7*?3jm**-qQYF>|fe$w}T z?2mutiLF(A+tr;x*SV1T>iibFKkkS1)Wwxv=Bt=qRhP$u@=MX&BOT{2Kds~S;6I)E zBQ2%A|D!vDzJ7hsedM>idF1IN;zM=u2xs|taHLz+T^Fo)^8B=}@3XyOdpB4eA5T2H zj=mAj>RCULe&(0PvpVL5)jIYkKk0k@eZ>>*+gjBRJ-92`pOC)woHOclA)Xjdo}N0C zPo6wXzI-Ac%z72)`eIo1jr@Mh>CbJg{9b!{SE9?;)L(j@(5tRnJ=e*2mZuNjx=?)< z>A#Q# zTzIos%{`m9&MZH9v;BOAsUzkqKk56t;k8@NZ&dsLv|DvWy7=^9p5J)Rk?Qp~T{^JJ zt5-PcQ#T*1^r}1^Sn)hx^3!@=5B~WXKh|2^4?1|_(K>kgMmVcSw?gYw^-+Gd-@LHW z^}NPU`o3TF*dzK*RX@AGopSx(3v<6xpRVTzb^7KKC*nhO@hpz|vYzqmI<5y+ef0S$ z=GV64`@cMD|L=8|&Y%nF>#xoe{?o^&15*!AKC!A-dHw28eirqa8_K7SIzREee{$C4 zhqV@;C+EN4Q0;H$G=1yRhw5U8539WTET)ba&gvvDuOsVX`;DL0^?it6e#kkEYX5(I z{qHH!w@>Lo_kq5=KKH|V@?w0bE*{}5A1~LK{_^IP@&rfx)AC7-t{Cv4CgvVF< zP(Q!%XOYg%qV<;QEbZentoqf{Z~OL^59`mI_WxBM>66o|{B|GHZ?Qh>(}U?xl~3Mm zOfPY^KY6YLW?gJOev0{hze^w3sOp#Z?Y>5rf2rqnJsw@LzRatq9$x0f6=q-ZvYv7B z#mO^w=JBC@#!r0b_v3%yf<{$8Q#R8bG#4?V7| zClAe~E{0WpwvIQ#YMsoR8&DKCg(81%IJaoV4%j+8<{!*m7v@iKf zb@&Xce!lV(&-3UvuI+z-P~BX3V(W-2)HlM^!>@GZXL%$3QXTWcO5fw2pVskxi1!}( zhSuu+m9IRgE7A45N*}sk_BUOqzl!NVd~t>JV8xS9-Hh=@s1J_#{4}Tct3L6l{tv*o zANSd(E8$h=iN_7Ut*;N7PYm%l8&}mI^|=mM&8^N)x<22z{3ZS89rxpiH+CgF`#AOO zdpu}8_enl6_0%&z8(01Ea8@sQu4`6@pZK2d&ir&gK>U1p*}FRfukzde#)EuT&pGk( zdNwOwRe#i%*D;!xpLF?s%B!B)THhkEE`&$ddg2Q8jWG4_D_!|nUh4)Zp@~JE4 zr`Yr5uAeWvb9~`rBA;Q_#p-!}`~JyuUwK%gsvq0GE_?mwm(B-z(0P&1m)4UPyBkgMwoi|bSj+X>96=zUSFblp?d0g z{moBx--p=s_hvY^5&0y`XP9+7=RxjouRA{SXZts*`k8z8PP*kd%B0YKh zrATLKU-Fmg;3uXozxe5TohO&w|8cEF{XXY*1-kgD>-9Ik@$|XR;>6U`pZVEXKh(E0 z)|WV%Kl%K$KEFPFvyFcb@2vNC27Ub0tM)ga)%DYZsaNHbm+`1i-5fA=Mll^&@vHsq z_26Is(2uuP_k)hP_$LqP=Kj_r*_mBI{o03j zCAxe~ediIs)#>Z6Vtj}%o<;gF`{=@~&&K+pzFfERyMM~Cw)Vrnc0_CS^M2}7`+MZK zIV!ZyW}$VK>dp4?8CL7qfBdBHe7*1=`a6a8_ul(;CA?jJcR% z9a!<`!iqP_>xUygKh5Xooo!Ft_4(_1QRe zs`cezrEj05-_DcspKxw#RX^qMu4E4THTBbPb$#Z7@(_QsaaH|MpX-3t+;sRU=J(%S zczkPBzkL6$gy(UP`tGBBtF8~4PYm%l8&}mI^|=mM&CM@<(sh2{EWQu1+m^0`r$6=o zz4~oV^NU9~;`13+dUPtkFMCc`Q2*cc@6VgFx{fM8>dSh@m2T$w46`nF&hpcG{yy86 z-n{Ye_3Fc`V);aUvzYy}G5uM7#hcaPGpzKSuj#kXcRsn}v8`2o>l3;XUHz$FohR8R zPE5Vo>mK!`ju>XYcvK(FPp8`7{QB(q{rmR(p8fp>{dqsc&V6&b9%w#T<<(~~b;NL1 zCwZ;|W?k&M`DtCxmoNB(jo&}H`Ek9IE8tYUbYPWNuh6lvqhRW~=xKCz!y z_(_l7zxs?fHLCr8%KFB~(@zgp=ZU@%(t-HmrC9Z6KI7SS_zbH)zVOp}J}=(Bci+(J z&%S?G!dumEbJ0nx=3O;Ue`)`0pX-6uI_mtS?>@ZrecM{A`ic8?CA?Mr)?Z;Y@2Yw0 zEbX7|<0GuralZ1CzVB-ub<&pB>U=-y;Lf1yc`^0fFZy)FUKh#J6Q`bf=Bs#?pS+AK z{mkowSr>a=!^=V zKg1V9`NUD({39N}`HAm5dDbr-(3W;T_BgOlg2%s{&;E6Or!kq5kySJkA?BiFBa4cqvx>na_B39oGe`KIZ{Ht>--X{P*|2 zH=};Vx)5IFx5o|MQp|bvXMQ%W`sLxQUh?=n(s6$C6QAE7+ImcrzGQvFZa=E8L|=cd zZ~xlg`1D6{>fvX;if8%B%ed0dygrz9v3^o(*L>-u)~ZeiPdvMhz7fvqS-(Q-RP|AQw%@$4 z(yh*K-#^*y1N*mDe($-xE77gy$@2tHpM5S)tmfU>ymgl9jQZ4JrSEagPrlgS_r9{9 zEb5o<=t_9*SL!>b_^NImeRLA>p}Kg4vwS=_(yi*Q3*x0NKlrK6uYY;Kfvr{j!q;^r zy7=^Ap5J)-(r~)Lhv=;R*JgF<-jb0DB zpZMmH7pudZbF<=E{aK#5TnDWBtMi19zyG!VlP16Er=E4xE7Ui_)WffItNf@h>ls(N znKv)Yy4XJBr}dmCzxl}ifu;J{zug&h@##U28~T~2ub&>ohgDuZah8t1;O6>d*4b=Q`l1pPzX4{bP>o-`w>56#95# z>xe7VH^S7zuXN>Sc_aQ(9rMCUpRfG1j`QTvdvE-_IRCvmeEP6DFFYRXfBoi!bReF1 zDbDKVIvH2%(jW0X-gAF@-E!6L8-IU(%OQP5`ufdbJ^kwPP=ChqsV9c|#1+o+miDKv zxnZTpkM!IAKH$y$|J1Ks|9%zST-Q4GFMWA@{T1SGHd=3~?x;^4R{H$lCqMZ0rVsr@ zYgMO%=X{Wd_BY;AqyzPfM>xyJgCpIl&gYSi^P8XeK2Q6>k8~yVZLjJpx=;8y2VFdM zbGc7qJTX43)={r8`^1U*<;Ad?E9)bkeauhmc|CZ@v3;ki|L=Og%5|-Zu4`%kY#*Ot zwT?PJ>HB*HZ+cXJ!^G$J`TZ*UH1%_T(@nH~=2K5S^GoB@5yPc=_^t!WC;Iu2pVsw$ z?BBlZ;@0AR{O3<~MY{N@>v7|pz;l1p<>|rHtMbVc&+_r0{-vlNR{i|ur|b9nKflCJ{oLQ`_~xn*f3wm0a8}=(6<-}zJo}8F=HS=Cw|quxRX=EbTpbbX)gBTwDdTAer7?%x@F#HTOzdN6rn_Y+^xWf8U-(InA78n{OIxe^kiY4;F!jVxpSZ$V-qQZmH8&jT@srMN*BcIe$#ye5=RDvizWsfhA3m>9)vtbGXV9U%gLPz~fu$+Q0l(*Ed3Zh%cVSQD4?G zo?U0;Go8xs-PZ}nzPNwB!|${E%kk^~{Qjf6&M5D? zV8ypT`DtB$|Kz?$zO1#X)4{7^`9yuQnEgv*>n!cVpY7)})SpO?pJM0o6R$g^wWvS+ zqOL&KK22TyS=Z-2TTdRQUX@Rtm~Iuz)1Bq(OQZwUD_wr7J0~7_r@OXR_0-{;Jj~}y zdHN&7-&wTIsyeg%e1_F}_8&j#d%k<&Qy$w|)zAJ^SHi3Ome2LA=YFcwgVnq{n_pFb z)R)&$&FOK^PrCd*{-ZBwt^WNII(TC1i7V8HFHTgaqb{y+mZ!hsS9yJj=7s91V;}NU zo!>{^zrWyiKYsZxT?tP=eK>obn2#Q$v)Q<+{;1D&z-n%fZ+_DC`um&@Z1npN-q{&+ z@l&7Q>d^hkIz5CgNq9`&iikzQ5TU(K(NU%9`%{(j3L{U0Pz&-29d9zDoU_fy_HFz1xVhgDvE z7E?zIXLXY2I$+kt)$^s#FJJW9H#RE2&%9snG+*VneavrsXfAQ0`JlRZgtL4+IMS`^ zt_M~;>+nU5T3)h`d1>f)Oh$|v%hpVsyI`=U#>v=;Z{%JuVvE`FuUUv+)*;zaz- zM(Zrq9rdZhO22x2qVJ%Kf2g(cd+o(tiO-%FQ{VmKt2$kMbQ1BQx_A~xed=&lH+ilL zW?fwQZQsBC;KLgAdcW5jbH33!*3&n_Sv|THTBoXy^0WQsg(F>l()azr^A7LNoSsj= z^_tGWvrkjM+TZx@C!NI9Q_p-A&+_qLr8mm+8CHDP&rj?6d49)UC$?7iW6Ptv5}y9l zmv^4vnS{H5nU0FkE!o@!TR{tbDm@!FZDJnUR8h8 zm)B9v%P)S?^?ud158b)7+W%*t)|K%1m-_t7bHiNnVtO$3HY;9Lf7ItXU^TCO$WOZb z+V%HLyK{W0eB?8%c=GAD`*7`T`wMRME8fvpwtlt0ok#X@^4(AKsY7#ZHm<5a>T?~i znwuZ|qziAk*Ou0*{$HQi75S_`_4%1~ed^|u7pLCR*!=qFW?nrJZ?+E)>Q7z!n4fsg z^^4!M@%`9~U(gjt{?MhzcYXX;mp5-(G)Tc4WU& z^#}ETPVM@CAFKC!CPt?H>G#)ouZ z^2F8tcAk9f5r;IY*XNg<)H~_&jUIIVr;qyNb6znWs83wsEKh&(GsYXCK3MTRPw>l>ZxbGiYuMW(}k7ZD6bz@eEX1}=JEc? z|Gn32^p7SVm?ccl~ z=5b%>B&MEv=11|UPaTf*s=Difc&W=5eyYpw`_KIYV)er==}LHfqYphkD!=V#I*D|k zx_A~xed=&lH+g)9Sr^;K{Io8=FFEDJMpfT-dhf)m{MKLjEl(%Wd{A9H!dX5Z9O+hd zKEsOVy7_5c`}6RF@6=jY@4n%Q`6#YXAHFzIosPP= z!dagFieKgRC7Ktirw(8FsqXmr6aIeaF&k;N4izrys+YV zzT>BL@37wRm3JJ_{?GpZ%%^(+&;3gMJWtg5tdCA2K2#Sk#j0N(F4e{7Gn7xP&J({s z|2GdithKlwet+J4d`n%A5B{o~$9<-gh!54pBb?>q!I5rNcU=%Kb?rlbs`LBB|LgG9 zs(#jwbS1j!r+&KX?u)wqjOBA)yv&PX^5iS5_*I@x*3Atoo_)wqI^G}rz`NhjTGcOn zcvr%+Pw7LC8-B~{GZ&PH_?wNZ>W}(d2dw6{5BW*g_wE1q>;3(2^{+gpGx(@K^}XK1 zOLU*Dr!G!C^~_hX>#p?l(S`C$(R#4zHxEC>zOVW7ulTXn>iN?5k?G>6zI~rOan|Kw z>TPzsbe$1jz4F8S_9s8_o!^(f{??7^{qgfp=nVS-pPtxxjb|QlwZHLFugc>kngeEC zOc%2E{fAP!#53Q{DdDZ_m58R`;XQ$#w7&@%U@KL_GcK;t|gB@!&|es+$*9ylQ_tXZCo= z?OLn*;qN_~&pu9lx_ng^=XpX8;=?Mho;b_LgQ+tc^BGoseBq~cohP5W#XVcA`mVok z|DB!WGyKl1wKe;UpZs>7oc8Jd%?R})4(|-SYJbb)!SB}o&g->)m9Of_uVOxrbetRf z#P6rb+S*@!;Ka6s`YG#QOu^%8&f#&xUv+&j=ak3aY+O};)aN>2H8)@QN!R<|zjQ?Z zrj^%=bnrY5EaF(YZAIdL9KEtY?FZ{Ibt=1cU?aclGn)@;P@1eVo z{I))QbBPo2p}Ke$M}6vWRyTRB2WDN&7k*mT_t~EEdq*^?`rrT0-l-3t9)F!P>hjRz zLfz}qoL606u7i&U@x)LbnscOsPe-48_E&j*{G{hx|MCS}TC4h%JskeMNB1Lrq7QRU zdGl^IuBt!ka~-gnyYkyPv&--9+>Pybf0kTZ+vj=xj@jRzdGf~f-LJf#)&6$>?R(de zn0o4&AH}0SbvV+i>aGh`JihSLy59f3|JBcIt?G2}#G`fa^o?*=)ywPR98B zROiR>KY4JYs2|t=IiBmEFZo0V%IEc%OWk^jbfCI;gtL4+IMS`^t_xN?d45`#-(Uag zQyWG7$^*NDbiIzD1LgUR&u9A9Qy0^L>f#a3^6}tEx2o%h6^}3cbiLlM+ULQyZLR7T zKcFko@9ZGxYg6 zCH37$e$y4ZAM*6Xsi!aVRZPeAi<6)0j`-Ox)(0~$<`+NB<>&dw-K<}!`aw_Z4EsNQ z(C<3b<;Cf@Idb0Y6DPVJ>x!)>PQ-`mmAlkxF ze!5<|vq)!Ew9a>{pTDqLpI`jMrU&si8&}mI z^|=mM%}s}&Vt-%rX~*@~-`+o*{d_*pZ*%b3{f#p)H}S9N*3)Xf;b%F}@r z-#+9gJ${^X&hD+%{h0kcO+O!VeREZazu9QLrMja&bvV-JC%>IDFM0kxtyTSuKkQ0$ zJKKox?USFk`dGQES7hfLAkI>wBVkn=e&QEpc@4FxRgqFIozJKC6%>m`jv)O2! zS=|xeys*->&-h8#=f&s#d{?mlx9-=K@Rq(l(Fe^ZhSj_~n_pFb)aN?jXik38bsrx2 z=l%7G`*Fq_`_A|H$EPoLAH?R6caEvk!%ICps9s^}R6Kexd6~}`-+EOY&vo+?-_MV) z-gD#Yll*yyfApaH#YcJiBgEfXw9ZnUrG5N{RX;zf{r#@jp3p{B|M>b7DZR>X_mRHq z)i*-?oki=csx#ZqXIQQ0^&CIx^Xs*re0XbBKklVniI0%J^+vA;-6!kO(N9kuj`Hd- z`{bcIlpkTvDTbqdep=Ug@Yc89y|p@Tww%!w@%Wnh?iU~N`K)d}b(nga9WPyH#CILA znpd5lbouyq7xq8M=6+nhe^*Lg>hqJ|>iVGd#Snk9aaH|MpX-3t+kPX1sqgWj4s(CY(}Ss3<&&2&eVBE+>M(huICUz%>*uHSy#D^fA3dBP^q;q4T^=9=)e05mq+MoR7pU>02efRseR`osl@p8T2sjol%mWTQ>mN&0D%)EGn z*^i(3jPVkaXKt8$asTOjZH=FFoCh!Zy$3g{x;{MdXdOI#BTPMfIu*|H^d~=Kyb+og zR{Uyz`##&gLf$ClMd2i)V4vrw(Uzljpi%)-&d(b?xt) z-Q{JC>iycSKiC!V_?CM7#)J6k=2M5Mw^{M3`lG(Qj%r@#2|wxj{>iS_-<5M4^Rs^2 zZ@Is{Klpn;yYcm)RooY^W8|UzJ9>RWCy@?R7msk3j|WG(Rh{3k;yE|cZ~Oj!Uq8IH zs$csPUCDgzSL#=Oo5y_+(}StES@Ej+qdwOGt9j}0Q|#yYZ-2D^!o2!k2XrO8YJa<5 ze6_wlXg)E-zp-%XIwQX8fTOwjN!Q;$dBB&BXszmW@TyooQJ=ZQ6=wg^nC{X({Mmlj z1NA4;em<`sPdr)&Pu~br51&qjvpoID&lqon=7kmC<2_%Wc>TTW-{0xZ zjqU@V;mGgY-@ecG;7j|zhwgqHzy9}>=vH$$zmwd;jFmKJ?VKg!^&m5uL#|d~=BD;+aRxZ+v;Y)RV_oAECaArw%Kge6_xQh@a^7 z96#xK{rxvjI;XX$U;WLlfTuro`HctpYX8e8rk;A{NAajn9gg&>I-f^6UeECp-|Oe6 z@7e!7M)iyL=_}&t&pGt-S6v^>Ipy(hEL^(Ii0?XJH8;QbN!RP|J$~e&tyTRqM|TJD z^rwF1x5tAzoy62r&-`p$^~=Lqz2xy3W?k&M`DtC}$pLTfpRB3Rejk}`wZF|(q4_o& zt+!Nn)Ta(BeSYwh-`=l!^1nQ{wW^=^z^;U6zo!0Dzv;o$+pKt1{ZSvEVKuMwfS+{j z?+<-=~Z>t1uLFBKdtNe@>|c^`1?{v{I~8DU9bD-Kz`GM&Y!H)!=nS$#UrEx zD?VLV@kV+5qxHNFPQQKs0 zTlM4FpZvu4^WO31LT)XimGd^~l0b$PLRh4@un9xrt>#;@{p zV8!PvKk0ct_JrG<+gjBt9ewH*;>}{EvovqL8>^4cuv(|`+j(;ByI;^+`Tg{J^vUf< zNZ)$UbwYgWW?mmX^$PJS)HkY+c=~fbdHqm6(K*Xcb*~4%@r3OyrM~Ou9S&_o_W^FK z-VixQ`RV@ndFM_4a#SO$@6|tkxc=|WEPXvl-&{~0-Z0_j{@<$lqdwOGtEoM{`AOI7 z!AG6;s11G}*1t}8{rB1MQ@?t?#7{IQOuZ_fyo^VESTR!Vt#5Gy)`fC?>s9N>>lCl78 zH#fvf)W=UTzaRFg|I%92uehK)NtdswYai3YmtVTSQ-3zLKAi2tGq<=xyxDd5iO=s- ze{*YF!q5AsJg779_?L6=GxxXH(?QNjH}$G~@@8XtP+!LKBXk|G;=6u+T8|&^`SwAr z)%~D@H|uxy>8}ufXOaG@Ip1OJ5Cwx40{H)6-;#K*oUagxtRZQ31 z5Io zXP?JCKk9>0Ji4*amxx}-WeN~+K)F0JH^V8!uKk3S! z|N3V%s`_5*|F9aKbAcYL{MHA}DTZ`58&}mI^|=mM&Fyi_PrCN^J^%8s)~de!sIG)p z`R#t?{?}(NabnJ^p7|=C<>SFhZhBuWIcIt>dnUb;cQ>>%#-;l#!pOswZHke{cGD=3%}3Ur(cQd;ajC^f8&ek%O|GZ z&c^hY>df|ye73IZ<|lo>5BN6^?*IP0`fmN@%JusjpB~KpEsvk`WsJYsXnk1eXFcQW z%lRw5{mD-}=gjTy+Fx+1fAIBvB|PU&uCL#IRyUV*=_KOsEYe+7XSUyU!D>Ce@RL42 zKJ&bbTC4h3{=6$CFZJCAe&dVPd$<}v-*65RbTab(EHze_2c{cznACV zt238#C-tlIM4!3DiK#am)0yRuc(Z!02UfZs*Y+=cufHGv(EbaT>O0=j8FZaTsqb|U zeRYoq_rrSh#Q0EMJi=K%9vtabb^Wm7$@A0o^5w(dd}E`iuf4c;@DZPm*uKY8HxGS$ zJav5aDvwv8ewaME>M(g3Cyx$HzSwp0laBYRzOvTRUbe2X?^GKu73|^qjUV(m|YvbpM>vutNQ-n@t^B|-(Fo`t~-FHrF6}3d`fgjg5}x~#`pzTgmAXD?J~70Y_lZRQKjm-f=kECtiQw?qB*lh3d~+U&wmySL*YVkLvnHh!63_OR?(De8#iu z@EKNpmEZn7m^*&5zjMWJIyqOyc!_wknEgv*>n!cVpY7)})Ssx%Px@Y;-0?3DY%S{7 z-Ly08TS%9l`!+ul`K;r~CsuX3RXugY_>c}vp4jVee(HA~JnW)ZHL804 z{qO2Lsr;rtLi6n`T4zU z8LeY|eIragd^#1*^7P|F`K4%Hs6Tbp`Kj*p_i-OQxV5?;m5x633h`#K(i!FHk9ec{ ztPUSxrRzN4Cw;HKkGko`e~FMshF{bW(sXMN|9Jfxd`>x1SLLpnDWE?sBDcO9^r+d0ioy8M31O^Zhz1 z@a)snPrvbJG4*yfw(e4$**-qQY8`s~6#KsB!yey%Ax(YP`?25INj}5x%vxKk_P6u< zNB>jbsr)|f&RxlUsrI-0=scm5nDeSZ&dYbF6|8ZUfM)|Kd1*TYXfTOVJ4 zh4?#*)}xcLe1-V1;^lQzJihRguHWZA<;nMKt=?bRc3fA&vyW5X{o*&ix#Y#_F!eSo zUR8h8=Q?0Dul>zWy6(dvZ`t_%$^N(Q47z-yuit%8mls=Co*qm+G1Mm>;Zi=msxR~A zhWZm-ckXY`m-oEolUj@V74PUPS&y%&>zu}e^6sa6V(O`9eiV=T)Zs|4s`GiI49QCQgS>5FAtlxgVeC4N(Y0~b;b&u~Wna@7X z^_>^|RX3Nsm>$Hxv2f`+Bfjf^)!fy2;`Pa2eESit)${4QPVY+kat=PKi_HP$_2a9X zR~^dZWnLYsLwP6<Ri{8X3U_3w>xt|NaoW^efZclw_CGMAe)Sn$+4G=vaxV8Vd6nPEOFi|>XKemzuH@6j*PpymtRLd1 zp64xos`Go-_kFr^=_5a(I+Rbe57Te&pM3F8H~#$-{qBo+v<{v=d~u?>zEzR#jooiv zSgq&rRQY|Y2X1Yx>c_pjUxDjzzf#{hg$J!y)$vlV$|p}e%g2NImm;5Mb@++@gX;@! zJHG#)pFj7yOJ~rB=F`tl{>qE1=hI40zjYF;dX?9&4&`T2pShuY>ZtP*&)=i|?3OcH zi~2`@rz_~=Tk3jVz*85iTTfn$539U-B0kKzSRKm4Dz6Sl^W%BFz)$*~FL(WWx}56B zpN-l2{gdQ-+~;}ned~gEe#6t-|EYi9g&Wtc&TsqP`C&f$zNy85a-Uh3(~e8#gpeaW{z#2XSkQ^WnLf5y4ZQZPxCpyKX?35 zjq3Nk_2G#}>)`3bhw@Nf-)15GrFx@2^TJBs{^TcJ`|rIwHokw7zYpksrGD;jb^6eJ zVu-)lxT^lB&vn3RZuQ*Xz7O%FmtW9Y-H*$^(L0gk77pZ&>CI=5eMIOQJqZvW@?;`Tr41w8$!FK>N(zE*X-)Z46hRsB(4 zUS~Bg9zW@Nop{Y%`xn^k|AW>yJ|5pv-+9Deb$uhm-z>D=QoY$eKEi4pb$-(K^W{B$ zyMI#ee$c^lKFGuL8-Et*>?~SuRh`*>KEly@{G{)Ez5R!~^X|u`@9#=@mEZbvf1Aho zE~W=Z^R8N7Re#jyI$$*)zxYYl>%og&acpZ<-+TRwDR|X+Qu(b9nokU?d3QFys{W|Y zb--#)`;VV=`Tf-WPuk!&9X$6-9?tqrCy~yoNN?4CeX}}zhWZmdt}DNHJ9qci;=IX! zU&=Y1y7s;MX>RjWdA!uqm-&omdHRx{G2RIE!HTcWPuJ`D@tffio2>^ZSAKy-%a6@3y6P;_;6j)X#5ueIvx*S+velouz$zgjGLZ_=)HH zK5u)`#=mEJk4N>%@n-k8>(!TY$wPct<<%=3^;PvMU#;VMAYSU&f9bd9%WoXre<4kM z+v#12&Qib4V_iCl=7s9wSseAL!&%+rxh|M>akanqTqnHYd-iYt=lN7$&X+M>BA&UR zd}8)*R&1SFy%FDaz)IKi7C-4aPp-M`#jVx;rb8EB9@_V#eXO4zqyzE9BgBUlUmYf| z$|ujSV_v@W_`sqRUan^5okj_%v*?#LS)!{R&`uUN5^Xt<$ zJG70Ue%AZD(nwc7eb?djjJmn#L3~){)hitJsha~dDhlAECaArw%Kge6_y*tY?hJPj%<=bB{T@Ev0_#ul1Gj_-7ty zfAdk^de!})i@z#9L>#7bNcr$KfL#&TdUVEXa8Kk6MV#{FLocqP~PK8o?gzY zo@l)aQ>WsMbk+4|EN{K4j%Oe86W{B>|9PLDfp2q<_$WK1X>&yA%AwI0~ z>WQ;_JeWGOvFm|VpLO^t_I}l?-}$`Os!j(_JX!}&-w0>*=vHW*sy@ok_L~=0y7o6e z>GOO4Yo6U&?f)z8+7;>QPkrY`?r(Yf-+J`Ksi&U#DxT%z!AftG*AFYc^MIeO*XzNn zc7I%Jbw9Qo(v|3@pZeVgb$O^iV|nwci(!>lhqFBV#Hv5@=7y;w<_ACN`1>cHz1L4R zs?Rs|;n}b9aOAiC3h6*R@hpz|)Zwgd^7st1F1Ek?ui%$j-LUOjP^j|Wp{Hr5ZTK6QS&etw+x{s*^K=gn?^)|K$8{q5&R{^E<> zXX~rOoVUs+ZxpAF7>@d@y6b=y&pza*b>;8(sq*=t8>URdK0~ zd12MhSAOF8dHz?vaIZG9`l|o_JYAS`$;0%i$|p}e%THd$^hW4DYfCzioeK*6T$&c;eAIc=|?|diZoI zoaO1qhw@9&yikAY+QwmO`Wag%^Rw}6zj@(EpP%&k z{l$lG{J!R{e-Gw6JIQDGomp#Z&eQbUIdj09`+1^%<^8)7UH2#T-7os;`1&iv-)ywr zQr%IXI;`~h!B2kpJngT3;W@2U{p$7iWA#<~?iU_is85V1U(GA8PYjpl^;KBSlk4EY z)Jr};n9n0o4& zui{xg9<206dDjI;e12Nj`TeC+`w#Hyd%n9f=sKrUUp{^HbEN%mJ$mBQQ_p-A(}5L_ zF06Q?yna~m)%j^Y@1H#F>i!3-)PMPJx{^NoIQ8xO^joZMJ$mBQQ_uV;rU#Qx7iN7n z)(@*b*Ue9J`Tog)$L-!){XFFJ9lHEWefyZ6I=*%3B;v!=kso2zC!bj9;Z=3>&+_<* z@AKkc*!s}6grE2Cb6sbQ^gS=|7vFtwp2#Ow*R^Wib-`I3d|36(&d(Qq(&hL4Kiki5 z^-~V-E8^LwIfwQ6t*%cU%0v7c3zx1l;;Xx!YHs_DpLl+M{>jgHVoRxSKdLk6SAN^S z|K9vIx9gn6Q6HaSrJH`!^?bSKy|%Sh=lh92)s^Ua9g_N$-}>AK>&c5#Pd)QhJj=(k zF06E`JROLadSZTx`Srj*+_$x=|HKhpi7wyhL$6!1PG5h<@^m1cxWbAzn@?WG)w4pLG3v`Ex&WZfjM);$OOw*XQY{ z^_=Tj7dvmPhp*qf>iV*dC+~VPFHXeQ5Am!o&U((JF0aoz_8~v%+ut`m@66Vser$ia zvhv@D$a$=nXilgup2bn0I-J!_p6h~H7rPJqwC-)!3EzMD>Fxj2&wfZR(A7^5+P~^_ zq5h2J>A)(l4(U~~JfsWdm!f{C-#k#ApYqO&({A>2tyMjBs`K0H8-8b>bt=S%c;X6Y zdHV68{8Hp2)StTcAwSjmz0cSCAB6CHdfNKmx2KCwA3Cr2ji*mtoJeP9(Ykapmah;W zR=nBy?L&Uj<@X2P{f4%L`*GpJ`-*sc%Q@)sTU}qyClB#A8&}mI_2uKFWAyr-H$7Kc>T|p&SCn{ z`A=V-J~W>g;%_#tsz2&;9k80)`O8nb{J#C68_(}6|5seeqxO)4c1WP2NGDV3&{wJkfam0f5jP4mMv`__D)@n<`2F-+_kCaYbwBU*KI0i<&N=3o>G#gFhBtoo;^|*rzkGV|`-A-CxB2{p z)+dJetBotw@AbJ3SgqUZZ+_DC{lRm8;gM6T`*GC6ni8J-mHO#7KC~_<4^ywoC$AT$ zPA~51sk;s+pU5wMn(y_=N7|1YmejwwUt{RQPhIDF?r*WDU+beMPCb2@&)9mPx){ov z3*{F@>xS8HuKmqVdGBXC`dyEoTGi>`RqM?CoqhT%#9t}WU1`6*Q5`-*{fYJ&Kk3__ zr@ii{rxtbn$&-hpep??sNaxnVMd$SRt^-!Y2~jx+@)gJbvQa-=E%i#6-2f_qm{T;_;6jbRYBFHBTR% zL^@Di+{00R@-pttQFmQXK9N2@#a@5kv}X6IMg7E$O#zQ@sVmQKeCtsctHab=t$3yS zy*}3gtM&SNg`af&{PMH=&i{Ve0rzW4czjEJ=LNsj^+D?sL;PC{7oF4NyAD{byLx@% zJh=K>?IeH0EOGhgniAd0Pxn#Xdd1Ewd3w-#)vH*(!tAejRX+PO)@R*NKG8npCp~^| zJoWsk#r{8G-==`K(*12+bP{u&D~;*i+WoEvR`dAEPwTVqU-9tErdIVeM>i$BMg6uO z`A}>i^2rm^h4P7XVb(`u{jlm&ulD!jZo6h` zRe$p(?Fw8+b)I;9@Yj5N_k&I%K2#SkidDZnTvQjIk5E35FZ?vu{@Y>OXH6~Y@BZbc zfXBbo)z5Ey>+yO~tPWGJ$|tWEr;Zr*`m4I@fIS{R&Gr719Ur#;)avW~8`|3=x<5~I zANg&*^~j6Oh4`zDE7kAyxenM{H$Um}>zJcYn_AtE{5{cS#d>w@wSf3*$T4{S5dF#-u^~XSLA0 zMfFDe_zZh<_(|XAJ$~zVHcYMR&woW@I49h%)aNsvy1d7ad3a)cs4lM1^;SCBM;B&& zG&UDjed_$QPOneC^rj1^R`quu+LZA4m-_t7>(FPeI5G9qGvAAQeOb@Ar?2jMpnM{~ z_-UT^pFH@!cbQt$H(cBpbkk4z>X0tJ^IbiW4%9Cm#q6u%%%}cny~lIi{4~eUFTeHI zKRl(fp1Sx_% zU6}RJSbsH#4j=f5@Ar#;{e=5ZRQ2_DZVWvB(SzWKV^7>`YW4Go(f#fDGWW6dWZqnu>#g$1tKzC(9#;Lm zyz7D$-}4qf&Gmfuk1uV`xF08du_@v4E%o(#{HTk~rIU!iT1bCUz0p2C!`>Wz()W7s zuP(lPYE}QtQ<@UqqJCQs-{~agdet*u#iM*YSn2iht_xOt`;edJ`g;B^UvQtPRs9}c zYf5zO)6{n#_^K|}XC6Ha32taz@EpJMMHeAlHGkq_h83S* z{4}@ylwoP<-sfL5wW^Q){tn&r+j{tJUSh6SJ@cb+)h`c6^^)hhVAjRv@KfyP#d{t# ze||smfX1L(JzvW6+dQ%7Q}gJF@u9l7hok)DW!#&it`Ev5@+JND^R#cBHUIlk`qP(; z@e-|v?_&Ll*}qz`Iiq?#zUzRMu5&v5_Vvyszw^+kRo&-3n(tgledjY@@u78z6H`w; z^P_RqFAqoclIMD0*2T_QewxegzklVS6IK1H6I&;qetOV(k$ziG=IKE?u*$0^j`Hzf z>Ws#&3s!xuo1f--{k{Gz^Us(2-M=Z}RepQi&^KQnv_3J!zqN4DIX%AXfYrL42kE!h z-@pA!>!()rPrjrnnXf+j9;xaTMS&96_^9X^dzHy=-Ip14AN zJsj1m=E$2PUKB_Btru2vs@H?|_wKKFz|_j`_aD-fti!pG`tGCstgg?xpghEfRbG7* zQ>Pb?>beeC^?BXKPxF00_M>lZj;U|ArYYf7&zH`N%5VMFCx+E}S1Z5hoF3nGz-pcR z;3r-8;S1LsIkl?4W}n8OtDnC9^jjWZU7j9HJv@0>ES3pc^UWSs9P_TPo&RJvB&$?zrT}?-Ez!lh{aE_{hi<+ncfL7bRA_12&K zjP*nHMPq%5z4haJUQ557-!I$cyoqZ6?{`+~w9e}FpgdpktxI044y_MXdG%3DonAbu z>pEbskDun-pV!=O{{1K4Y{$zIzwqgcE5FsPOH5Bb_3-3jmCt&`%X}|RU9ojT`9!|t z{`URY*F3F#LFx0YE??Ia=-w^`XR&;)Umt(f<;5whp7~x(4m7SybkMv=0g0{#+B;#`dkOB*3FOf+xh*?!!Mp%)#>E?-kj{yuTKo+^{p1t zUsSKxXT7k}SLY{P`}1Wd&Ocv{zTe)ycR%Txr!JqE>s8NuFYfiJ!=7GMcU`dJx$g9P z=UKy{uWUb%_x-GEE^P(t)1UgzDSoS4k3Kqy_)uM3;V4gk#jo=E5?vQmPaUt5`Kj*t z?%01id}?(+&cAw2#lQn&Z0pN#A+$+-ICQwW?ow zho(puKlLlW-5>h;>A}>i^2y7%*QXA%e>Bz)t3JN))AjrMa_iqGshpe7^BK-pmu`MN zSM6`_kA2Of=70Z^j{8!@@`={tT%l88_K(K&M)?(QRL6C}O3&jv_qXS}?|;clr&jfy z2G4Kj1z*hXidHNOY1)%yh+cyO~_a$|6f8&p0>Ma_Z zvuI!PM|JoOQ%79wZ(m=2;)MrIRDPfI?AB==_9Z>&e$kh=9(g*6_)uNk!%=?nGVaY$ zcU@3Ekzf2Y&;ESF=_gGs>W4q4Dd6c(UHyDjw;p*qiTF@m+`~~m9_;B>b=L(ep6lkP zxwB(q>2Kco!l_k#{oR`q-spK^zP=vfZz-CyQk~I$KErBW<+s=0pZ)NIr&fM%cU)7V ztDnC0ExNzy!PKJ*)f0R5ia**{@vIx>9I^e$PkQ`*#9MZqsOksr)EM?ZK0W6PbU*OK zo~Px#p2_v9>r;p3LV5G`t0%gCyzG}yL+2J&;BqM>bD={^+EH+5P!9CrTV=- z*8!__^MjvsJ>D<1gdvoyg^>9>=ZiVJl^H7L|*ZBO{{od6W&LRBN_dJSc9rDh1{mDx`^~`5X&%CT>Oc!Q-G&T?F zvmUYQPrrTs;EOJ53hFmKwsq2Vzv#i<>l6LflQA8rE*?etF#G7jtdGX}p}w5!d6A#$ z{Cea|pEb39M>prlC+fo&C#vgL7gspS(_itcyuL*1h3cu}am`P4U+TPMfQr*!$*8{8d@`a!DoxfW@Z@qGEKF??V%~v-^_Fww#>z%Fdk8NJ{ zeB?LW?C|`*D!+aIspFjHr@G$< z-1_%Pnsa>NV+AWAw+2z%)k5~E+o`fa{* z#M3XGM0}_&?%^mO5B7AcI-g<1vk&=cuJ3>EeBkS*R`oT9HwIn&)Thf|JaPI>52jv~ zPoCI3D4$3dW_>i)534?Pep;WuKmW?}j-Oh6KkM`(8zcRq&rkbaOjn%!o=2>QUiM{P zJrOVUGPa(~>xWqvSD5P&JHPoU@BKV~_1`X;M!Fxq|81Qx*Tp||y4II@u{u3{Fza}U z_?gcb4`w~%YQ8+oKC$cMr@8ju9Zxx6qN?k|6ZhsM&s;i*si#h76nlN@a8x&W)(h1u zU4E*2zI^q2+XtSg@AKrgbMbnw2l;BgbH@2jClMd2i$}57rw&JTlgDS6b+PN`r@42U z5&mN11EznepZJ=_z|&6;I#2K*{{O9hyG}k-$WMH~KYyQd=U<3V&_Pr4rOpZMU~sa1XdgByd7`1Hm0FP?RX-4A?uywsD&SMQ;| zil+`Mo_sZ5Kg3V;xaTK5`}+xh_?)Rl{nE>u0$ukjb@|Chb$#ZVCojf_>f#EsKjZA9 ziwBd}i|N3MZy)m0Jm0VS+C5)1wW_~=_J?-y_?G$}AN;lseLYM){LEMJC_i}_SNfTE zeK6}{e(=*g=kkwVyw^lk{~zbK3SE49u=3meQP)oo(t%Z8J#mze2UBM>)(?Aq{B-^H z{T^puGPSDH!K-5VM19cu#EIFzTCq8!dOg1DfR(O&%ul*LANikmZGV5q*Zaq|U!S`9 z=ZW~KZ~vN)2jweFz151%TU4*tr|$YHeRY0{J>Pxyy?als-d{)uPi&sJLVZ0NrpNsqXpmb6eP9i>3 z7ms4EPaTfxCXdfB>tglX-~4#ka~?BM)%96l<+sNTf3r`2h4>Ip+`~~m9_;B>bw0w1 z#}9s*>-$xQJhr_cQKy3^?#;o|hY#hUyuQ^!`ituI`m7iB^!Z8G{yybyKRmUnZ@hm~ z!gC&_{^tX8Q)2VQjr@!J?d3}l2 z3)L$feya2PZLhu8)arh0eLcv@dE_&kuWpX`%1?efPxgE2+KH@w>B}1f&v~?2vHgoK z9(4XV-{liiPd)QhJj%y|m0mB;=bjF~_=(Stt1h|UG)n!Nx3tN4`g1+}c3!Jnm+yy( z=|TL}#+B;#`dkOB*3A!o(&g9Px4m#`b>7gy6Zhue>FZ(Y;nS&bl&8PqS9yJj)(h2B zN6b&L_iG;e>GqqL?#KDF1A(r6N*}sk?kC*}>8aC!RbCy^5%(~4dvT>_F06R+{4~$+ zU%vM)YbT02K3)~eC+Zu;>|beY-lF}ZeSC)19Cd!ucYfdd{EMbm^%L92q1^oWv!4G_ z-?^Uq+dO^dC8nNw=11eIUmlL?CC_!itc%Uzr`YS0gRcIWiK>3xn&u!~e0tD1qmIvK z_cQBwbYPWNPaNgr!PFUz^}C*`&vo+?-}!s-liP;=mc&d5kFI&*3h{E^(}Q?)S#rTQI_kAONT94N!cmJUirWSSmcvUQ)sBaXre>A2) z%CC5%Iz6B1_s;L%{KcP`sQmu)M_MO+j{|y8-u}iHJFi@ix|q&Nk={!CtpoOS%!5^* zJU_*Lzu`Xb*lQZ4?)z0eUH6f{)=MAKNyJ}mG-pxWUY|Ow^!dS0J~)3ra{BY8RzKgo z>ZqoOXP>5i<#)awv_5(;^;RohseZ4|b--%9eB~!y`4>LzX;Z8FK?hIVn}esXhpC58 zXHl&B7tL35try~@ezm_nUw-CMFP>U^ey6T^iFl)!{VR>lTeN?)kI%3-hoAIkAFICf zzu(jDi2Z+?eVP(o{M7gOa6i@Y^;d`w^@$fn{ZM}|u6Xh=uR}hOAN(}m`@cWG?N3fE z?uULnv3cSO_4RO6uQ#WvkLInj$G2Ws@jZX>lfL&uy!)#grdIVU_isvc@l(HgzQosG zVd||`Y~G@Jy*_nV>AQY@iraU+mzG}n#x+x`{r~RAw?_nB{M1js@!Vf^I*F;LKl7up zeyDHJSYKjq{rIk*pXPg=_kwRee4@G^pZSy4iKm|)OfgOub%A2jZt5 zU-+r+``<5n+RLX_^?e@Gb}nAEzwKZCS_i)UPbblOp}Ke!dwuF~R5y994`y9#pYhXN zeqVd!k55!T-~7@MO_8pCdXS&|rd#<<504I17gwksRy=vSP<~O=537Frke}A+_qn(J zeb>sl`8=QDe06hVf9C#{Kkj}fO=NYQ?u)oL2Tvc=Cx-I+RtxDbs@LnYURdeV;iuT| zlYIDd>!()r*Uk*%o|Js!B z7WLbC?(st>G1se}`6?dew~#Y zdHh=o7oF4NyAD{bn_v8->-@gMF2_%;>bt+aDbe+Fv(&Hr&OY2y zpGZIb_VxS;uYBOtqQ1|6(CZWH%=P!;UY|Pb=~Z<;!-{AB@zY%2kKOaH=HJgo2T$Ca zgQu^DsfSOe!cm@nd?>#tS}&~n)zfd^S9$W6Uo^FTkHbDbXE$zND+qeagHz*Q+n{y|~w>4tsi4T|caN zp11hvdhO4xpSRwe<4ffupJB!0cfS6%@4vL>l~b$x@wT<|^X=2rx9{^ju^xHXk(hew zneWBDK6TjBtLm-`Ry<$t@Y7t+mw)h&?Z8&w>yb@~E`I8}k9<|f*Iyw%)F)mP^+WxO z#`+TTIh%j9JaKOhp1vNY9zLB4M|t`yewEjk zXuVK9b-d2zr@GgJSAA&y^9rv2#m2yMU+BYX|NeXLZ|kvMF_ee$iz1(UI_i0zxDOvV zw*9>$e}Bq4^oh+ASE#RtsfSvaS_<=uz-zM_4esr#|hl}!ncZ>g`JzxdW;u6$zZ zsb{_y_xjXfPp_)GE?DvS!cTMWF>Clge|GNlFZW~g^9S7rdXWE~kL43{z3Q3o#l1dt z*wd@(eD3MEetzQndGSSWIC&c3e&qYv?9*I_^O-)~>h(!)z3Mrqip_%+k1niuy}W*i zpD{o9X?^zRJ&!zjYE|FvcN-&l^r6QMefi37dJrF0dG#Jv{qnG`@oy62z?by6Zw^z?TeAg>Zo_)wqe14yO&BarT`*G4GO@SVM z>hhDn>iY72$U}Ts<<%2M`FO4ard}_m11rAQ3H&rq-}9a~|9+qA9@3QX+?UjMAL*#; zlNTrAuN3L6v|rz-PF|1xM6WN>Z?8{I`q4H~{gdt2dAfg3zLbqbbr%>saNG!%F{;|$}ft1hE+db_-U^Er>?zVYE{p# zFRSyrI#2koZeC)w-d>(gkJqa&s>5ek^;hQ!KTdc|`$;Ch^Yde?{Y{s@=Hsi&C#E0j zneWBDK6TjBtLl7)70>na(_H8Gkw>03wW|AlSGt~WQ(vAQA;Fd zH+dOX{qmzac>FZS=M`-I{bG)OcjP0i*!Elc?eYGCA82pzQK#d+hE7D?)(fk7eC4P0dH>0WzrX$dj`~jfHAVNyxsm$vc-G8tovUO%jO&QX4v=k>{}KH9$W`R@`lA-dK}U%%(us*VTg!PFB&ec}p7d5iX^u64so z&pzWP9ro_H%N?gy-yf%gC+^L`)7Qh)!>3c>C{KUIuk!j5trx1Nj>k7Y)%`uMAKCBe zQ>*&5vllLS`sqWD8~Yo76zM>H;!*7Nsl!p->ZvV*7iSuUt5_I&Xe^*QSJ5 z`JLx?&evZd{%WB)i|UQ`@flWg)cHx@*Yitneeu+){?+#OpYG2azE}3Q>vmmYnE8zH zddNpu@$F-Nn&-YE?hw!A*&-eVqE97n~>R^z~OUKExCEkbaL>>5lf($A@$w z9$)yWZh!y9udkn|ejf4Yw>AbJy$+!V-3L7DaQ>KU9-f#ER2NsM-@J+^PZ!EBisr$p zpD+BhUhl8k^M!{^t?tK~-)RiGJ-_MUSAMJG=X%vM--~;F>aeF*)%92F*XNw&C%&II zJog!APE_?v@6eQc`tGBBL$~KQUF(BYUcJIzpE|5`dU^e@;`4={eDQcc|IbdFTGc;u zSyRGuzfzxW`fWXYr<0g^>Y4Awy*_o=)2r&P3syY7@Y7u9_YQwP|NEEM9odxd_?G&f zhxx6p&$`5k_)uLuioHH{II5dG*8{UIR!_gZKKb&y_MWKfXRT?S*0)l>^_wq-)^}^+ zqH}tD*8!__d%oi*UC)G%8Ro&XSz{B*tizWR*EPOa)|e!nTLriQFzaHz@Y7t+ zmzV$i#S>Ni&)c6z?|yx$pTFkOht?;C__r1=I;Y2X9k91;y- z*W-r2>h!HkoQS_#NPkhi(LUD$t2y=|Kk4&(_b;9`wfg$=x;>gAUHz%=KBV9B&KvXS ziBnHK^Hn^`$Agt#FRvd~eEXZ9uGi-kTy|LdO&<4y4j%vHq5HsZ{6y=^Je}1>a~9R@ z^|=mM>GOr3bossg+V%wLew_a0rbHJ%^}SBS<2y80Ju&sFImzqA=HStV@`=4V-5!rF zKk)zfNc-8*S^E>&@lUygBb*(n8RKM5fI$*VK`;DJ;cb+vIcHC>H zfBE^(QLk?Wy7;NDPG1b&CvjrxRdbS8#rh#$FRpm2ef zD!=|VKhZj%x_D8n`ZJ&L=o~)7s?YN!Kh5*`-+y~XJ6Y6sZrlCl*Ms^~-+5twt6Pu# zZC)ZiR2Pq8uP^HvkIr#ju-C^=^Z2#(=fyqeTAk0Z>QYa?J>T8;PEVa$)px(^yngOq zbKI}2yPt{H0qJZhu2e7A*IS1PNhI`j`5~N3;T7 z<+uA~Kbx=5y2Oe2P+i=^QGW6=?#)qmJy1T8U;H%B_fKATaQlJ0`hIU}3Uu*P*Y`)# zZ+(lN-_@L)D>g@5Azg@+M`XxbH8#OtNr@KyqwdE zdwp5YxTmkqXDFX&pYhW?_u;Bb?l-llU-Ooxfam!(b*uBlddwBmgQ>S#@k;f3eXavm z>-Bi&C*Ag2u1ibX-}}U=Rej?Zni5`bf18hQp8J$_ylTB$nqR4YuP?8oTBm);Pr7~| z@QOWNJ+-P|b7@n;>-o)J^Q}uCokV=7E?yL?etEd4F23u6@`OY&`eCYXy2YY-z zLw$*r-|~Nc%0s3WexLNCO~K=$IxqCATQ9UOF+F+fRL^|Y@nF`)iIpy1>SbPkBAz^* z>im{Jb-TMyt^EGj?4Z%lC;Bk`meeDcI}p?o4;nDxoTp&rfrGJ%9BN{Ls{@{-F4NEMHSU&u_YX^|+ExOg;6? z_u^h()-&$utMeJkCszBLAHVsIwxPFS;7lOb)th539^dIC;^|Wtk7BP+9ggZI&w63j z#cbrKxxT*q^7TJ3QSI;jAKet`LVD17!B;%_o9E6N)FB;cUE&_%!-}8vUR>!|H>`M` z7x`%(pU!r}sT{}!v`(--qIdFs|B#*X`RL5T}qzgxN@nO}M*HQ62-|>^KpYMGBfj=>|s&Cl8 zG5Cm2U+g|4Pwal+%UgHqRe8J$voCq-FzZ!p-Rkp}Ke!dwuF~R5y993uax+4}O~K zJb2AEyG&H|Yum?%E&P0Gp7oe(ULyW#A^k=5M*CbBtmfEf{G{*vzF_UsrdIV6@7$Up$JvK6N;%n>;?ltc&@@PjmTw=$hu7`tDzCop|mSJ?QaaAIpo~ z4?2nXP+hzzR{ipDQC)mKL;1v>-`hO&K2r<7y?>A{|5BHqS$AFfGnP+1yv%2uyo^Wr zy}s13ZkYXI$WO8T{mkw5out**AJHb^Rr|YozSNiNl81RcRX%xDT=mPts=t?aJ+R_e zuTSjHt-mi-IX9o@Gn}t}XJr4n&%U1DQ(vViAVW()&c2^A{|)uxo&=%=k>{N zTyXHz>VCZBUz?IX&$p>Bk7pfX&&TG;i&Ia3=BpUL($iP*^g;Xz=|H^fcisF{_xmL8 z`;!+=t?K#rcdGN`d*$mL*W>yV)nV30V?M*GPo1CUdVO;J6+b<-s_(RCQ^KqK9)107 zK0R2icT4jt)$jGW4p^;Iou71_znA`L`-ZprmF?F{Zhl>Zm--$z&Ixt;(0nn(Uu|5e zey`7Uz-rxm;U`_^$>UzX!_?}0-+1rF!0Y*q2l;QV^{K;LZyS@$ z9`MtA&v(1sX8!%(r@o{ydiwh5L4G^m<>|p(XO&N0#`Iy<>B6j!#`>!{bntS2dwsHQ z&&N(w_25>k~u#)y9?T_xfB1tk&&3Nx$vyPu=*+sa5^@X8+CKFR1)5NzbINS z)StRyev0{h&S&)7MioD-e?~mVKqmcpY-{4*+uie-|!#x^POt_qj}dg%Eue+ z<0I6csGjGy=euJLXbkmjf2k>0NA-Hp{mT7qp63lZiMdYo%=hA6pE~U6Rdv?|@lsdJ zPjUOL%cZ4v-}%R&x}Y!_=$t$;-Icrw+4!Ga)-IX)eD%_w~J|R`p$P z*BJIGK7BF2@vK8kA736X^{PBxh1r)pb(r;xtvB=fp?dNwzkR*)>R;V$YUTIa);1+N z`su^;Ti$y3OD7Q@s*6Xl*QX9gb(811VAjR<89&XH|Fx6aPma|;`S8|>*Yg`M(Yj#P zt#7r_oJDneeR&;~zVnrzbiKZK#Z!J{YE|EO|E7eeKlQ8qt*?ivx23Upi|UN_@f%ii zJYVvYKEJSFhub1aDtoZ!mr@79PeQ!VidDQD?UkKsxFZK2J_BWlx)Z5aS{-Qdg zeSC)19Q!Q&mVeJ9+R37xKc9EMQeVFE+qzs&V(M*aOn*_G(LUD$dvo|n-}?vu>f-s| zm-7B9ylQ{DU+K5?$kR#8^{QvSibwf)u+r=0T^FqQe5w52@dq!NTGelSURxPmzR|aC z&$sIGV&{!KJ(zlU^03OQ!-^*l@q3tj>xNaoIzQ>~Vi7)@q4^OS`$LPEUMS*Q+J(}zJ1J3 zeD6p8*azA#xVj%V{XkQ~<6r9YQym}X`s88iRr%y)-0M?^**_Y)9$5A9g`ei~`{XCT zdTLew(r!%&kDsYe*W*WBUk~wD3(Z?pZ?uokus4UF^zSrlxZ^?X4|u6x_n^kW8}%EX z|DKQK6LY=lneWBDK6TjBtLl7)6^~#1G}rs9{-3A**wm{2cMobxc)qSl{oekzE=Uii z-fG1w)$jGW4p^<%e&Z)yeqH{mQ>Rw-U1o2Tz;nM+Uq5|yebD;E5P!9CrTV=-*8!__ zd%ol+UFY{3e|7%-v1^WQN_dsu&WrThdgSRO=6cmL--~;F>aeF*)m;~?c=loX?fZj& z{BQGL&-;7`x{$tgdtINt;#Ya~)T5huafR8JJaw4$Do(z2!>Uid@_WapoIg?2@vC|2 z73#AtaiV&qvuK|Ft<`6}u$p5X{1kg0{P@4L58zNg{h+3VSDhywH}h6;~zPk0QL-|B~P(5RL zNEgcM%e*?oOLTq7;|D+Kd%iqsP5T8`^#`?Yr!4$_d%Dp2#EI4k)y0cq)t~u{N9VX6 zSoPVb{4|f>OZRC1dc*7m$35~ zJ@=J69Y3|YA9V1 z)7Qh)!>3c>C{KUIuk!j5trx0SI{Z}ke$Bg{f56nLe!~?_5zl!F zdOg1D?D5RwCq5sqIx+s<(T3K6$3HsI*HP}LymjfLlZX%1#iQ8kQ-`Cv$#Y#W>tc`l zJimRvYUe*{j;ZI@^X>~h==@K=U6*x<6X`&8@hJBC)ZwUZ@>~zhx|m=5G}r!o%5Tko zp6T5VZi;mA=|Sg3`fXpU(@CUrYoU3g`aQlntaPjW?e*Z#?|bsp>VD+!H}Eg@-N*FX zdgR@&#MD#Id@t_xWj*7bzPjsz@`?5_Kh5)ex#z~crWWV>4YR+OLD&6CUH5^n_+sac zdFtZSQ_p-AkMfh3aiyPmeK6}{`;ecm*W>+%FT7}?x*tpJl*`)Z}g!1g$MDgIvyQZ<<%?f^{K;3 zr2AO6nMr%~#A-Mw|-d3@x0^1Q(RukN?=l%IUz*W*7h|NBy3dSr7J zukzdDhCV)YpT&uJ{}zquE!vm-Q61Lf#>Kffb)Fta!b=epvDCLw;JH_iNtyz3m4&>c2eu{T=55K7I4l`HLr? z=eN9mdipY-F+S7>Booiiz1(4)z1%pn(Om5KlP{QPOa*@Jh~~-)t~y#i>%YvUt#L$%X}}+zADaq z>h|h+9oa8-&hnG4{k`?~r8xE7anx@r>9_s4_t)qD{^hZ=jqkp=Uv!}Jx!T|Qty2u? z+*-KkoF3nGz-rz6NWbmxBX>G)YE|EM_oih2e>T6ZyYi#T_xkt@E1k-3->*9To6nhA z`Td3q+hn?4-=x0hVfy%Dj~~~eE>1o5%vbR!9}iY~y}W){@x6}Vr|WedeE4slIkl>f zemK{1n^Y=bp9K)T(}6dwcTD->-7NQorZ7b(xo# zdg_@UjjMinII5RC*8{UIcK-6yT<;(Jtrs6MQPm&x<<{vO!KVk^2m2dO?7Xo)dSW_I zpSXv~Hy`S+)~TL+`9$l6Suwd>jk^r~O`XHAJNe(G0#+jkyUbP`ifJ@bpksUvnBaHYERs(!xk z(>$*S_j={4rdIU>-r1DwYy8w#=P$G#=ZAiL_0-d!`6{LZE1tR5fiJH5GOrJ+XDrW8 zbL{&sZ`?3ZwsB)-4S4!1U3G|;*GUiJ>5~_aVy`dj8IR7fURd?{ieBpXg70uW$IPZawm@GZ7!Ei+ecA$AdlH zs_wdA#WRPWV((|W`pEWyn(7PS9N*&Uq3yF539U-;wT>vrp{=rA69)H z*Zg$-zMjA76T3~V>d${zQ{9aS!Rhicc30R{SbY2Ufi5Jn?+@g=>zSTKWB& zecFoXrk~bJS6yDLE>90yuR4^6ResTWPlp~%zI8*qM2~NN(&6{(e(oox7Ipo2;@%uQ zeLWo2qg$alRlS!V?YCZ7={jfmN#A+0_45aB**QL`@)^n}s;A%f_g25tu@)Yn7%pwb!5n={H!-e|w|!qgFa{^BQn?;rfsU5=lqUccO9ug1W0 z{?mibDf;+g&zIJx4(Y%uuf8bKOI%b3-*rIwM85FTeCPMqFKf=bAD3=yig-Q0@vH;i z>qYg%T&H^Gt9X=;2P?f^-u3l#ynf~ zXMWMx9DS*qamAB|bgOL}fBoFl>VB-h^Ss~sQ=gw%7pt3Ry)gCgGT)2!!Q{)ss-F3bts7=t z?7I1BuI~?SfA13~s`F;!7g{HtbAujqKAVrvZ_gW9$D^~_xKjOIUtUMGZjXC@(&g80 zeE$QdR`p{KZAy4Ozxj&~bA9qK*IVV2SH)GoJgoYAdDjCgzWvQlbG<&f@3|YMR`u=P z*p&Fp&(v4X`C`x0;)+k#y2MZ($|q)B4DmCr@_5j7h@pO1?QcKddFFQePE>yXNBzCg zYW=RGhgH8k?CDf>e#44qpYhXN_u=j*95uE6tHewQk8W=cp1vNY9zLBOj`EY2ac_>g z^+NeX&s+R7&(C*`e$s2E7WdhFA5Q=;qfk@_AEd{w8fzl!l8zPQ3so_>7mfa;@22UdN2;iq|? zFAsV6E2dWUEAQ8o@Z7J|_xh&tTfg;*Vd||`yi)yMpX-3tdilXmy3T`_o;?5l!8Nm= zr{Ot|QeQtFv>uNO`NY&y&wLe+@{^ZwrJs4%)$6bP=HvU$`>9FV{rJdLZIZhFoS${; zur7XAaq8t9dWrgac|5xK)}vk_UiM`^`&~Cb^?Sa&{_L}-7WJ$4XbN~+dY;fp%yp_~ zz8ClUvYv5IU!BiTKC$xK`TdvI&;PxnH~($xu&&B)y19Swo$quKbDip$@5Q}7b=cFZ z>U@S3k0180xp$p4{Pr)cpZ=x($(OVO9{*C`^DupN>(NIi5g)3H7saYy9xkej@4BFT zqB=j#-C;(2rr-LY`C>@t*1|>S^!Tm=R_nG8`AJv)<=<$Zr}QR? znGhb`-W)uA_~JzM>=Rc=r>bYZ;#c|X7h5lsPaXS@pX$!<>pyex)ariRxS=WGITz@| zYJXdgK01kXpt^VzdwuF~R5y993uZlIewyp^A@h$7KUDpAt zK7R4je8=_A+;*?l>VTLD;nD5Q!PD2n)WfGUioL$9XFNK`dSTUPpDoS&G>_kZ_U<#M zR{Q^e*>7^w^>usd=l;ewPoH_};?%41$;+5-=EZcOJgoBS&>SeAXxs9WzVrLzyEKQ? zue`n~(8aHG`KvDWxT2GY534!qJ=72J#ZW%6SFhHeeXg6I>h||V`=39J_WH#-@Wkec zE7XTCPE@C(F7Dwd9}o6)tGe|-ao=-p7etl|*Er;o7 zzUM7|^26)F?bo)y@2Y0QYaf93-)D)>?Abc#^N)^xeyhtv{Ta)ro;XqeXg+zpcvRoIVWn%I@sqy%*1tbr zIX9o@Gn}t(j_kiYznv!^eRE@|Z?~rPx-VYeY*uXl^7x>yt`AzD7~|{G{vaowNSnl&RJI zpo1qKoujXZqk867Xiin{@rUNTJeSV7h^~m+7O|9zJKdLD?hammzSC@y@CB~CaJv@0> z<*Rz~#ZX^j)-$$lnEhgX{1o41M%eoM#hjW)KFRVK$|w3hUi$6z$(oZ6o?6rop8aBv zdDZI^j}QCSTz$~`#1Q}1!bRuw_^t!?*3D14Ue7(~PcNBT{ry)uc;eAHcd}?t&-u$G-!1MU1bopu>V)v6yV(O`9z8ClU z)L~Drs=Gc|@#OhwuCFh@@S=?qMg3WyZi;mA=|K09zj*GCx;#Bd2UdCY#8EyTOr6nK zKdkzkzx;Il_TMM&djF}_{-=W{?#;o|hc8Z4&pz=e(yjPe&$#N#y!FDYi}}q@^W-o5 z#{C!V(Kj#(_6F;f6;!|1FL@LG(YkDeCJ6oXs^W8H=Np5gjf0P>&NuldNNNB zx^7tI)hq1vsl!UAmv>#T;(J{4(_Ft#a@()AS1Rf!eX%LwRen4F`D(t{eWsI$zfz?4 z@72#|Sk31PKk@keqIaD+Em{4|-`5y;?pI!ybBe#};zj*7kIpFe`qbg5Zu0mHvo7Wr zKh1T1@A#DV^LcfBc;en1Jbn1$M0I^ziu4!N8SS%PSk0lwPqD9e?)=fqrdIXq_ijpf z?pNwp`&*xNi4#*#J@ZvO%EyD1UN7%@V8!>m#!qv7|KymzJZ)-Kf9*Mq;oQbgeY(yM z>kvD~9y>{@ulfBp0Z+fVP`~Su z7we;wh!54pi(=I;4;R(N=QET~gIWZZ#{e$tHac* z^2zJPsUwEH{;KXeV8!z|=BK&*{vT`SKi_`iBbpN4_v-oWdaYBOs1CC}8uJL1M~uNRN%TQ{t9tNrcu$={ydJ}->l z+a1~z`Bv#WXYkDTcyK>m4_>9GPNzb8>Xn{4UA*ka>!E&#PY=3oe#-lNwjCZh|M_g! zf2Q3by7no3n0~vi%5Qq+(}C*ZMX~Cahl}ds>xc4*>iK>M_u;~Ge|l>1ez$98hUnuX zU4EtC@)gp9sV9bX#EHFn@~gPg$-H&LO3&*Pe$sKyyz7k1r&jx)4qg?@C+dUNCr-@% z)r!p-)$8$H2ds2GzWGVl`ThG>J$PzWzx^Ri2~U6OyN}iWw=Oz~skhRY?xOvR_PHKd z^?Tg&6OZ5TdBFVN%Q)dFZRL2C-_E1-+j`{bB&I*=nIDa-{>*1Qs_(jBuP^=f`uo`R z?F~Na$9}je(RJ>mzC2&m#qP7~qbJ6PRbD-jUgk5V3#)qOGd2%qUF`J*Kh1R>y!Ch6 z=W+OXn?5{oZw{Wm9;O~XogR+z@nBE4s#`C_OI_yyKh=Fc#2IgSz|`t~?6hZ7!qZP5 z=6Qk-txKFp2daxlvDcUNj7R6V9$5A9g`eg*Pj3G|Uo^F<`+IeCoe!z+@u5yP`!cVd zdU%3lFHyb1 z)TwwqU3L8#%bN$~6RY#v`!&!0(4kX{`=LMQW{j7}S9A4MnEkz&PLDTQAJyYCtn^(s zKk55|Qnor-lD#TxHG;dMeUY|Pb>GP8xUVp#g^pmDm_v7&P zaS;o@-^YCGvCqv*#E0tQ9**+yU{ANIyDnJqTsJ?>_4@b;pM1j9s(!=Eni5_8sh{V! zx<32fJb8$}rAR+@s(i)k@vR%Cj<`Bc{Jzwqe&mRW>ib!{-nUKWbFM>Ny*|-rF1~!O zS3S}CD@>h=*V9$kpRv4oP(G0_{G{joC%b>~tf|F$bKM?IkuKj-*Z$?By1e~w9z8KW zR2TPfl#d5{x>a32ta#21e!5=g_f7xn(y7(gL;rAGQ=*%Gnv-A8)6KrjtJ8s1UOjP? zk7pe)^?ETKh@X05ev0k;6V|uCH0ARnDxI8zmq@>dsfV9>8ISVxRs1TiFVT9Tdg|EU z{8Z=nq4%Bt`+%2T-jwLt$Mm6o`=72p=St?qbQVSWi}qFgQ9V3Z>B)Qj;d*_&^G6Rp zWTL3!<5jVIqP|hg{$5PC#~ZDW>hT#?`s)0o@9Ujwzc&B=!2|xFDdqm8PcQ58_PfWG zdH8tr)T_KYK9q;@iFA{fan&bZ>B!@`?)3XkGs4raXwR4G*Ua9?X+8SsL9ajfi$98V zR*L4Vv|rz-4qu`EM1Js-zVr9mueR5N+c0n@fJd)42TxxQQxBg`g`+(E_)vaPv|gyc z(&eYR^JM)a+aulmsC04;ULv0J!@NX1{p#WhM|t}3q5Ps~y|CBMPjh{}^Qe2ypWjE# z{$2(i-%{V>!{bL?pLK~7@u9kS6nlN@a8x&Wt_Nma?3hTu<-d03-V;@Q_u2Qqt*<)2 zE5EJBeV~(QeNbK8!%;pS?CDl@*99w{JU`9#`6rLQ*BKK<{mqv&241zl-LLc&U!6{( zbwPD;4@dcUu%}zq`3x(b>*lAqvzH-Dzw+xpIkl?O!4vo9;OXn(s2*KNSG-bORL6Q@ z)$jXF{KWI~v=_g)y)slE{rth|JfUx06|U5$UY~hAUeM;8=<|jY+_1S&r-+ywz zfAYU~&#>yR z{O04UwrS6JC(mv`@9*iFqd%W7#pdE$C*+5^7*_ewI$jT}IhnU^Sn2BHr`Y~JxBWP6 z$^O1_|E9z@eEMSj$y2v3F`j(t;mN})pY@8D`Cgp5V(W(TiR%0`&(9my-}ZG=i@JV1 z`&b^P-}3r;h!63_6^`=s<3ssHk&jS+>Uv!BQ~mCGx4*56PGahs!k`bBV)Woyiv^lMPqXo?ZY4K=QGrw=se&jedqUf zFKu7%*#CPS-xTONk1Ab{AG-RgJYMSQ%Y4S8JblT}7_W!=V8vJGr|Wf|{OBW|IkmbU zbnwJ<#1-o6;iz6UC-dfIz88=7Sud<~?K6JT=l9aKFPmD`Z+dG}@^fPR)OW7aSGS(* z7pvpL)R9lrZ(WJWrwg-AFY_7W!K!XO{1o&1{U11WYE|Fih^BE~&G z{n#_7R(_wgrYX_UPame=^7^3li6Q=K<4W~=eXavm>$b1>N!RPdH?G-pYE|EBx2A+w zohSO~o3F2j_*;tRtW;;TpU<$GXCLyDzWlBSx1T(y)4>z>=HTh;;iw*6NLRe2II3s8 zu$t?20YB;cdGS9T-%b|wU!A=l8E?_^#D357n;y*dR{7**-0M?^**_Zd8CHGj{503k z({A{+E2dWU!)Kqjsn7jNeUA_R(uMjH)l+Y^V)GW&>-DL_O5gtEr|b3gzst*~7WMr5QuwK>-~Lv| zx2_8DR~yY+RJYfs4l8}HFZjt1`*!#J)=aJ5k530r+?#`^uZO9JPp5~Yd_36Gt?Jeb zE1uVF{4|%}|8hv%q3(yzD@b1ISNq$3r*D1qVCq%*xtn_+$KKFF^#ZP?ixBsobKXw|W zZZ2LG%O~m^#q3`+HfPa3{Ly}XL;Z>L_$jvk4&3dhrxt%d(BF5Z>+wNXzx$Z=?8BFb zsaM7FiF7l+Xq-A3)3a_UpXhPVPkQp7eQ$fe<{Kqu0(kU#bMW-_F!k{1^l+44@v3|^ z*Lopd>NrpNsqX8)+n?Xw;A8*4Gk$()AJd0)=~+j{nOCQ?+PG5vUSD2EwQf9q()E0~ z;emTit?G2}#B{_J>g!?Z;a9ryqr8>!^+EHj7iL{-AM;bc@1NZB$df0k`gXtD76O&(^C;U2R?SWH^-#^x$>o!Nd zLVft+M0Gmq;tEH3`YV2w*OzF$P`%ROr@H&_`d7CT+Wx-i>y3d|o!`!BJShKv>-p_E z`JL$T&QE+lpa0$4wco!~zuW8^-uhtbx?fqRpNNnO>xR{Q`;DLU zydHe+^V@gA)&0H{-RkEJmEY#&{-$SswcgBEF}Tqtjjy!E)x>WTPJUEIS_J|67pR(03a z)A2flpZL!2cl_b}`(r;g`#~EXUvnMK3+J-BKA7v2$6sw+seZ4|b--%fmEZC=-F5!& zH(dAjHrf2jZ~gpEzWZT)>abexmgZNg-|KT7uv#ZSa)0yV#J%QU5AOBTP04)cQR+J{ z=;K?D`y`*3dg_@UjjMinII5RC*9EgK=4<-x`SPUQkDI9K8~1A~v%bo2`Se?C|I^{RaGGVb-M!|Wf8^+SCb zyKa8Ee))I4xBVnjeXmD0B|7dGeW;(`^7QprG5%_yIdD`LA69*?2Ua{f{1n@tPd%r- zfld8s7d9ojo)=TU@>`#MZl1h2^;R0wU9{giV6WdCSoQIhpLpIs`24>eNXM17-} z{VR>lTeN?)kI%50Q^{-ruw@;QhX!zYmD#T%ZG;Gk8$m{jffDNN2TirTV?TypC$! zp6~ccmmmLd=KTAs?)IyVL0^CB%jf<{zs;j3PCfO^S1}z}@#w;e*URgN6<aAA1QvF_^>wvxW@{_Kg?`-}1B+a>=kLs}JcfSAI>yvx^?ELq? zFFm9wxi5?EZ++G!PR#YHXTFL@`FOC>>*ZY!toZz@_V@q(KgUn4?#E5nHYK{A7gJx} z`R#hLPYm%=Pd)RaFVDj`dt=~v-b?*FzwyoU z^{0Gd>ZxbG7x((qVNb8B^BGn=e(}>>&zE0$`{Sopbvk(B-W)uAJsj1eTcJ5sy_X;D zw_e!OOQQi5XZe1D6rygGB#V~pD6;}KzPbcfv4J)2=gP(N#y!ik9|JqkRUN8RL~b#FzaHz@Y7u9>nA?AcA~l;`ybpG_ANd=aelpH z9s1m7e0jXolgC%@p}vZz4lAB~HD5o(Pvk2<={dh&{g3Sp5bD>zqbcBx?r(hSaX;h} zbDip$@5Q}7b=cFZ>aGj+c>FY%-%tIqBc@jM|15vs)%Cg_aiTiR`e@8&SoKx=+d1<; zH?$Yv{NDYLreuBkQ{Q>voKV+iUE)N1s4ni|C_i}__vWa(9w?vaam`Qj_;sK4?fKH* zYt)Y??#;o|hc8Z4&pz=e(yjPe&$#N#y!FDYi}}q@^L$?K5j(bDNOM2Ve_vzJ)lUz4 zJ%}go@!;`e9-bH|-Ixf$bESn0^?6Z6v??}vEQ6P`Cw)epb1G4S|D5AxG_ zl6-afL^@Di+`~~m9_;B>bw0w1C(loF?ayC)_yZ=2`r${n4nC&7etzP~<7Zy1Ze98k z)hkS$ir3Rs*PpSxc~Cx)ul%Iv=Q~#)`S7VleZzYigD!sR+V^}^mv=w((}Vc1%Bv@i z^6_BmjK=z5)#rJUpRV8Q>bJdi{{69AKhN}*o#ZpTW!BPC<+pu**#Y;TTHTMs|Dh?_ zx9$&pvFFj`iRt6Z=X%wvytx(XhsmQ0vtGriryo{*^8BRZK78(;8zzeSDGzQPboojL zIxqMrPhWo(<3l`g59#-KmF{RieSAn4;_-!_>U{kCudJV_>fRqq7t+%&FNXYgUdiLb z)Weg9RbCxdJb8%U!{m!$)$h9biD!Rryix1qif93V{5P!AMyhZg! z`|J}~&EX3_>C1oqXJ0h6svq!%riAD9M(XQNzpcmPhfZSZsb{{5NBMZL((C127sO9p z`R0=l|M~ia9!$L|pS&ur`sHEO-^=qER($)ApXT!8h8lYoc5GywC`tG2cFnGafSNu#fj>4 z)WwTp)t~u{N9R~CtoqdXX`Y`q9P#0!rdIc3r#+hzp8J*h)&ACJt~fFE)H6RCSN-yE zR4;k12WDMtAM?{(zt4TveI7bd)%D?tdvoyg^>9>=ZV%1t@v=VJXT30W#P%US>D#v- z|Na+GRQ1oa<7LV79;637-)7x<u?{luFvZlaboIiY3#b_WGtVE2eU37U4M0+`2B{T+<*S_Pagf8 zHo<(419PD~zwxar^XiZetn%s=_WH7(aiyDi*9EhlvHfeF*MpC{?V5?I?(@R%?9CH3Vizs-g8VCt<_yi)yMpX-3tdilXmy7uRN zc4%*i_4OwmJaKOho<4kWqI&j;7e)Hnm-&p7U&Yo7E1vV4pXTxVz`GnYwc7uy{qth; z607yDG*5rg{?R@@!)lJlJwNGt{r#Tf+jmmb-}czC*C$^0(1Xr<_gUULVXhZ1^Hp5w zB%dy<>Y2~jyh;b(dBRV8UtfOT)9*M@)xWe`>%`+9J;+b`^7?v+zglSCqI#o!{D!?b z{G@Mx|KwNN59HMk|Kp}a*FH{t_aXfjdt8}EPn>${nXlqeJ|3*}dU^e@;;Zx1_44EL z$31ImRe%2ErdIXSX9e@^*VOm=hQ7MK z9^!8)nzN|RqJ4aZRX;!YiRWDYzU}v%mg?_MeR7+|psU~Y==Zo$mlxBQrw3CHPd>4# zS9$&FP<|BkSvQnV9s7`K7-%N{?qxF7z0175YiozMI=S0Bvv$>ZNzxagc7-*v!h z-TdMwUBBP3_3!C6=XyS>!$tkJKVScl`Oh=;{u8?HPwIQzb0gP-R5dgoF=zRzQtQ+So%`FhYg@c*m&UF~nLFLwIcYv=sF;R(%Y{_#EU zXZnfn{yJ~;tHac*^2rmA^6{YlMNz-&>Gj*c`22X(wI@xj-;$UK;n6iuT%kUEaiTgM zb@3?n`qbg5Zt|=bW?ih#Pjk0fy7Tlig?r41J1w;@bgsSYeWuKpX2d5QcgP`2o0$I= z|Ixp8=ftIX>h3bbDP^t3fnEU6YI9yEZwsy+_1TM)4%RAyNYG< zTYl|{PuuKI+gH^-IqE&2q=i9xt~*nNsc1v+42sfbXv7yx*|fcjr}ezE|?w z%=W=Hv+p}C@0aEO;O6_-9v6omb>IodZ2Faw<@&du4`*NS?Dfn0cZa2R0&cUk{q(pv z|J=1_pSS*;^VXfRth%(c{C(YN1Ag27TF$rJx9^+f9lPDqnP;3mbuQ2Q_Q(73<8b*= zy?K3&e|9@QruE&@<9vD6@_(nFbHRD*PCw_YQ=W0gDQiz_XVG&0xI>OQ@JWX(|7&UK zkSDg^omu|(VaFYK@GQM_?9s=!Zz65VAA8{Ohb`Ba|2z2LEGv_f59p1&YlYEZ$3>o<=38a<{2}u|0`VMWX9J2+v>np2evw})q$-J3_9TT^FFiJ zZ=Q*7>Gk=(L$A+wm|nwta`r+dUss(zd*3L$?WXTHEK~FAh41n@{ekV3<^kXM_DlR; zeVxAfb=iOX>&9>Ydg_d`&tG@i|IglC2TOH^`+~;ZCAho0I|O$P?h@SHLvVLXf;&Nj zCpZLm2oizd?jiV`v)6O(xiwR_YNl%bm|Hdbk33ncSAXyC{nppLd-YDb+w}Nfzi#}G z3x)srb#0BZwJLu3pXbT@L$O>J)OQm8d%Z*odA-{uosz|9|}ilM&nb%@{j)lvVKl z&3|9d#1FY1@xD#S`!xU4(?ZDg#{ay|{`{l`}Y-3-}Ya@f@!)Lfhb^C1V<9XI@rddv;`h4&}ET#qpfYHQdUb+|9i_ z!V^5j(>%jJ`GC>><2|AO{E5kUOvdEQz&y;yVl2s0tjcPv#pZ0m*6hHJ?9Kt4#Wmc? z-*|<0_=pL<4*E;T+$_S{Y|W1B!tU(Fk(|n3xR=Lxif8zc|L`NDwh!{fV0>m^MrL6S z7GOcvVk5R@Cw6B~_TwN9<~WY$G|u4y9^@@P;!}nX{58-&X)|^DOkU88IOsXo<&%UE!dSqIi8ETlB>Cv z>$!!yxtE9diJ?ON+VmgSC5*=eOv+Tu#2hTe%B;ouY|4Qg$5q_HpShR&c$n9ClXv-m z@0mFG7`9O9nU6(Sise~}&Dn~5Igq0`mNPh$3%QuT@)U3I79aB!zvvpQmqbj@94y3= ztiUR4z~&snMO?+T{E7Q`h=+NKmwAz#3t;-UhKzV9K$u- z$j#iwUHqA6c#)TRn@{+P9~j=}E5u+5W@Z^yV{JBP3wGl-oWr#|z+ZWir+JxId7XE8 zkKuX*^InLR*_6%rA8%QN{B&kd_UE@8$r+r$4@>vIDy@dE(%Wh+?e1G}vyz z_Uyr-9M1Xtjw`v9+qsvAd5kCdh;R6j;g$t?BJm5RWJZ=|S=Qs%?7}%*%%8Z8+j)?` z@;6@QJAP)w)T*o3I5ta1`h9JD%ccp64ZA<`rJ!pS;B< z4EJM@Hx6sF89Q(>mvK2aa3_!R2Ji4bAMgo3G0eIkM+7EdDyC)*=3zlrV{LZjD30a= zuH+!xLM+X4ti)<;!44eG@tn`)T+J=q&OQ8<=XsGI z8GS>LKOHmkOO|78HeyS*VONglL@wqEZs)JOz-zq8=Zv*6$d`<1n2Uv2n+@55gE^Fw zIGdY!o;Uc6FZiC}HwAejGA7e92XnCmE3hhCuoFjdA}4VkH}WJe@GsuwTYhAi%|YJq zjKbKA!^F(RA}q!_9L%8{%W+)8E!@h3JjPQz%?rH9OT5fKc%Aq7lrQ+6>3$0O&c!?| zz)~#3@@&f19Kc~5&dHp^)m+1E+{+_8$~*jrZyCY&xWr*R#%DU#;8$$H9_-1!9Lyp7 zukVYQZhHn7ay2(`FOTsIpYR{PW6Z6={KsZI7GX)2;+HJTTCBrnY|F3No&7kOQ#h4# zxtRNTfCqV;CwZPxd|y{I#$z(3VQ%JS9kykA_TXTS;yixOpLmpK`G)WLi7~bZ^B$jH zFb#{a48LR*HeqYFWjhYxC{EyPF6LTp<6S=H3;xSYJA&K=Sc)~+oE_MSy*Y$aIF}2! zgsZrPKl2bT^D&?CC13MBL+=c7M`RR6V=|^@5#}it{2y|8*`EVAmg6{`Gq{CYxq~}- zmgktLcu+4fNAO#Y;%oe;B$%P(KWlF*!3ZBQr5GvoI^OF*|cG zCv!13tFjs!upyhVIa{$c+psOWvj@-e91E2U`Yp^NEXum9$L{RGu^h+gGlKDEFn+{v zA#Wso!A#7|GW?Ql*p>r1h?6;m&-sEWA_etQvMkH7CTnpl$8jPj@fYsq9`5A<9^^It z&KvxjH+hQ@BL{gSF*;)~6;m?#!AD^CVC4H(q9uC_&z$+{K@{ zhkJRPC-@hy^DW=8#j;@hmfZC?*#4P^c$i0djKA?R|KfGt<{gH95{w^)VHu8b8IM_* zmD!k`uLiNd&y$8=23x~#`$Y|dY}o1Yo# zSx_fBV=xgDGX+z!01I+DXK)2qauru|4cGD-f9Fl!V&v!MgZY@B1z3>PS%Y<1kFD66 zo!NyGIf>Ibg9mt!kNJe38R|uly8sKaJS(t0J8%IP@-5%7{L5h63arUm{EGG2nO(Ss zYxyJB@emL5Ht#UztDt@?#%3HQVNw=gLDpm~)@405U_*9fCywMOPUIvm;Zi>53%=$* zjQ%?4Ck7KRA*-_n>#`odVtuw?OZH?hPUIv`<`mB7cU-`Q+`*mP!@c~CmwB6a_>hkn z@t>goNKC+lEX2Yr!IG@WTI|S9?9Lt>$U&USX`IPfT)~w*z=KTjCdi+XX_%Jzn4h&- zhuztOgE@rr`5nLK4_w2w+`_Hg#h-bW=Xj03^Dgf(;=j%h<1!x8FfH>iFAK0BtFs21 zuqlW08?NGN?%`e@;$fcW1zzD*e&i>HeH-)_j^P=B`Iw(&_$3>$G262PCvp-Oa3Oba zCr|PePxB0K@)nET2p{tLxH!wQ9J{g`M{^9Pa|V}jIahEcf8;uDSuP29nq+{3**%`*&tE65*#Q5lW7n43jdl$BY9 zt=O9F*?}WDijz5ohj^I3@iJfX6|>w9@@Hi$wq{3m;@D;pLLOh^IGz)@fD5^btNA0> zaSONd5D)VtPw^rz@i$)PAN-Rkng@APvL0UNSE2XG`uaT~YuHt+C0AMi1sFj2KI zCTz<79KdB<&L6ps>$!n@xR=Lyg132xkNJd2+64V1W$$beLmuCK*q6ik4M%X|#NhZz zoXo@gl}C7jCwYpuc$;_llCSuhF(w6dV=@+FWR4V)KPF=_6EialvoagAvn5-xHM_7Y zyRm%bpl$_LWY*rn{%p+7f-J!%v`$dp59K$mjqcaBcvj7XSG|TWyR$xU|Vjb3HJvL_xwqz@| zW*c^5XLjMY9LZ6f%Xys71zgBQe8%T|!7vGec?-*MOu&Rp#2+sN$FJjhhPxQ-56=iJ z$xZQRHGJixL&ql7#!m1B8UV2}IJLT>NLUhK_29LPZ&%pn}g zVSLMXe9sR|`5>s9im91~XLy$9c%G3S2KSH5D2&QzOu&Rp#KcU+)J(&)Ovm*6iuKum z4LM{-jF9;p%3&PNt=z`#+`*lE$VYt4CycW*s1uj*7@uD-0q1c(zvK6O#K(NXr+mie ze8C30f*cLmh>h8cz1fF-d53p-kN5e21<%C_nb$%r%p$DM25iViT+1K1j_bLTyZAGI z;co6>g!4h3h>XO@jKWk*%`{BQKJ3eW?9Txl$i-a3rCi4K+`x_8#Phtsi@d}qe9C8h z&KG>i_x!+*{KU@;bs?Cq=4`>1Y{k}W!zrA~X`If5T*Sp(!lf+xF?R4g$nvbfitNt; z9LPbuQ8P}+I1l)m;cEr^BQXl&@>uD(A@{q<2Mkpv*dLWCn32UcAY|XA5z@ePL$(+JDT*CGIl}CAjm-vif$_65@;J})DPJ>Gxgc*MCT3D*WiA$AIo4xic4Bu9<8Y4P zD30R{F6Bxd!&g48U;xew`W^U&%JjPS}oe%jhKQTeY zpx>lS&fLt;a;(B?tjXbAz#Tl#E4<2Ee9W+wf*j!(nemy3shOS`n1y*+kcIgr>$4ks zax`ai75DKV5Az6*@ht!1J4UQ*ewl(fSeiB2k{#KJo!OP$IgB$ohu?DnmvB9|b3c#p z1TXR`ukit2GiH^bzhq3w49vy~tjV_Q#33BXQJls#+{xoS$M98y98nm9@tBa=S(2q$ zhb`HYBRHN@Igj6SA-C`dPw^7(@eyD09X~T-wVI%orPIhgvD8g zbo5MJslR2N? z@n;_AIo{wCzGdjIf}BwqgYlW1rC6EOS&NO>h66a9lR2GpxQd&&nHPD5fASgsVYvE1 z{_u>LO z3nz0Uf9D-O;#-EYq2r=avUdeA(wD1xAG`2@*X2>4Duvo8fIm7=4T-m zVL3KqE4JoPPUJMs;tyQGE!@U4JkL9P&QP0z{Gl0#iI|IdS(265h>h8aLpYvuxRINA zm~Z%jNjC?%>aYRZvMVR=h##^pr*J(ta0|C`ANTVu-!b&gV7xGl%qUF2giOJdOvThp z$Mnp^%&g0L9Lymc$x)on8C=7)JjBC1&J+BL*ZGvs_?#~oc~>wWQJ9IDnS(i5ilteV z)!2?-b2z`@OwQtL&f)j`fs449+qj*-@iOo79-s0VWB(lV8;3<$l$BY9HCc;wS&#iW zfFn7IQ#p+*xRSr}2ru#yf8%97<`ahg#d>CBMqzw@!4yo%bWG1&%*|pf&ay1W>a4-8 z?8ct##lGyvp&Z67+{(MW$NPN1$9%#!{Fe!K2lJDVshFCzS%-C5j}6$6o!N!UxSSVx ziC1`)Z~2a&8EQ|EGbUp(Hsde_Q!*XXGXpcSJS(s+>+vhrXEQcuXLjL04&p>k;zn-b zX8y!2+{#`2nIHLy3HCaFOvTh}&kmf<8Jx>`T*9UNg}Zr_$9R&bc$#PUk)IfSUywHj zlQB6nFe59n5*x8GTd*bja{!0)8;;~CZsaEJ<9;6CLEhy(-sc0p`fk~K@shFC1n3q*qjkQ^at=O7VIgRVNffsp+ z*Z4cbj(7gqfDPG<&Dn-+*_}Pun|(N#L%4toxsUtV`caUl4cqcW=3GPUIv`X710y@kLmHRoReD*^xsyj3YRg z^Y|0@@F0)yEHCj9pYk=|GNSMCOv>a;$6U6w+~*nxdGoZoOX$8rLv@jL#&MO@GOe9jjPcRR=(kx7_> zX_z1g1wIFLg*lFPV?Yq_02^DxiyGOzMIBj2&Un1q>Fj7`~stvQXe zxSMBrn|Jt(?-}}TP(M7YuqMA^3-;w8j^}q=#Vx$V%M5ids27%z7>)T@h!xnALpY8z zIg9JKkz06}C-^6C@F5@Z72ohJKQhAoppU3b$Ry0o8tlp*?9Y*$#l5`D+q}a^e9p)Z zf*iS7h^1JA_1Tnd*q=iB?@)*zZ9MAItGgJ)nW@I*IXFld<5f_z3d7`isOLGLj<#7 z7H{xx-sCOD|31j~1rsqbvoSjxupwKqHG8rbd$SK0a3TNTpM1{`jQK;5FBWq#H|w$< zJF^SBvj_XK9|v*}M{*PwaWQ}4Zoc8a480)88-~#tgE^R!1z3>fS%FnqjkQ^a?f5lk za}ICtZ{FoSmR}g;t-vO1%7Gljp&Z5oJjlOzo%i{G5BZ4i`GKJqnLj3Da@J-Y)@418 z;9zR7PWM*5Qbq!SUa6B!3Jae1Z2muIC1B86L%=;qfDM z2_ya$72n6Jjqi$&8K|E=X}A|nKFdr zXv4N_$FJF*p)&`^hhbQTV|YejI;Lj^W@IK#;6zU1WS-_3p5-~7XW_eR?oc#F4rhj$t4 zVK827#$jBhW*VktI;Lk9W@R>JXAah6E!Jioj^_kUS8yd)aW&U)Eq~<^9_29}=Lw$VDZb@9zUK#i6SM6t3cS9^erkkW_+e+MrLPK)?q!?XD4=M5B6eT_Tvx^ z<6`dQ-@M0%41XrbACYO8joDd%g;|uvS%nSRjIG&&gE*Q~IG>BTl*@UUSNMjX7$-%x zkaZW2*_o61Sci={kl%1VS92XVb2kt27;p166Qm6CBxGVHV=8869yVhaj^$i_&$ZmZ zt=z$#+|P5o%)fYx5BQX!QU&?LFg}wp1#>Ys3$O~SvL0Kq1Bd?C{)pLw=MA=HJ9cAt z_Fzv=;Z)AyTyEty-s62f<8!{^YyQi(j29`$8=oneis_kwxtWJ~nUB?2osHRqt=WbH zIFOS#nKL+(o4A>0c$ODHqw)m(j^+%`WT!2`?VUM}<9UQfd6n1rh>w|fYcO6u zmS!1NVO92GZ}wqde!~%5%{AP_&D_du+|50Fz=tfkEy!DnKX3szaWfC{5U=wFU-30R z@FT-*59)()@40@#pZ0m4(!Mt?8zY<$_bpv{I`RC3NT*e z93jsK@tKs#n3wrjoF&+n?bv}Gd4eZ-iNEnWZ}1-P^8p|737_%9!26eMC8}l+B^Roa8vJk7Y25Yhr8?yVC*&HRa5xS>Zd{zh)%X8y!2%-%CNJ_mC$7jrWY%dtEwup%q5GLI|^#y`qqJkAq5 z$=~@0|KwlHxjYyz7jrWY^D-X?aWIE)D2H)4zu^de%aPp4UHqB9@D*S4AHLzge9L!y z&ky{_LwkdMkMSIz@-06x)V|>O@Qls)Ou^Jl&-^UFBCN(*Y|d6}%dgp;V>ywNxPiNP zlz;FB?=$NDpr2@r!Gz4m{H$6tSIFbI8tbqrJ8}T0awg|+9v5;Wx9|`C$-BJAhkVS} z3|}kA7nwBJGhSrd69Q{k53u4 zZqP>r#$LP1a&VHf3jaXD^Q7cK*WsyueGm%{z=)FUXsP*;s_dSc!Gngw6R4 zM{_QJ-~z7TPyB_4d4a$4HXrjj-!j=(LH?A?$?9ytHtfL8?8VXC!>fG4PYhK*s277# zqU8>G9*WA0%*0Bp%sQ;grfkOVjt1j>&!4!35BQKz_>}371^3Uu!YsnlEW;|S$~vse z)@;KL?8ph6$faDy>%75de9jO2$WQ#tP{)IQLNhs2unMcP6Fc)8j^MW($#ERdrM2>f zoX=%k%OAOc8+n!2c#F6Bf-f1Wb})Wu#$s&7V|;$W1WeBiEY1>~!@0c7EBu{*@NeGa z6F%h!eq^LNLH@|h$y{v6M(o4BoW|*#!@2x{3%HysxPcqFmD_lWw^^xfkhd}$vJsoI z88>k=w{jZ~@(?fc3jg67Caf3KPsA@-mUUQ{E!m3S@q2FNHlE^XUgj01|0<}Tfu&i7 zt=Wd1*qPnfoxRwbLpYS9IGW=)o^v>tq3Q?uLNh6oF*#E(B~$TBmSrv0Wp)MIG$5Dm23GUuksp0 zHI$F(nSsSwg5B7i!#JGZa0Gwg0&eFH9^p~`!#8}#_l(jg$QzZh7@NtNf(2QK#aV(K z*pdC%p946MgE*LDIF_5Zng9NoKjinzTSoaS*pAA0jL-Zmz~U^yI;_ivY{akGo?Y0L zgE*LzIGOV}pFeN`V_XmN$7DRlXHMo~VHRN(R%JbY#nx=Y8Jx-NF$#ptXATx*A%4lS z?8CmCz=>STAGx0gc#wy9ffxCRkC{GZkS_xZvJk7WI$N?8XK^-HauqjlBX@EakMTG| z#R~F-W^$%rPUd21mSHD$=5^lSJ>KUdKISKWW{lWDo|sI?M9j=A%+CV+l4aSPE!cq_ z*@>Omi@iC7Lpg?Hxp{h!_a|=QhN=Za*5O8O;%~gnEBu3h@-IfI7K{^>(U_WPn3e@t zkcC*0l~|c$IF{o$iIX{nDe@Hx=_@5uu`r9UC|j^4Td^I#W_$KzFZSkI{>XLQ!mZrK z?cBkgJkJZf$Sb_cYrMz%e87i%#K#PkKbW7;48s^xgZ(iXi_hK$`=9d#&s8|&ymw(&j^rqg<~+{lcU;1yT*e2DiiC{+ zkdK(7aj-upb1^sbFfR+T5DT+Ad$1?x^E-ae)m+21{E_Rpo@x~#|19K*33$MKxNS)9!|T**~j&Ar^m{k*}yd6S)WPZ-@EYI;gAM**H z@)@7=13&T;KQq+mpk4tMWFZ!25f6?zy0Y@*Usv13&T;KQq+oV7}_{ zE7oTNHe@3QWilpb z3Z`T#PUCdW;7rcqY|i0a&f|Q3$IL$lIkPY;voSk!Feh^{H}fzr$8apiaXcq*A}4V& zr*JB#aXL5iCvM?ZZsUV>L4ObVh>!V%Px*|``GPO`im#b}eK39j7Gxn7W)T);F&1YD zmSibbVO3URb=F`_)?#heVO`eaS3Jp6Jk2va%j6q^K2tCyQ!zEuFfG$DJu@&5^D-av zvj7XS5X-SVE3hIfu`;W$Dyy+Nd$BkBurK?uKL>Ci2XQcma2ls`24`{>XLAncavtaN zJATg}xPS|}h>N*|OSz28xq_E@g;#lvzw;0N$-j7=H~2Si@)m0js~GZou`eg_M{b=L z+54g zVKDw%Mo1KFM`R>M<`+!BqJx9ui?JNbvpyTJHQTTgJ98L^b1|226E|}^cW^iNa6b?5 zFn{F}K4r8aK|j%1gEd)?U$G0jveVq){+&68b2*RmxsL0(m;3nPcre~ahB^^!hh_}M zWS%F%?RlAxrCEkwa%IlSA&-w$T+MY{&kelHE4<2o_=f+o(=Sy*#_!B7?8=cG#nBwY zZQRZs+{t~rgYozC01xsI5A#h^hAm_fNyLOvm)h!2B%0 zf-J?kckmZpW{hk>uIB8>ZXC+- zoXL4y!5{fEf8hyU8+@k=dAsrCFIR*otk~kv-U#LphQgxS2b- zi+gyOPx*|``I_&TG)K^1Hs)j{HfA?|%Q2kD*__WMT+LlP$n(6$-}#E~_>p09203Ff zHsdo9lQ0?6GXpa*JBzX;OS1}Vvl-j5J-e|xdvFxT@W`wn|52XdSzhK9zTitnm>t|d zA|o*}qcAF?F*;MWuNLzBmWugVfF)Uq_4pNsaX1h15I^xVzvvLuOTYmf$Vr^cAGm;v zxrCdzna}u~@A#fkItKNlG8&^Z24gZ7W3v`(vniXg7khIEhw@vFbFe6ju_Q~e5-YO~>#_%XasnrE24`|BxAAxW!54hV zcMF65-tz-L@)JKZ?V{lLbWG0-%+4Gv$U>};t5(S4r6Sw19s95^`>{WVaX7!_NRHuH zuH$-c;$|MR$xUoXA8DvJ1*x6Zsj%}uGfV;Nk>MDgkrTJ#y?7^NK!l9hR z$(+U6+{$e{%wKtgM|p;4`GPO`iJzIgNH8BMn3AcOlet)urP!A3_$^0r9LIAG=W-p_ z^B9lw7H{(jpE69*Ab(g!V|2z~OeSSAre!)-V|6xX3wB~>c4K#r;aJY&e6HpiuH$-M z<`v%IUEbq;KH*cwD;CU0d=BGqj^lXF<9u%C4xZv^e&T1QEgsZM$Mnp=jLgK6EXC3+ z!#SMGBRtB#d6V~epAYzupZJ-vN(6ahGdpuIKMSxZi?JaaaS#V{9LIAW=W{1_aWD7r z13xlp$slhswqz@|W*hclUoPhg9^-Ld<`rJ&4Mr#x)Q`v*jLD=-#+=N>axBjZtjMNp z#@1}Z4(!Nr9M4Id%xRp?^V__CyQ5IuKmSStRVOzH2 z5Dw+H9LY(X%vqexo!rG!Jk1Nd$jiLK2Ykp^e9Z`DtOrJ8bjD(A#$jB>V|*rN5@uyK zR$*24V}E|f?|Fnr8Rp9%Z&*fPM5bp3W@R>JXATx;36^FVHe@6AVs8%NP)^}g&f;wT zzy&nkBRg>_r*S&h@<*=YcJAO#{?0%6Cm-?=AM-h1 z@Fhb%5Aug*7^Y%sreRv9V|r$07G`B0=4Cz>Wib|K6EXJ)&kfwjUHqB9@K+w;QU1<9_$N#J9^@>^QtZUe?83!d!lm5E zP29|F+|C{RK4y!M`T2nhxR5KjlB>9yM|hOSc$_DAlBbv`R!}!FlQ1dkvL3%;eKuf2 zHe&u9twZ`Lz=ABq!Ysm~EXHyy&kC%_cKn*{*@3s?v=6C&hj)38kNJd8`HcVY4gcj^ zCW{-4pPVU}lBt+!W9N_@nVE%InT^?*gE?7-U$QLAu{a4PIg7J7hjTfP&-sEc`HHXk58v=#zU4c< z=Lde|Cl>fAn74u~#KJ7XqAbSZJkAq5$x}SdGd#<4{EOFlgMafTZ}B$o@GkH1J|A#a z-7X>P_RtB5RT_0PUTF_=Vor<37+Ixp5q1HSDt%H0?nTvT@fTdZM zRalkv*pi*ujlI~9{W*klIG^8fA(!$;Zsuv849EW+~KG&;EdW;Q+)Y&YRJj^{eA=T7e85gz3!p5`+?XY#{Ay%fyK zY|PC(tihVB$FJCr{kiyjF#Zy*zF6RpV$-j7=H~2Si@(%Cv0Uz>K-=N>ye8HED(=WI^ zF27&`rerD>X9;#-NB+PC+{$g-$z43cqddWrJjK(z%q#qzfA9&P@&i9ILjR!Oh)l|4 z%*t#m$x^Jw>i=c@ZXxTj25WLi@?if^4&!h>=L^2%E52sYaot14FUI05!D*b%8Jx*k zoXt6$%Xys7Sw98!zT;A^=XUPkejeaq9_I<3-TY~z@nT>f_lGRv`P1u>; z*_(Yhl;3g`M{^RVaUHjEACK}hpYvb7W!SAjKT#Q-ahZTAnVp4Ln&nxEm05$mIhHH9 zk=wb4hj^G*d5aJDmSMI9eS~9F#$3(Gi6#3W3`jI7AM9L^$OPX7xic^ub22}RvJ@+@DOmaXHM24|^ROTX@*9rgIDXFsT+F51#+^LQzxgji{Sf2~&&Z6zs7%ES zEWrL8z=>SUmE6dmd5|Y~iMM%|&lqMwkTWdfGX*m;D~s|=HeefeWnWI^G|uBnZsH!E z<3(QQ-+as$47)JMABi!Ul*yQ#vqd0~WxSBt56A$n)pYjz$Eei67 zVl# z@*wXQEW`?|${MW2cKn(h*^MJPp3}IDE4hYyc$8;(h1YnGVO9jWBQiSUF(Gra7^|`d z8?!6>@Ed;1v7E*QT*~EK%`MD!BFLGCC0U052XprxbxBp73%^gor9~8kM$mY9yWh9F zK@k*C5fHl(TOvjYwh>7bS`k}C@zO+$7c@~cN)nQwlBmPT@q$W>7gRK!h&d98SEBI* zFP|ZX0Kpqk6Ge@F-$*^bopENe#;&Tp`y0FedEPl|%{iYn=d4w$YS-TV(BKKdUksiY z{FUG-!6ycv7kpXp!r<=*-yXa&_=Vs<2Y(RU^)ns6y@UG&?;QMv;5~zzgU1Dr51tr& zc<@QVrv;xLe0lJ9gRco*9DGyoox#h3?+$(-_~GEkf}aY0E%^1|w}ZC^cl&HN-=*L^ zf(Hi=4gPSC=lsXMf0x^KxGs2j@TlPC;KPEC3_d1!dT?9t8Np`-cLdK3J}>zE;CaE9 z1TP3)6#S#$CBgq0e0%Wn;OB$a1^+Slz2J|7KMn4-XE*QNgZl?pg7*m?9(+jfF~MI8 zJ~8;z;0uEn1V0q~%i!mNUkTn2yearE!Gmts@jE(rOz;E zKX#4GKQx}dM}^n^8IPBFMcw3Oym63yJuW^zkRGqC;bp%3@GA~I8MgH7A8%lA_aHv7 z@_Jl&tz^77{BlcAhAln&?$?KmUbTuIA6R)kFTA>6mmc5u{~5XaOZ+Y6K;L}5CcN$+ zd-La)TY55V>CHdwZuqLcyej~{5{i~Y3tHLmgOySFiDSM{#r2g7UL{bl_e%j*;2wJ}pNfAcj{dNOS3 z*>`U<8Qp)K_;h%6ze+v7bz+xLaec(BZ{L3@yzUf3^EY4q=*f`Y_2R%BV81p4F5T80 zP+$EH%|qXRCA^M}KXLf2@yXD5G8E@3ieTS-{rr+oN$k9dej-Vk1`{a0UJ z2|t0BUEh4&E4yYqjosayFHZKOANEzgD7+SJy?GqEY2~Y>XFqnI^TzO6 z2?25Vot4+v`^U?}YiF$!XPH>geDxF5clzyv;iX^4=1bq#{lwUP;pfBan2~ifzmk^8 zy83J7K;QNLE8#Wo>yUaqVI;5J&g=|l-LKX<%A<94=`LNIUBgpf8CT`IhF4uDj5A$* zM~3{8p*|k_9AG)T%vXGM5Qm-&TYC0m*RMmu%e=(lcUt^u`8t?`VBfw!BD@Zb-B4b} zm+-69vF<0#SD!%R&GUrtT0fE(KjPK(>yGhzRuW|#KCtq7N_gF4Bro~mcXRxR|CkKe z`oQi0JwA}Vc|I+?#^wvJ1Hz}~%YE$+Gx5H>o)=#8J|E40Kg7ekZ{MF6UW;}=^wRKZ z-EU`_lYJQ(>IeDO5Arqk^N#bwYhwaQ`)&dkeNi|0vgeN-y?%%0VV^9rwyW@!2m&|MA;&f7E@n`I`rS zpAN5C_xF1yzCOdtb$jUZ=+B1Nygwhpt9y$fr2jwYTgT6Z*SyyWyy||nFRyEMQoi)j zSYBTXFZ;UM_vTIipB3r;(x+9=&T|0A^|yX!U*(&_t93r|-{RHMv)hJHP#!CfD)0H^ z&fq?=6Ng`J>B+FAmzVPl4DyQyG+zGp3a_!xO{kgiZ=a!xf^#d8Qe=J3?Zyh&=SA7pgKXN;k*M~V6$m{U%+F73?*ZK6# z*L}n5=*)*7bue9eGHmJDkKM06Fud#o#Nl^ZeB+IS?4L>z?3=Gm;k6-l;_wUUOPI<1 zQ`NI`o;yyjf1Ni~-n^6QXkFcmP_S>lekr`{Gv&p+AbkllHD6a31O94$s{DxXs`s_* zxgE+&oUwg>a(EpS0^$$lWnGqc_Vzj6gQ3S8*6a4^;k9U=!)yz$`uyI!rq$8;WE?dA zvFpSc;WhTYV;V1gf$#45L;aw6@b}d4TD0eKPYW;i#pc(yj>da_R{Q>h47i~C8JNlE zZ1MzazMd6c{ECAYq*o6p?_}7r6CWS;RsN0e+8Br8^9$+4h5R~RpUdU;f7M4akiW{K z%C8Bp4Gmu6i%W(rz4&!qrN;xxqj|nFysV4ju%8y+c;g`Z*HQ#`58?wWuMdaUri>Sd z->93{LF0{s?4L*x?91!#!fR)(tDhgq>r3G^?{!t5oIaA*4dGSqJJf;OR=rw!_FFgz z_O0VLXUx|$UYCsIwSP|J`h3-VVJp6RmGF~wz1r1$v3FjQ4+yWZ&oOIU{95y|zpgy0 zd{}s`#GyF+ibGF^^wz&Ua)AAxM}Bt}D0)0#UB8YFuhw%De#FNIw)EnB%fMjYIzB4A z@-gpZ{ltgcR{WM8ulgL5UwMMY>#s9*5-;%|YGOfpd|>7EwD4-(ckt873%z~BmJ0%I(&Gc^Zx@B< z9PCbu{!j8j4Lv@seC-G?{={Kl&oz&_dZA8y-xyzef#T^;dSTOsf#*5 z`VwaHeHi_&AN0#EF}(gUon>7o@YiSL&xY5|daw3`G7IGD zJC!2Xw~pTkFXy-T=EW^N8MgH7uQf18k2kFO`nT{}j-5E{xuqw=mY#hcOBg=c$KnGk zuYV7(S@${oZVm&y#=ggQ+oyDn=AAF+-IR*OkY2f2gngK=Y8l1Hz?7|`XSD|c#VBe^w98{b)Aq8C|_gmhwdLT!Dhp}d;sbEec$KUDvC@nfBV;ya(V@7-C{I3P1TeAOo&_YSDee z(vzXQyN>Bo_TJ}SlZNlx_a6^0{>0~3e0nl$>Dg~JFi5X{P=3wx`tVwbojB~dr6?irUUv(xopnDH_tMz= zdlXACv>#9}b>NVm4C$?Z_S5>y`t`Tj=k(3jq2X2g%lTX5lObMYXkKSh1pD$jGQ19o zojCk*OHYO^J^O>p0DAL=@~bZ&G-Dn4!4ry~Z5MU#{@NT~^?rz74$HKuC-(yrMgCx~ zzZ|ERCs^}!TzIvvL;SROP0Lrc;}0M9RX#Di=KUN~{K~6u-=7p->yv1H_?3TpGHmJD zTYrqFmmgT`_~`Ijj-fd0`J*SpmY)6C&lOG$FZ)e#_?35he4zeV69r`NerD+X_A|oE z=hNczD?U9Lw)E`larII^NN=8JhnGI!hu>-O`7sWPGxmAmIpO8LbSN+5@qy-b7DbR= zo}hV{=gY!tDR$zp=a!xf`O%-Q-|WZU51k)gcOA)V8s9gVK+wKlo?y+_Z->{;dd{@9 z@RCP-VCD73@LIIb)7}(b^S-d52vj5e`{BlcA zhV;%u$o`iz@RYn**_YQx!)xsG9P0$0P#&I45$wzBFT?9zu`_@3<(8fdTYC1V8yKX= z8`e60bH;p4<2APLS3j+r$WfV(IOb)#^kmqoqxEx#fx*6Y+&{dQV<&!JUdB6boY(9> zAHN4YwEKfz{a~%*ox;oYO&s=+K4B&xUk5~EUn0)f_nGb%UW<1BxGua}-yfQmFL_*( zeAV^KdkWvojUa#OC*M_GA71h=4toWoCqsV8kp0;E+IxkUd5X`kIP_%L(z9P}V31yZ zpnf#ZL&9rk-9Osbq1ShvctChLpUmGp%#WUb*wVM`|8hT!K2E*9A56%#o>2ae146C?6*(^>G6OyU)#d#*w_u_ zWqb)g`Mt-h$^rd~53IbN8D3-et1VvgvVA14mxR}>zq_J8osW1O6XM>7=$o%shF5(~ z&adgV^3u|)%bpwr`||q38S^!*?|*G1uQ!C(*z<8(zP1%!>Y(4D{F>*R!zC5Yb;njMc z!_Tz5EF~{^sn_=Ck67FNfv@_>Yn4A7UW@kL&_}|{{hz#;Z>BprAIQ*nd4lZ6J~#Pz zc(v|_YJ4)p%kLgkUSoex;gjK2KSx&Y8lMdH2N~)k_YL&=m49eFf1eAlS+9@cLUlZi zBG{MLm&2>xcks)PdX+Gf?=|g_1GSF#i2n53bbrW;JjqX$|319NUWfQ?@qJ7=V7_=j zeEGXRyz2cBdv06#YU$bk0S7^Pd|>7EFX7et+=`!8eDf>eC+}xhJMr;hU*+$G*M>Oe z2OmgZszQ)t5AFOq}GQ8|}#bM7MJsGz2?8n}BToqnp*RNK8(T}}P`)PQ!u0zxEBEE6Z zIqIlN|Fzm^hLzVyonqWAy0_2qTqjQLW>gr9(XjlCZ_IlS&3kLtp&K=fqT(&PI} z1_tZ;)jA(fY49>0zgGN~zGYW=;V&Nf@!{2aj%m8B_$_^lm%hUT(&P2~@T$KjYJ6W_ z`0)>Yj^cdi+xM>wult1n|K{75mvNAN+*9{{F8B5ruEcWQynb=QsQUw7^?>rLjvorI zvFj>7CHyM!PA&)f*6}0ZHS7MXgrD4}jeWj)Wq8f|eG}`!=0dn{zCIRSt>@(WLEn`y z&Cg3)cKS%2^+A>YYR3L*@x^POGLgRd`bv1&SBk?gxAbI4@49cDV(B zeXcO}KJCWv+BXR>fAy|;Aw&HkPf)(R_jqw`P-}mQL(kvM;kA@Nc=0PfJsGz2;?&QP zou~Lf+;eR&x#F2rl>@1<=Bubs8tKLOET z-+bLSyvEkCl`r|O$NTa+dd50V>#woTO*V&@_Z-a6e3gNo3|o5k&oVGb?>vP1(L5hI zV;%V^;V18}9$XIet>eSOYu@__^WQ#_*XiN4G4|>r-`vuZVJlzks~x@iLG$46iQ!f6 zJJ^d~{ZG@2^G*YUee3w#Mt_+Hw=G`gH|@E}6}Rm?_2u>a@Nz#TzI;J?JR$v>DC%|l z)6oz8p3eE< z?rWc14)o>qiJe@>n?~~bYte4@^#`}~WZ2R>AM5eDPT+66 zd0ro0?hp8x7q9b7AjmI1u=4s=qmIUNi!T|r^y1g!ef$3V;Wh8;ka6HYwY@k7oHHWEfhg|d|>5uLU_%4pCd1MTr^>)U-5yJ*TchW zQ!Lfdyf~yM!cUp;RX8U>+Rt+_BtfL z;zGQ(Qw00+x-7iZUmSk9r6gY@bLYaK5Sud$!=wRq{HuNGc?^YyXt+8BFr z%!^xkGHmJDKgGZxy*$C1udBjq(LNvgtMKx@aPv1WNMEKu_;Wt>l_%>$l|LI^)*bQj z!GoR*>AjEc{9$kZ^}{?^rpE);I$j%IW7i4u#S@A%_Bqp+!)xsG3HibY;$&mTP* z^0QYIweOdsANs!B&%&$j@B8w?*FM}nj=kr;L;oMC-JaP^$Ua3Je&vOp4CRXq)n)AG zR(prnqTN3p5MGC7qVm!=U&hN5l&>==g8b@tXuSL%8eTi=e02YaUSD2EhS%8Vh12pi zcD;W|4hdhu6HHuUe1x8OiGv;dN~6)kPh+r6bmN>Wq$Hlra$@Koa<}d ze@%Oj&pIa_`GeuLX#0sj53hND{;2QoT@oz+@&k>RuaAV+ez8|CeGBPJm`NS0zGWvq zK6q65n((UkkH(2Nl$ZEpKkxWrc+GoVm9Meap=-nI;QycMh_8CI>iAFL<$gxJ*pICv z`z;&<>G6Wut z?}zRYUdx4-^H<)c@$%edb^Nf4`&B1w=nl~HuRp8&;2HD9ue?I>J&&n+JfME$?-AiO z>pCGW6sPVB@x=!kkJqEZYyUV@FZR>=o}Z=I*L~sk=!gEU;_2aKzgcvSb*~tjKfnCZlVMBG z{&WL_^!UKa>*Db8JX;+0+|rX_OE1pY_oFWhFYAmrv+`m;w!i*&c(vAvX?ZaZ;~-wI zq6o^P`ayc@&L_faW9-D?ms@%=VTm8lUECYk|&Oc}#{O$7W&S2p_hY7FNdN1$O^0kzF%xhnG zVtDBT@#Q<=n)hS!e#Z>7KC^#I^v}$Vq2DXsVL^7MMgEh$x!R?Q_kKcN>h-bjI`Qi8n)mZUye^TbU|(ME39tLa zPQUQWEj=0P4>DxGg(66=ez4Z@&%!z+F9%Uo)Q(*ANmTKhraky zc+LCys`$Bmn6RVA2UcER3$J;vU*a4#VbuN6x4&)(ul2Ea-kBG-^kmr5v)8xA)5{Yy z4|TjXyjstF`D?{*>BZN#{LteAE3bW@(^)LXP#pIB(UW0I&puzTG`#n9`;Ot|b1!ks zgF|{Uq?Z@_Uc8RVfT6$7v=Ux-N+5Cgh4lEqmcC_I*L(dW59EWwYr{_Br4HtSZ{<58tVD0;dhF9x-6YGumctUw6L-DJf`1r7|^49R0 zb)7IT_X&87{ePrR4KM!8k6-hrCqsI5G=KQE3>^AB)?W=T=eamTdEp7!kNy78+2Lir zLwU&yK2RTxeQvTNyz294eJBpTWH^o2;RXhsNBl$c;P2eS2> zUuylLcYfmoE3dtGbQZJT*YdkL1k7vf`-S_5*WvLeU*=(+^kmr5U*TZYk-m`jscrDt# z@I~R}Jkl@vz4kX58c&A$YwUBmmxkBDaVXBLyx4yT(ICC`0h)(?eR+5tF(a>~*jp#W z`9SnT|NoBP39m)lN53|_){e}VyiZ#vhJN4u_2E_5FMSZY&HFU{XdGnk^L%>OTYUx9 z-8?@SUiXWkIQ;TQPlheMc-svO_T_a&c-7|sv+`m;_P*l_;k9q(WB%rAy7Xiy@6JPr z*Sj)s=%l~2FwP^e3&7Gz`2QY3LFXQlnczr1jo|!M&_T{xL zylUTzH;tG1;p2S(ynZDPJZDm0cJf7rPYJKOpXkfWIJ{~dFAp!jhm03HyvTSxJG}Ob zz2mTNzI-15FY&j>!GSp;Jv^^zI;i$D8a z`QraEneW4M0mjR5yvXo%;gvK`)&ueQA*(CoXH69Q1lcV`z9lCFJv&(Wy*|9yix1;= z!j}yBRY&Ke_*ZA(MGx-&u&%PJeCRKoQyuYANBw}q^6G+(@ya{#YH;^>q!y94z6w|IRryjuGl@%bS`dc0~K zkI#V9bA#KrzdjRQ_edadV2w|P#?}5(ukU4`{a4?7eKWl3b6@dld@@u=_d)pK_08n5rItNOk^2T)&jEnZ9ErEiUgH9i^Q?>t=20eQJ9a=owRzs2jo@S1h~ zQpdZ6hrEp47akX0^|`P4$V-*!BBIETd z;iYdKhw@TodVbu0s-wPl-{Ja*FFU-*aC>;odY`7hoY&&l^&VgI#EXpAGs0_LeZQ1M z;`RB+mv8C*pcfx6GQ2pvHpCup7{nNv_^!)tBq@qvAL*^g9SDc`DRW~1Jh*9XJP^#dQ+mzO#^ul4uwakTfw z&Qss(_7&l^GGo46hiblVjNhU6j~@@O`nxM*^Hq6W5?=4l1Khs(`b2nH7x95{+dLoj zsXFcv19cqxT<-6}Yi<1D1E=va4lnhzP92;97e1-;r0>}|AIauhW25)&Aqp@=ul3$KC|_guIX?@p`n_^_o0czmw?5#dK6U-Vmz{BBxcgf>28(vzu_nB{FDy@R zTE2{vFMTR6ABuw)AKU#wFE6cp?H67fV=oSz#*2UFqxFma)Hpad1;&eiyvXeC5?EOpzs>IuJVtUd|6k=@_KN1Sr5g5L-Qr?c-8&t?s3R}D_>6t zuX*)-&DWd4>p8M)A>uO(KpP$03&RhMUdCHf5U{|k?m6!bRL;gZ|IiDPdH9i>{ zXPsD^1J1|Rb6@e*kqo~ZUiE!B@oIcBVBfNPFxTNLq8unF}&8t9v|43SF4V9i=!8u-g%No{#$iCC%ne?J^y=V z0{TneCE;Z~6bB~U)H=HELwRvuyD2Yd(2L*V_44pqwC9+wo-tqY zs(w&jZc4(3?k8RwUbF5eTIb`~>-Ouy>&}^%`NL`bRmT}GU-h`_6TSW->$^A2$crD> z$ND^{UbpdLhZh-M8eZyvH#Gk$SH1Tp^_O+x*bKZp7lfKey!glK{oytC{*gcV!b|+I z>-`79YeVKG4y<`0L*tgBsOwi=zh1RtV>bX_d66$N{LAoC2XSDHPlm?H*Ks)@{=GBc zBWHF8=-J^##_LPrrC%L~)ek<J;O_1;0;5!*W-?xz`e%cHwLtY7@Guky*^HS2Zg;KW}F;`QmsL*M`U)$rO7d%R)P zz1InO!)uQ?y7sZ1Cwl%{b=($S^IGrmaz0jG&c|uIo)TWpZ{uO#eBtAK)Hh@Aht3YK zS@+R(osh3c@7uf0OZI)<0u zQ@Ab-OfNoOWcbDK8oOWRXYcS*M}6--7|+Son_Y|7*TZYkp8I|;yw+vD>J0TumFdmf z`n5R+)ax^mUs2Y3{^X0y-}l37L+l-g;#ZlTAN{P3c-aSEyt=cXXNMOV?)vsF;>t)~ z_{vx1RreEkvBQgu*KXnExt`-tUaDO6m6yJMcLrXX2Rr(nzZS3E!)yJ>e3>6!&Of|< zIet&c0@*iT`-Ye2R^|a~UdYfm>y~|q_)pHjkFV?gz?WSsUx$X*tna~i&ty$_;Pu7G zFL-G8hdRnLUSxRh@G?*HfN?w7myw}y>S$jmFJs>)KPtR7CXhI=#wWwc{G#ATzHW?u z=;tt-!pn0Qe4zMMu6p&W>%`F+_=;zRQ$;{*BE_m$VzGH~ek zkxvY-wGCe4w|LQeo?UruiG!ivqj+q1?LQ+geee8{=X&4ad@vt%B+LJ);k9V@k58Rb z9py_M@@|~&^Y;4N4#nsUsN zkG*l^H-?w>&~eCLm8-t46Y6-+7@l-mXHj{nAOCoLD!hDt={V${j1Q!*yyWX$8R&OT zt3N#WA%7>l*2ms)SmTqSar(-AxA^B}VCy|(`N9W&FT7gM?`wQAqtnc~FJ!-*yx>*)t8cyz2(P2!4{xYmRjzt{ zFCTc}@$5KoKFW)Hk>T1IdGUi+t>dkkz)v34{ehQx;zj0fV|eKg$6?J28S;Zyt>f6= zskv`>&ALwD3DvRAuj*U*dPsQ5KR-~MDp$RI7eC^EF9WZ9T=xe(e=T0W7+$kJFT8i+ z-mK9z4F8tFEVV|sV{%* ztNf=k=8Hdha=%u24gFrzpM{st55QXJ8NN5XX5D|e4&fud z^Wmlt=-c-n4X?55z4@u%a`K|TuZleMeWs6v*ZSD27mT`jJ{qTf^SU3pI=p86yw*A) z@A6nb7nMiz(O+bCpAD~-k@@1s^&0B455@1$?{|DTyjs^seQG}H3h|OJpL^i-^$cv? z*Rq#4@^`~)ZS2K|;#ZlTA2M`4R=d7+yfwV^1wT-nD%0a7U(QGQ8hhWd;hkN?vApn| z_rCDJ@EUtQ%9s2>`Eowe_pReW;U)j(5BaMyJzmy(e(?H1240-|T6+HUJDI;t;k9V{ z!V_oYWqx}nFV=nMgjef1xxBRUMPGU8FV{!E z>(iIlj_}eS;=sPV?v#P{Vd_|)7t*W0e39X=hu8Y}#mDi!yo`h5-+&&W%@>b~%-_#OJa zlXr#}zw!cG^EQ9u*t>66$MGAEWFmvm@nsJUB9-(@6h)YJ{exK{%(PMRbFR@ z*Nbz)*ZjBo>+k2xYbiX$|9oV>+ez;{#)}MZ2rvDrUtnFI6J`R&xjw3=_<0Py%Keb@ zQG8hW-5Or}iDNth^khh{j&;4iF#}y6@nVM$S=>D?>ol!>PK{57{Nc4F2juJZk^TNx zUtV_)ud&}(Fh4vYUU<{@<#k|qEya)@$X}J|@w$B!^?ZDJ^h4K)W5a7X_Tt0Vyzw$l ze^y?v$-t}Ac>I|U)Mwx>tp9Q%P{!+2I4}?JKQ=>>ite@6HLY@5P7sk++4{yskr)*JHwK=>PNc)EVn2 zU#&WheQ*2x@S4}}DmpLK@k7a%`wn{bs`aD)^|qS;ng}H<;8r+u%(x;dc1GGJ{MljBYxnt_{Oj~5xQ{}W#PIS%V}15e10Jhje8zl(t{JBT0o z+VE<9&exaMp&`0HDE>c1J|pivx=%81JgWSq@KOiIp}bVN>a7#?d>p&3ekHu*Uwk+% zU&h%#FNLstT^D)id!k5kG_#XQ5P7u zljoacXq-CMeAVY)ebIO#L+V`f zLWc6Czx1iR)ODgSuXi_iiC5#3A^z%Ed5t|EKOA0*_I}Q%!b`u(6O@-KSH1HFs$<=+ z)^(!Q_x})H{D}{1d@@u=>ncBZeJBI#{UiS^UN?l-toI%4m%{_E)^*!_@FK&14zF3) zRp+Dg8n4HPr~SP;ir?aOb9g;4_VNp(ZtipVcRtFSb@ik;82Y>X-wv;Pj^u^EeAWJ1 z%7Ejybphm&oqUna^QYl8_W1;V_|)~w@uBY*{@lAd%XvK~$4_3aO&x~*zm&U$*Rdn( zDDV0T;&pNS9-Ifbefxg@@N&N_?=WtgeQ#W=zs5e#SsPxn_C5YkzC7oydh==Z{krfv zB!5%%H6!@j)s$iOv0dHG`Gp}(_roA6qRJw7n%X1?Uz{TakdzPy)4&rZI`@D2;g zYya?SeXdZ?ANhmkXPoQ5yll$Ap}*_1KD?Yq>IeBJ;{);1SNr4uyQRqX_w?*wt>**6 z%eo^z6u-*!{K~g=Rh;uP(0gf>mvQ{SUY<|z(|7%PSa{94-dktn%ewl=5chc+zUHHjWchzYcrDs>ds}#!FWyl8t6cT+ z1m)$X3>^A-?Nh^R?7mRmty|0CftTln-Um=$^K8}eCE?}zVLYsPAw%P=Uo~I8XEXHo z3tu&-{&L<_Ub~0*Wv6sA>3crDF1%)akFS-l9Z9VFH1$<~btJ>LhL^rj7g*~>hQ{p` zMSV{Gs_2LQPV+m$YhKTNm%>9{J|B7L_X6G@Uhdz`AI9y}{nrs0So`bd=&cj>9sH{! zSskwkFZWlD!)d&XYw;TUT<)WD>Mwby`8p#LyJ>Crhx*Eke39WN!fWjL$dCSXUW$KC zcny8-`-SjYwC59F46h?IFZ}|ep4#`uLDw5~qE~PJ9Vg5GzlWForsI&mDp$SzuIr>a zj(tzzcJJ;Y>R0h$jZcQgc|OYzUO&q~zf(icu9dI#;q}1S8xL!IGBggKdf#FFIv^W$ zbyP3B$nYNFHFjT!S8M&U&l&o;=<(sTeopyvom8)}zpJ<VBCuFyUB53{MPkyTWsww@&Kf4lUa^3X4P|era z@6G*Ac-g0j5AmpS)!WzOWu5q52ChE2`=idY)nAu{7k}cz8lMb}YxUQ48R-4{zP#QR zUW>NRxgxw;eP8oJhWgh2Mm^Q>s~LFY!@56e-?!@ciSQcx`D3jkKI-NEdg$-M{%v@T zy*|p9eWmM$JY17}`Th{T?2IGBugu7+=Bs|r*V<38!;6g9KZe)F%*$~Y^<^Hsk`p^oy>%Gaa9OJCpv&6|uTq{qwkU)*V* zZyU!C`PA_8+}Lqg-~1+CC?XR0z-d4`i0>&_IaVa2G68z?kuNg*LU@f`S6h6= zugCGG$BT^DKZMuV-v{7F->*)7@UnjSe?{+~4y_{| z@>SP+eJ9VY{@Ojf#?}#k{i(n3s_Sas{@N?N)+aIIz-jq1?^fTp^2NV=k@;H*ud(;r z{8V20Xh$aIbL77HIyAhN8~M@?<|BWQ-ab_w*^Pbvb)WE>bwAPSud&a+9u!`!&pXUp zUgQa?ib^`uXPDz zJgo7_(74+7)(O`~_m4GSEneHg%RI$_H9i>{XP;EBL#=f}{qQ2gv%+iN1QG|<_+-dW z&6j#TIF5dDR%aoP{NqLD@BHv`-E|y_UuAlJoR53tz@%O=)c5$Z!;1`mC%jtM$GQ&U z1No6Jyu|-{1`hq5`QHt%owZ+eKV%-0JVqg3>S3OL9A3v}oa2zcD%10$-=Tb6oPk5X zFZ!Mt`wL%n^xP4z8^depbIi|#*P`tU|2Dkj-~6F^RheELozJct@>QSb^zE;|53h9z zBt8_s%2lt9YjOaut&x{f5WLJs-;?2u;Wc*ug|F*2KGx4Y!)xgO!*XkQEyrFQ*mwQ1 zPqGig&-Kyo7SPKJtaMj``#Q}^BVXpn z4o}FB>rmzOwG15k|6K1HUbEg0weodAcny7CxJP*D4|##kAF@0_di`C`$CqW`H7TI{ z%QJiRq`yOWE!sYMpYR&HuA1-a4f zYoAlkYkZ*j8RvSWk2YoC(C_IS9bRi=Z~jpHD%0adhSvLPr;haMNapW>;Wg`Z$U1?K zeBtrz1eRCv@gl>m;k7Xn#T)WZmJdj;KbLZV|KE&!Wj=W2&v97!y)e9H-G8}m<0sxD zL;Qqibp++leDEQ&J1@LkZykr3?$qn!!5O$ZC@=4dJoLHmZ->`?V~-Dvy2;Bp{UvYu ztnLf-7rR!zUKw8IYdoCBOWs|#mt&w_S4AHB|INKKyk@;_zeTL`B2|{y+6EW z{oLL48ZZ5|BfLB>q?c!PB*PDcSL^pW;&y6(@mG0?`=dCLN8=qQ!;gnoYoF7Xm%d+$ zfxLV^vgvzZxIm0Hvf9)@JWB03{ox%%&7B71Fs&%aED!%OGi)@~s z3$I1H@A%vBnsvRmzfn(lsn1ufYx2{|*B8TU*1pF_9i4Y$pOgQe@baFzc|h|f>o-VW z`wOpneXRcU6F=l@!)rqVISy-lGBj=}3ik=(pA-Gi?+^Vy;l-~wFm9Xcs`>4mf$E6Y zjnRAlrN7weFEYF?yjs_7{wq&>`O#m?Il!*+qUVo&m2V2KSwD9-ucgB4(~++{zWbwZ zzP=M)i}oJ!cf-s3Zt@8;-KqU`KnB+S+D@MWJ5SZG)n7jjFXs_HurDvq5h}0G#=(wF z-5-7Hxc>5vf&DK&urDw7#dyiX`EfAx_Z03EUWddUA6WO7ctZ8CuI`Zo&bzNi9{PF5 z;o&v*e8e9w=Ow=N`^f6ZPF~6E?jK(2;5d|*Dp$QeSYPs0?}z&G+8kbM<4=58di^FSP&*dIac>O_Szo*c*zg`kv zW7jY9RmXZh*7p?Ti(RX~elNT>WnS_Lqi*gejKfPmsQ;7V;3sRlKjdo~uZzRWJjH?h zknx1{*3+dN!0WomzMod})#CS#@Y)c2abVO_^R<+L^?V$A-|^1yTAq=YygD!C>x}pv z`a2IF46pqgy!fyA%kWj|K!zek6T3euU*}`xb#-{zH#rWu ztunp&;-w$NANxCje;r<}{i^saUi8jq`NDVXb?AohS{nl5z_GmKYfBv1-`90Q9nAy& zTX@-T;sg8g^4vwf^tb)f(BB8RHN5!42lnNq5BQO8_X)3ApJVC+^W*=5@ErO+z?$%Ke&Y@M=F2)!uR~|W!O;H~e82GG z4du0mo&F-jlfrA(&-wNY(aLM= z?=(Lqyw=CBJVAM>GQIifugYue`M52-HjU(kFJAN7uRc4xR_4U3=4cYX2P@M^7J&L4IOGXeU#pI|rk`wB0d!V3ZM$&g+iYQE0Mz@hu- z7lqe@VlNJ?@yW1`qj!CjFM9mhwd(ld@M=AuuwL@RKcu&Qt;qp(d|Bk7@1^}=crDuV z`-{S>bw5<|LWc6?e6-JzuWK^UdkX5yu2siB39qrweVy0tx1HB`-56fhiP{$}UhkMO zUwGLUR$kYJ*U;ZzeQ$V;ea?iJI@WdK*6{ktqq;x(o{v|Cm;I)?!?D8D1Mh03XN?8Ba(rPfIz#|8GVf`d->K;pKfwabS&4hQ_I*eXstm`BF!` zTlxBSc-fzb18aOTG)}&pmo?8r|Npdq3$JwvBo3_c$USr>9;s-Bz!|Uqsy6C~(AAQ%=y*|(}o%Q`M>zDlwULOiC-AKl=9lrWtwZhlE|VRCs+l^3eAuHiy@8?9Cs_J2}H9fR}w`tM7+?ALjV*T50e? zpv8;6wN8w^PkTss&D!@ZUSsdmP7W`1Fn?J4RQ{lG^WNWY4X=Gi)=_`8@-_DRwWo#G z*z5K*UT@BP&rPG${9Etkoxd}~YtimIwujf)b=CarZ>%@g)fbl-wUs?>lgoc)&0bQ8Q?yxUO!sA zE(|aJ#DO(0WT=koqSz3Wm)pkCG1*wq^WWn2`{6bAckl4>y9M%YpY!aHFjOa%kv5Q61-afPli0p7a4vkycX@c{h2BG zGH-cN4@lp-J|30Q>*^5SRj#IN}pdtbXIyvCl7czHg7mwb(VulBAp z@={0jtGtd*9)~`kSPn1!Ar3Tea>7i2-t`eLd93-;U+j#l@?qiSy$r`80#&Yh_u1ZG zbw2LMz@gtWJR-d0TYOmOhYvJgGQ^7?`s$CpapbMxHFmw{uhvohTh}3W)(J9RXNH&O zF^)s?tTH`6`g1u4tm{`r9{T;-r-avr*ozOvuX5GPm;H`>)xM|aua&Q7hu8jd;#K#n zbzfNfuEpzx;borY0c&2!&^Y<3yvFXM&kwIz``)^x&uYDfey;Gx;k9v2b<_{)RqKea z^Fs^9y?*F0PK`ayUtXI{p`dOedTDDUorT))Nl`Owhk zfajR< zXg;m}+Aq9TVsAXGc_Bmn<^BMQufIHJqGyK}8Qw9x+{fbs#jkSJ%a`*<{F<-Y7kKfH z*K&AyuI4zb@yXD*r6|P3>)Fu{{oVAV!)rtA#eq>z{k&GboR@fw-G4neycX>}tK-AV zK0uz}(0tYX*EMl4^gXMGhu7G3RiEMy)zSBB>Ghv^xBC8Z3))|2gx8|o51koa-cy%f zDF0Qidi&?vUr)}!kFV?gu+QMHRmUfX*R0Rw)+D~`kp9{c{m|zCFPc(E{>?98CZO-- zYub8W{jukte13Q>+WYqxgjdQxS=aHGm@3nopY?uo4#>;c_pDwUUajkP-~Fm_o*Uw| zDLjY%PT(cswP^RXmxb4&U57pxUhWt45mXPdzJm1D2kRg^pHI8*a2?W@&^kuGF}&(TweHhuUdT|s@LI|N`TBh1p}*sH(1$wDi&n?`hu5sXU#NcjlziPBdFcNO zzB#ik@X=+%*Ip67(urZ{vQic{sPS4aC2yq=VSLw^_JMd8Jt_>g}xK9FAhR&sz{ zUGHnYF!SQWStMxq!{>8@! z@?$-%eg7L7xJw>Lsw02w|BL+LPQ$O`Q2Z)ay?j+(w`Snb@7L}gUi^v=YkV>^U-s5P zc{wtU&V5Q}p^p5kBbmQF!fVm4Lwkl-Jst9@_%xiTA$>QBD;As-!HW4|ZOkG`+#{WneE#O@D$&rTi5@YcP% zBlUZ~<^|117}(OYb6;z`^R)7*GG1$9Xn)5qG!OpaG+y<1UtV{)L&vm!zKzec_$|Kd zD=&Kaf%x)=m-F29h&^7$lVOcZ&)&YK>hXb<*GlrW6hm?Nh4lQxY5IB`PkhX`%6NGW zU>rX6{J{qrPlotF`o6plO1|oIO#Jz){^{|uK3Koh#r4vA+vdqXG@d`a)+LZURWu$S z*y1Y=UdH#$*HOur{jNB8@kdXFEj?aOGcd@nJVE2lAFt)u)%A;8}6Oym>G32lE zqURsdtC#bU{!cUT+zjAXo?y)vUiM$+&t80bGHmH9FXQ>e2UcEZP01HO;_we!yyVaI z&3OLsVP9puHpY-08J{i*EMXwL> zfzAiY9v>G6RrUgA&NN3*YW#B0&6kKa$e>istU z@-j4E`un#y2s)4WhsMhvUapVEsUNq-lVMBGzVcFEabV^3uZ?{1D}F0q^eY*7SdiXx zfT7PP@LIIz6aSWc9a84mH(%=L`s%r%dU<|dzptQvu+|YT&+m<^yu?>ms9t2q9xrLCC0WZ0^&`aH?NVBhr%uj67@_Y>T<;Fntc-coA2l<)SUnTr1c9mD{ zFMh=%-y!+(Tu#68D;_-=w)E_`85r!le&Kaw?B?BHV|ne9d@b7jBVLPk|9HSo?yv15 z`wK7ooT2*(=T8YgIUnnJ;e2rZ!MaYIkbEuLdXLv}JL!BJyPtSL@-^@0h3Yc)^KHDA zN9L>5U)FV~|N8dV3zM%!dw&A2b(xU)RBYv?o^SG^jy~U^mp|trzg2!^^0jEsC-7Rd z=MxtvU)EK1QAhcwCqs20L-y5-9NrL`Fc<$YJTQr{`6!xO@FL`!M^)1 zynK!=;l-c6C&Qc1>5f!B;>k~ytKR1;c*(bQ^j&d)ulUn=`Cd7G{EE*H8Pekg z+21Au|0)CV!Uxjx_x{UiUz zPYC4?ysDmEJ&sr9QRRJ-FY6yaP`oPB;LvKa{VHQ8=&J{c`kvZ(DqJ5I-_r`qetdFEkz>*y78s9>=Tl zsPe|-%lXX@oE9Hn{pI|@>*aAU^!H8hl7GDUh2rB0D}Q=+c+pSG7k{TLXuj}Tw0xbN zd_5qEmPd7ve|j=hU*q+udi`|<`u$bwmwti9^M{w`@5WUg#^VEx^By5yc;fYwqdE)g zJ-ZrDoZn2o>ir|T8lMdL(Fc{+<_xU)Vuu$Q;^q9lUHq7DjZcP^KRtWA=;aF^SnK$T zcIxrN_(X$awK5Uu5{^MVeKh%6 zwCf{Yt@GS@H0`{b_x15($(PUFo!`!9yy(er8sBQSYYeRqu&xt$EoZzq)gL`Ru#T(x zdfa^J@gj@!ndHlJU&q^I6XIFXE z`!u}RRX+6BBwzgD1I4d0JwNrjE#EKCz@hg;c-hzC&o2}oAK2o{4lm>Nw>bQg`TLvX zOMlQq{;FK{<|XfV?Z`m;9K0$YGOWB>&nb+@2e$Nh)%oBhKkTdgkI5H4{6O)mT=lD? zkT<;a`OxR9czG^?AHU+87a7vy1=){%zIsFQHSf=B@p_yI1nJccs;_zC<#z(DL$z+k zmoSsx)j2!|mP-7qBKv=l)RjNH$ncg%zACROC*0(782cpWqxs*QfkW3Xy!gc**7#(I zFFsJc@Ul+G7yr;aL#`N5ArJkxO|cM!z+(lCVkAl#wSDm$dDc{`rY#f zKCssD$H|xHzWm|E4?X{IntqouKzw}Ur^e&;%gg;^xaKLHg*xyzjn}3Gaz3&b-#o~W|GlHwGss?l zT$6^-7x+N)5a%|@*P`w3E3cu~NAqK+j`B5je}9MMYtil>@mfw_n7_W!NAzS^$5nkj zE|2v3i!9ET(|tP}h|ajIPP-m`N3 z#_QP`cw{!jc(wS@fh(@%+jY zlwb45%loT$h=a<*c>W>1>yYc2_?Kqj>YP}+7yo#P$KT#pbw`%Vc;m#cGCe>1S6G?AsGJN1acSl^e#ew{g@qzrPW99X# z4D`NS?IZS;A6}l5yML@a*y96@Yw>z|241n!{ZV;UKH?mgeAy3(1E=N7IQII1oqhDV zNo4JNJoq8wwLbHjHD8t2+41}01i%*$`6A*7)?bjxAm%CSRUong`^s%2n^WZ@+|B{eKMl_B~$C@3Hx6osSop zKu!$TM|~t;WcbMBt3IEo`KhvS^!lnk-_u|IAE}|dEA_5C#K#BH%eQ*UGhSz;z|;C(+|!e<*8LFw z#^VF&&1X#x(Cdp+G68usAIP4J*ZTOQ=NB(}d?3C0w0L>WRQ<89ytXA@N%Le~$4mSw z)AMVcQAgu{oPqDo#51~ zW9-#KJn@YuL*w*k<@Lx6yf7E`zPx@t`MPiH#ep?G8RF}@U3tAf1HXFz?hoq;|MErV z4=?kr!d7fb)8@@F8Q~TFLlHlieKfbmnVHH{<;j@nj1-c`NM}? zd08jM_Lt}6D<=7Hz$+y!5a6*1V9Rar0j9uT8!d?fwz3^+|w!k%!t3@&t|FJqqg= zd-)%F|9E}!wP??C@bY}Y{LHs+zT^+eBR}-kd-^(#{LjhPtn0nF(0s(Fx6h%sevvCL z&vUFp#_?x78P;+1v-0|G^0jFD3A~mv(V_h%U-RBi>~V8fVfz4apt@AK>g)4Db@Y67 z=<|EL4sYa3{CV}4bE>pHiQ%8F*s%N8dW)<+;h&d$K#{eHK4ZoGMqn z-*2t^!qYPF;n}e0J9);R4DqrLpszgW@qzUA&#oKn@$$Z}>kzvZuQQXcvHJ=BmqT}o99^znlHQ-EnjCRU+x>^6RHO} zWtjlI^YEk`VP`!%V)g%BNA-g0Nyh7b@mJRu{*9jy${+O3L+77-wmDlqZ zG+%fvTE5<%d^ykc3sko%)2oB)qjf@EF3i9e=ee)G;LkWRe|Whr)^Yq8j}L6=+2Lip z^}g~T!}ld$i?+YVYbg_wH@wZel^5gYy}!Ra`LeGQ2g-kyt6qOu$JA>}20k+njOsk~ zjd?@7j*R`#>mxsS*;nB8n)tmaH>P#{YVmr1@^x73tjDOUwFw^<#l%a4t@XYGs&0x4soEwRGD5KoiCNwB^fyMd35D9Hed7peEV;buT7bk zyg>a+mM2JG>q!6V41B?U-5>hi@rv?~*HN*jH(vkJ;{)lZ@%rIC{(D~R#Uo#ze2v}T z^QW)u08a;;*&d|2X-2K9hnM|1{*{OL z_&|E~!%M#K8v4H6?%(K`EZTkouSMHW?2&ve+UG-f*-z*f{iRRn^%bOdeOZ$O?4933 zKOee7^5uIF{6X=nT=n`L8h=R!p0uU=!#o|Y=b3rpb$IOQ+3`b<4{Yh#Ut(ZTUL9v& zAySCX$qd!B>WQs!0jW!}^BWgqQ)WOqXR z4t@XYwB&2i_N#a;+J5!<$(Q`=W2heFlw|_y>pEn;VK?^sk9avB>-7i$@yQV17O(ob zmFp0H)-N*rt>kOb)_c6-pjXrAzmK~&!O)FtWLhH6Y2-!cItk0 z-tU{>r7z^S@}kER(z_qBFJXUL1`d7S-mIwh=UKrtM$C_Z5i15yp}!xWW4NC z^`UXKPw|2L7^ff12d`GX#4(jMAA z;{)mOuKnfu=6e)ewC}<`FvOon9o&_{q8Vc{K*s9Jn{0}*Li82_{NhV zJw6aG_u-!B=sW&fypCJYeBrfd`8qlIT1sK$7phm4t6u%=OVsgm8R-29byRV5(*eUBGAyvX7_dCK}# z#Sf}uUO!&d_wzKowkKcuUOr*MOx9U4-83|zg`{h^NRTKU3DzQ*oX_0@8C z;B|cDtt;Iheb?2eFKB<^C0~ouU(ZjzTKlh*q1j*hFlAa*>sX(E=`VKri>&V|uc7(k z&-qyQIdwmQm-u**;VY6a`!{jmw0!a7xvzZ%`%lKf^^fWPa6Yh`#>?k?#`B98JsBFe zXB2w{>G87O+7I*B;`Pb}%@{hnM>gdc3?}*q7I#TWc+um<4llAemn2`VLyklKs$BJHpH;5k{MYqsJU)=#dRloMn}L^S zfOUdhi`SPIG+%fvTE1>bzVK2%s9sg3SC3X5ugXBrC;Ha0@*3M;?q_R%9T{F%#&KU> z-&oLmRbGpduid}ZovHOcruI{nYkwUP#bH79IxmLDoZ20z>#cQyfBlM=ee~Gp6L{$_ zyu>^GmY@B{zrOp`J(4fiZGP~U7kVpt6l-1>Qa{C@x8-5-6|i4Dn@`HBOr6J&XU@?|}(`FchM-evuN&x^fvpNyCL z+M)U42d|p1-wH3^({UYOSNYIyT+sf)Yi;H=G+*jF@BX@1^5ydiabV~+pGV8P>(EjR z*#A@HrAKssI3L)x>WG(prSbg6ZS(&FGY&6(MvqtPdzp9{FaDtmnlHQ-EnmkZU$fp1 z>3en5M`NFVRbE5SNBy7=Azu21-ugw~>aPbaXn*0gX#I6!^5r@tzfk_GOs|fu``R5D z=>Kh2M|SE+hIr}wvHfLU?)UW9Tf)ot!20q!c|r4q*P`X?(aD$ni9Uk*sLJ&E!ST9& z`TXyuG}5jyWY_91ygauZ+h6P|udBkNzHc&(*Ao{sUwAEAzMhtR$-h2<`l!nE>cPJ5 z@4Z*+eG}^kyH7osu%A}Pwb9$>cy7fG;zxeNg7z0)i`HM4CSQ1|J5;YKSH0`d z{d2(hzlwazqq{%!h2!c+R!6+7)AYuPPfvy|J$t;2*LRLrUi9x+(0t*wX!&|y^5s5F z9-;hKx$3Q}`b-~vF9YlSHh;B_`T{TO4}a{$H=YcQ!^^&cAH3WTP3tdlJ~pTR(g*eP z$J=EB)>VAPmoGBJ%X43Pd5{-+GNcy|KlXTC5ruK=TD-oKe7O$s2gR>))m#6TbHIN2 zrpSJ8$aRQ6yvX?C<#|jUSLcThy^q3$I1X*N)_1?TIPrT^$Jzn%z zWT4N7^u6QwRT(eOB*2@JarQXukS};{b1M1*8$0w_uTn|;#ZlTe|hqJ!uX3a z@T4qo)-QI`cpVyh+WPBjMu8;M(oI1*v^Mf6%^{l)q z4>EgvAinwwvcv0db3wt29lupRWI^*)d6_5vWb=|IXq-Gjc4PB(-{fm;0*M1_d@{t> z^${=jU&_EuckljiK05yYGI!_UmR?o8@1qjw0)#XW`XW^!6+#cj&{MmTqKY08AfR#r zri!Z2Npk@eMFAxjJQ5lOxgepB0un?TX{1pEJsjyl1QoBG11btgqku=x^RRxO^*nd@ z%;%kZuC?xY{~CLYcg``sbIdX4JKuM$y|=oe^H;gpwX5;zq;^wW{?0=$*X@$8*8Q=@ zr<2BUn;7KFJia1!e~+)$eJfwM+#l=voEo1_%9nksa_vh)-$Rx!N(Z;hI~% z_HSrCs;}>59=E$5an<{Wy5Zlvt1D8wTKC%HaxPR~`e|47d(JdpxaO9xb0+d-9_88m zk?iL2gj`^MW_0HX^JxCax?kY(`J;CH;$p``vhR$c#pUmt^m6@V^0n&cY0i`O8{=`V zYdt?2SIrkL{PKdpFI;oW*JG2f9m&6Zk}*$yAJcu? zIo~{fE)G6&+jd+XC`0XW6BFz^Fk-Jv#(n}@?swR z-6*`qGsWdT9?fK$QFW2wQG+(&pmaq3DU$@P~$UkYms?Kg6y~kAd&JDkjhJH`SzRI8a zqVtE#zF6DU_;iw=EVt#~#ZX7MdjI|xzV|0z_5LHC_}bIS+KwI9E`~_^1b;OT?6}-7 zYM!}m#b>uaoRALU;c_3stDkms@=ua4b&r?CRrPA$8AGkFlVjiR|5w1}z1--2FW>Sb zFK-XmcK^S^hm$Y&H+doXr^^$`uD^Wg_uw@2IdZKN^TrPy*D2{=>tB5B=_G&7UzO`I zY3T34;MGrk(Z%_z}Gv67OPM`9Nm7TOZShiuvP!=-)QC-fJO z{F~%!)$?krzDCbql}p}m8>ey6N$u8s{`!BDFXv!!$QqwcYNy^w_E)5#@0*xk*K6Gv zKdxJ+Kf8YXu;U?HcKu(WVPx+<@!jOBjFGaOU-{VcD0PPG3GwTBn_YZdbn=Ac%Q*)( zS@S|C`Jt2IS3mVWgiAaAa5>+54%AK@?dfD~$G$4pjgqh1#-TW*I8|rItFElaRiB4& z$yd#5jZY_Wt^0ZC=E+yKtFRdtZ6-- z5KwtDo)5(C{i^kdN8QlL%2o5#o0nr7TxZ7K`hBYw*S;0kqx{?N<%|7O;i})4a~&65 zzA6{HcD?zU;`&1VQA_WA=&m!ZugW!-`nvB-^HsU#lCOV%*kVVyaSB|$+kezOJDs#n z)cqB5!9Mk*=wI8p_<>*Rkkp<(T-J$p;?lKeCtG&canX$r2VH#re()z3EpC#I+8I~% zM<@BI=LvD2o`&^vKI0{|=MR_PtJO~2+Mb;>4tkZi*tO@EUh{R$ndS@E-14<^ruo7( zw|t$JeAV|k+g*?9n$-XC43X^C7ik?>kGM9KugYcp;h@*`c&p^gdnWCS%W&E0r18;7 zagQ)WiZ4&3_Wa@U+^8Kc=W*>znC9OF)4y`@Pa3cG;@o;o>+x0r_Pi&~J!5}KF0kVw zYrb%~-)bi>bmL$rjh9{}E_Us4(9N$nXCz;9J5S)kD?i3ZHx72PWp{n_JaOhs*CVdE zt;c;6>(TmApXP^b&7-Ao$>O=PA6Nu z><`i~vW{OKaO#K4JZQ&VZO=}&cwMht;_|D%_2BwvW|}WtbIaEkSIn1rFfaBm_33%} zXHp=&^@ZzdDTrEM{FrxjL$>UdtF|`|o*}6s9^< zx9XLhPEOT*?Z>b6MOp{^;qv=s+KF4+vy=SLtE^mfe(7~R{zvk)>i2V;zt;76EiU)- znlE}QFUDK*L?8XU_F5M#GCH^S_qd#s02UOi9z+{FIHUn@S|+oU1?;<|5d_xC$+*+1~(p^MK>w(PFsq8lG>y7>J4 zQt~yo^F6M)o$sGJ(|qBYTfSb9e9i578`tQ0!g{jasvLdZens-tdVUnA72mpE_wQTb z+El)tZeWqdH?@DgHu;*{^CK?%t^M6F>E?@_Y}s8O-S6K&)B3_SxBB{T6ZvwEQm^KR zY|W#(uJ29wtMyTJT>QH(F6la*Y}s9}T%`=1ZuduAbE~giuUORAu55Vrb<#dq^=faOzwE2q{X2oUu9`shUva&c zvk$YAEne4g(fP68>u41aer|RsuZWDvN$=BOrm#@kr5B&3o%XLy*QhPet zvb&CpPT~`<=Ib^yU5~itwjR$&zSM9<{3YPaj!1Q&en#tSbUof}ruBtuZuNEdcIoOvVb ze9}qn>|gF1_NkAj;d_5#@dLYl>V~dfaA~idxODB=$(G%9Ty*j4{#DPb_nPT?#3e6e z9bYRi#;<*qqwDc8$ya@UU*lC>e7x$)_rJ~4chj)`{b>EI&;N**}ptLn&14|&{2Nh zs?Jxf6a8?t>dSm+Z~jR0NUstXyY~FjEALBYx_{x4PqL10G+(3p*DGea9&ycWJ-##f zn%nyvT&?q$_0p=ZmR)_0zR&siOzR8R-0JI-Gu^*%*|$jbF;!o#YhUH){`KkPYi{>P zTywiW{?nTFFXz+~GLfERZXW&LuCVyQeq?`9H*^x0y3meJYELJp>I;|lr1OM$!CwAd zwtq>!PE7}KNPeo$j@SLdIbXcLNW)7Xy7)mJ`O}WhAFivVzjor*yt9-1(Mj=fv5PBT z`q%T<|4P1Yn2y?!{8gP@9Ou>ge=^Qb!+*^S$vRGX;D^Lzezg;qu01>1;&uIeKSXLj zm9Hzla?wJ4=tpu}b#}bI?<-&8{%smwd~oqYZ@zG8FF)eq5ucrG*3JCdKOv-4v=upZUD&tZH%Wc>We1D!uy__Y(awr3~#p;w8k+Qs9)=Ifd>tuI{i zyY76A*4OotuboM>ypr-?b$0V;|El%%ku-e4k1u{ucl^l%oj+XSXh$ctr<45AtHf39 z>Px&@U#G6AzU0ZemKV<*bv+sy(#;QP9<9fnxnQ5Z zH2QYG4}gmw^&~D`e0H+M>pCtvS=ZwgazUI&&NN@R=9aIwBwr^dkLHDx_o}nYyZZ8c ztS)~g4eyc%F5}m))(JbVO})>-WgNy=>#Oefe~^6bPDlO7>W@wupXZ?yazVbH9=-nm zA^lqU!sWVgh>MROcCux69T%MxhyS`B-!s#E;hI~%zL0!fGYOVgQr@f1ZXT~4gF0>1 zm-*9AKIkMa>p?ql>DsfCEne53?uSV2@zKQ>=ZneLs_%2w{eBgfVaX4jG!8mxy!0yV zAJr~j;??!|-Q;UmI*LQ`SM_RN_wx|0xjhg4d-656&pU8!>i#(TeSo80wWz?komX)= ze^?*tnm)Q7FXgC6>(P3u>+zb&*W8}BaoKO>$MEQ_e6{TQANE6}{M3Aj&)@ZuuT}4l z&a1l<-~G{hU+=#vk8$IzT&|O4n9I+(dhOcbqLcW@>w(PEt z*4M3)uU&Cuo=N_zUhV6?KjNC({qdeNU5~ib5ow;tRvoqM`i-u~^OCQ*-5+ty?f&?% znXX4%b6by(Prg=t9&%r-@9%vMMURrkjM$fBnOTOmz{tMUK-haJkruo7(w|sqIru!GJx$R#cPrl~% zegfCr&R?IN>3YO9xApjs$(QdpJJ*ozCsnU@?^k`U=G=FcG<lem1p zKs#|gS8C5rws@^?T-x)4TmQ;?-ODW^-!>h!yk>VRasXc$VuAF|J zd%3Ia*~u2K>y=A;e(88C7w*cXpX=h0{LrhcT&`CxarxVkc|0Q>aP-cj_TrK1^5G1T z@?xAM&dPOC`e|3Wv@c6;$qjn za>%3c@7lG<_#M+xKeGCxll*wzzE3W&zbX1prGfbT8!ufyT-Ql|?c}eJM*~o zt32$uekA?PzdY5v&`EyiBs(s4{_&7_`^c%9=0N@$oAzjy;)2 z`-C{8ywmw5`KkMaxcq)Jj@b1h@zHVl{={(!XdX!I@sKTE{pxjGm80soPEMf8C13jC zA!}Ur7FXTxjhBD=zRctG;!qq?oT^v5eWvdBA4)^-qj8B}dDwB?KKp z*%5zu$e5SE_dzGMvmSTnf_Z#h^!oeT`r)EiF8eBf+KI0{oz$*!X^-oK6rTCguX3@= z*RBNe_pMzQUw=BOUF9;qC!`@RcK!IJ8_#)}NB1$;$r_(dYFE#z?7yFeU%1)ghu(bQ zIzIjJ@+&?&oow0l$EE#txlT(*>zZHwXuI8+Dd=-iOQip9+ExVz)|*lc3j>ATp=C# z$3@owCG%Rdrlb_CL}vvd)L`@?)Hpi(NZz z+36&Ey$|V+>#QvJ?Q(f@xxB)U%8xz`6KK2AD4N&Rve1QuQ=>a5=-}U0Ix$gL>?dg?^T^wAke0d(SUly?LJcI}^* zdAxBPvhz!_i%YUQk9c3t?!A-0AI%>g`K~%HzZamL{^GOK$(FrxY2UjZaq*9vAF^fF zPrEARtJ=*QzqP(Dnd$uzmwLx<9_782mzG`sdLQDSG~e3uhih*4$9HEQubTp>{OW?8 zPO^J$ai3&=bsB!{{)->vU%p87CSSO`FJzZj@!9EQ%U-#(7atGF&L1v*wBv_t+4<3~ zO8KgG9Q@Yx`2LyRA92Z-an$j(^3t;Fzt0aviZhk3x!oT>IMelrYi{cim-;f_r5uiG z*{AAjbUprA=CSUFb^pR$d0Sli+eh_dH%`(z;18GUBtN9~bh2f4y>hYR5U=iExU|=f zPPXi=JnA>f^|8$3+|H}G=5}7irA}J&)w1hn-l`luul_~mac=MBa2cm{LMK~!nX3Eu zb5Nu>=Ao`fT>NWS+zjulqg;QLd7RsOIb3sl?!Yy-=ZWg|F@3RGo8fceIdL1tN846lHKP*_$t>6QYg6akk%)ExLha2<-gkNb$XTZ zRqZ(VulvNeGmp1ON9{=ds$T8h=hXAko73>JEO_(4pYhV=3m3oa`isv_C)w-Y2^0sH z_e|b*<00jbKV05hX(uk7WGBU^SBZ;VdmMDUxc+6P>k-%7)*~+SMAp1ld%aGta&$fZ z*{c^jiuW<*n-r(&)$V&kSI!0P9~Is2E7UqNUUfy{vQMyYcRk`HD;GPzcu4C&d|ZAH zPdk3JuXfkPr&lRo?BepPf6e1(GLN@RN9{=ds?IKseDBN!ai0|Z{CuHUzW6g-_R(Ty#<#I;nr8~Mb{oTU3_tH*>|+(hmMb(xgjT4tR+R;hv>1509Ixad{=fQg550~HPJ1!lKm!IwO z>MsxcSa_IyW*+M~r{=2_-+06$wSO!_qWab>}*+xjlDWG4oi@C&pp8RqwSs z*J&>={Hc4oeBp9mV>b^ZJO3oR_h;U-ii2zaRTe$?!=r!IaT$l};?hZWQha)qxY)Iy zs;_I$bUosl+j_(`xAk~>=5cP%9k}N9+<~jESL=ryU5~Hjph)YH-}>Bflgy*@y>)D# z;f|e7PT3!*VI;qJNSyM6>-6+vukqMRnC17AoxkemX}F%83%K~hMJI99@9T@NAIVNu zes&TUU3>YV+t+cOG1Ga(HMe=hHMe=(pLz8Ap5|M9*1DjR+B=6km#{x24KF%=@dLl= zl~ix)3zzd3JHO(y(@Azbr2e?P_f_Ans)U54}oU?Ar5-2e0e* z$~@YCv~!(5b~?#^-5BIu+-JsqpM8rTYJB4rkHmFc`fH~Syx-G4&6Wb!(vTnY$K^U+ zarD=ojw^pQ>_5#TE`InYTfDAUF75H*sCj%?=5a?HX-D#3b#`%f#o&A|?th7WM<&`h z^)p^NiAx@}t8ukwCtG&canX$rH@%fF{wo)MEjz#3Rav=Q$Hi|w=UkY1JS82(A^E9# zwXgfVT;-BC^GdSgAzOCixTc1Y{K=F4RmWw1UFVKYvXlJMtE^mj%_CiW{+^zBtoM)F zo?hd!@63hj=X0_D(d`#M$d`UNs*cO^kaqgxV5gJp_JmGV{X=8@mJ9)BnEXkQkGNM!E?s+evSoK2 z7v1>q(QDn~(hf)UQ|a2c0*F|XpY z)5(^-a%s=6{FCy>AFfU9tMY|QTpajoef>e^v7Q5JdpiFl`_34uANTF;o^x>7?~O;k z#b>9J?CU<~;1b7t)5(?{mv&XkSGAi*e(QRCf9CPF>1bYRzSygOcI&3jo#c;RB`$XDanr>Y2bcSY_WaQCv6KAJs~qL} z=ggz~8~z%Py~br<_j@^9^|`}7p&lwHK3sHC|7S8p*8JiC+mFH`w*_{q=Py$UVgUAtAEY2dgYJAX`J85 zJkIU=Ik@KbeE?i{&xF}`&BLnm_#_S@)sgul&98a9{A(AM9qFeYNN%gn&d<8!bG}TywiWUODqPxAll?ZtD@(+}7h&GLJV*p{iq2{Z^gbdT?JP?KAIA z!|mQ5aq(~ciA%apCtG&canVWl9r5gI{Nd7GJ3862^P^prqg+?dJkIU=0JyxbHU3&p z>@^?k>Wb9g=kA_|?BA}N2ReVa=JtJn6KA>}amk;&w&ttXuH9(e2k`?|k}jLSIce9=ifWGj!Z*L-Pjzo(0D zoY%@c%9nPe_H^+`_MI_QKi(H^_x&oa>%}i#^Cvz#on-f3uEn+8&yjJ7)0(fA9hd%9 z%2&0UM}Du62K?PH^SC2+^Fs1hb#{K%{oH{|9pM+3E{ay~2_a)}ddaTdEct~7yQvYW%MAmxOUwb;PxxJUWapv)qj0Z1ixK*!q z@AYn4{P=u&yTAWfxzv@slI(cMmfbjTY0sZ}(ZA}rruGT__$T?LS6R96sx!Ly{GFY7 zG!NR5;#9rbt!MWkdGQ`|yU&lf#My3Mj8Fe6aaFr~iC6DKxSU7q7skt<{&cd%rGL${ zeTP31Cx3U(Jl-&Y_#wrwIy*n+kHqC1z1?}l#Xo*=N!RIQ%kDZZIw?Qm)p^9Fy>@i6 zW#1In{W6cvDdvk*4|JR)yU&r0o1N6&{2Gtz#?6lFv~*D&+aq7n}9oOeGkDiBICu@8sm@s6y>WgKLUYaHxk%kDZZI;rlB ztKNs?%iq7>?mXg>FP|66*XTTcA@j%|e$x1=UhTN*IpK@vB|GYNz9(la;TPFaJ+Ren@_dhfW%=IO<-1T=rG%_#@ek zQ@;Ga0M8}-imxA?#C1#zxWvQtZ%GLMu9N)px95cm0_PmOm5Ut@$?kJi^TGa54f91C ze({hkF84RQ`g6-pCtLRa#FY;G;vrjHt@roZ>yL+Qak;+CR}9+oi{JdZZXSI;RO9PU zibwKGud;G!&mX`zU@?f$5uD@t1UdS8$XGYKV0J0`?hxc;UtZh zUL~$-$Dx1C*Ng!x=ydMa=9))_*?TRE~!7s51r(fUgZ>*c$Mp5>g)P(C{CS6_8Pa9Fa7Iy z>b&66jz3)dXeT~hdv>zL>$-S!?b+#dU&W;zj_Rk{>vejS{I=}4TJz|7D}Kv=D~~u@ z{n*8;TogRmsm4DoHa>{-W4I}x*L*g_}Tqma=yZ+p=)5(?{Z{_N(dt6iJLgT_i zichbya%nF=^ji10*lT<`KQ(SEU)qgwJwNqTzt>Uc(YVATTU^@P7q#!5M_i--7fF0^ zN%mE__%m;GvSn}OOTWs+t}ghk`-FV4Yo|Y*A9hk+=v7uO?fIjtZ{x(ZBM#LSDUQ0J zll;<2{VNwg#!KSF_3JY|7vkEHf#Nk@<7cOnExSC{bIx}2RomCN^36`RxLmJX?B)%> z`sPo*#3l77`Jt2i(yN@}60dT-DD~wzUYt6Q>@}|OdC$(j_V-E$eft$e|Y$7d!u?{@TkQF3;IDJ{=$bB){}3E0?(Z>R z;?OJCD;nn@Ki{k(iZLw)gQ+;skMd7rGE{`Nub@sKUM{ zgdtMi@sRSzpM0@v#~uIGUa!-uyrdeUi^qR!9&xtf^Ham9ksrE&%pDLc{CnebQ0I;3B~wOg->11ukZd^MUBH8uhmu~#>HTB#f&n3+A|L0b_ z_&dr493&p?E0=R&{a!4$)&-s9hfcEZV2I=g7irx3$(Qcd1)Ve=I%&N0DtA;vbR4+E ztz7)OUfrqJYUiig={x*T^3s3zXpFGGHoj+Vt=N#?Ju$y(S z9WJ`~@s_fYpKr@6iKFW5xO@)dJZ?Yu z?KJ#_pIZFDuAgz!NnHGCC$2nc&rY^@UB^Y&o?p5;5(n2*-B(_5_@P(1Di{75UtHH~ zT-U3eUb)!$lP~qn-=|Vvw@(M{Nc#|-Uy{A6RxTX;(OX>X^2$HmzQay# zit95MEQn4|ApE5GRqwT*uVJM6;?KD0#))fZ`mt*#PwXYk@_c`lT=2e0|MO$t5dd8L zX@`#;m-w!WuU)m*>+~wmuZHNgzUuykOI(sW$r}MBmCO8>P}pKPKbe& z7hE4rVE*+JM|*Z$_4_8;<1#)xq;|%|k2tveJ`6wN>#sfipJrNLxaL-0xXgq3mrwJ^ zZvIH~p{_{tVjj0!U%1qT_?1gt;32g$E`BQ4cI)fwjrx*@8lO($avyS^mFIKP(BBiq z%fCF(NnFRLzjpfbuRR`;eRm9X9&wpRc^AjH#G~W7YW(65m(CA6$q&6sTPkNw4|(;nbJ=hjt{6s`uK@*D$iTzHseKKl6)2p4dy6jXkH~62I0L zo@&SEI=#yCt08)w$GWfL5|`vo-uPiBU8h%hel{PZd-m+Rv3U)SRevK~)P2kl6H zs$T8aj^RGV&z+-h_j^{jZkYbYUpd&*Y$@1{w|=hx*Li8E9`rLmbP^Z8+6hc2*-3ur zRpMed-a3!!n?GFI)%e=E&Ohloy~@hvI)6Cjg};+iUvqmehwGROq~^=GjKg@zmfdyl zC+OPqD_?aUan=1^dv#iQYn&5eAnhx-Ja6-ZPaN&pae3dNpZ2(n4-cuGaq%M#uI;`z zIW6_&@8^p{*7$T%JKtN{kqfwfH2O>O3x~!hFY-VqanZFCmvo&@;-XiHtJ?92SJxvh zzdz}DyT;SM`fu^dSM`I3onJbCm8;eV4*uvQKXh`63lGUZe>ZRJU#^Q|Jam%XImvUC z_2RwBcHhh4GJp7um;cI%i+y(tb)UdxpO6=Ej7vN^F7E+wh)d^(o#cmJB`$X3z^8xR zS8>hly&Nvw)~oT6@sAB*S^N5lj7rfU@oxZs_%pOC(Wz)xN6-Sm-zbAN%4(qcP^+$ zT$kk!U5d{i$<7}x=K<|p=U+cOr13ewT`3og3zzdf|N4oeJv%PfuOMKN_=qWO)^w=)fF$siUT=}kc*KyIS#Ko>Xzs9e=U6(KSwHjZ4l7EsP zdX<$62Y>X+bx!KbJZMLXQ}t^1{c8K5_K!$Ie@~&tmj~k`aosBY+4&Wpoldgre?l(k zkLwCM78kb5Wt{lcr+teZ4=FyK)E^f+KcxQJ8>f7EPSc(rT>wK9X<0En1EdAN}6`!3> zvRltPb3uPx{$Btt*KyJL!*%8O(@tD1TKBm4BgNxiTzoG|-Jg;U+L8QJot+=`Wj?fjb{f7Q z518_(U(FXguA8JkJHItPozxDOeV-lIQ&K3nT*pNhAJ-idsE${B{pln>p0{`80z0k~ z@*;qL*ZHNBxTe;ldcZ^SL$4ASyS(s6ue=}norUGxbkt71t3Nu~;u80dHH@skrzBt2 z6@MQK*Lmr;E-vv74|08erd(eM*S$8BFY_R6_9^)!>pXrnT>1Bm%iptHwZ6pr6rjl7 z`uh5Y=keem*LTBp&-7n+-CtI?dgt*?FI%*{YlF*t;w@p8^Iq-d@gC(u<+3kmPyf?! z?Q3vxyIn5hc{~S^l?zX;`;UcdwC-EH;*HkVmnP=Xa9iOwQ!xCj{NlIOM7ui{lCW$*_*FZ!Zmsyn&NuP zAlHq;bs+u4F14~UzAn-*vNvBh3)i~q-hNc~iC(S)8>%no!g^lq<+^*gHg$g- z-S5v0*PhJ3dg-mNseR%bgY|WA#riUi68pUNzoWl(f{WW0SIcfZqxYe=h3jqs5QpC> zF5}aVH2xeVb^nZK-T4}QPWxcEwg$P3qfFOw|1xi`)8&(N zKUEk1u;F}F?%s3ZSHiV-L;3RleY^Sk=WxyKzJ1)w7c+kE!F;I)`5@KJA%@7_`nq1Y z_NSkE=-t1xFX5MUe_m_vn6qS-7?`E^+uZTy{FiZa(YxjsGPLe=rH` z&DT4_byoU|)60biCutrJGeq`sy*pg{({Ei|#`T;*uFr()zH8=^uSXQF-u3u*;W{wL zCBG&7@?1DNk6#Vfy7w>h_@n~do3C$%tM$1Rx2?Ri?4Ac7%Ryvs-T!;I>hBpCFSo7u zt@UWXKb?cf-hBPQD;6Vg9udEnOZyUjSzk9O7kcw`bht)8S7_zSdaU);%XRq`^EJga z`ab8XD{@WExF|{_u3Gb^*nmcd2qPq_TJ>j z!Zo*Z&iUaQeeP)0J^Q-`>+6Z(TKD(A)K`5E(0dR$UJb%PA4~&uip*VzCo@QuW=nI4SMtSj&O~hbEfh&^?924c8=ul z)8QJe`;rdRyR7@s=k3pjYi{?)FRjQW@5@Bx2lF%fp6RGpE-Gp^Q?kMec( zlE~iu{)fV~?(@Cpq0#gG@!`7PV7{j6tM2!``8p|Fn>t6=eWZ6^y?(gXy-%3OXP5fw z<+^FO>VB_oxvk@7H;;Z_VRtU*fBkg);++;3db#cxu66GdxN3d%a_tS*fs9jL+;`=L zoleRZoz#DHJ)RS;(febIOTOyg+3L;LdEpxUe7luL`KsTa=;eC&iuEtoJ*NHZfXV_ZFOt7 z>WEIZxb%OAhLOGbdT+Q!_lXuSu7ib3y;&FJ(P?L#ADE~w>#P;OWf$*o;i~!4Uwd4i z2$#QyrJeC|%T6cxp_9gQAwy(uzCIVO(dUj9uW>!1aMgUtQ=P|u4A6vtqtl>rp))8syp)uD$7R z{N{mMb~@Rz>wilPBYX3;J6!9&56RcF2D$b$xcKF^l^654?&sRQ;TpX^;=)gw$NK!( zyWihGT%-F$i%Y%_1C8vhuLp)}Px_fJo}PPVw*SAJf@$X>3C!?l%u;`efCk8fSy zXL@0{TKfb(@yqZhzlS_EkH%x(>6e6S^#0i5b*b4sHDA z&%CkI$(G%`{HTVJyv4cT%+$-r|SNN zg{wDTUzn&b^>2PlnC5-R{&nrrv2q!g_Vm9G*WBK(ej{9?`&UWFGM)1Lu%%`3L(P{w zITy*-cf&P$AL6HkU+(u?qwh^x`Lci2 z^L_6;o*1st`*tf|>gI708`;ZsQn=RrJcR4vg{!x|ZWS)~SL0V7+_KZj);!`mqG4n& z*Vc;pYVjJ^=zVB!xJLKYseCIkK_CZrFSqP;a%vtAYZ%$f^^4)E&qL~i+bOPf zzc0KfT>Q@BR=)z&T5js^b<}+J-iMA3*QV-z^uB$qaJ9adW`C^XHa#u7c|5FPWSvL- zwO1F{3)iOVeu@i^^BS(3PUOpQjf+n5Lnp->{oMW5;hNif%pX~C9;fm(I*(_DYxMru znqT|s=h2e!~3W@}B9#$1b|`=IfK;8h!4V+E?p%db$2STziuNe$L>{&hj&>Rpdt3)kJ!-}sG}TXs4*HIJj``+o~p>;8yulxy^J0xd7fw>V zT3@|fHwahjK7n)}!IJ;!oAr==+IhgllgA#5dk4F734=opYYX5ZPPzzZ|a7=h_yR z_4vg>u4jd7^u0-I9_8yQ1-!TJe?45I>#>AgW=FmbE-i~6dbwT}u1)P}$xJJ*>t$DPbN59AS=5UR^|7z8}`g(*!MfT?FT`SJx6xZnYw%-@70~wEb zF%O2zPA6Mj`j39!_x<4-U5_m;^LWGnB74{4$HFzb-%oLk{vFEChpTm-Fdy5UNBJ84 z9QoVfn%jBxa=*2h@l8EH9vZB#%ZICV{#tb&N1unTI>BYQt@>)&)z=F-*yegXAzbTz zZ-VRLg{ybJzxjr^%zxcid%4aG*J$0h=Fz#(`vGa|-B<4!uB~(w$9VZ;r<1LHRsX{p zM)q=@6Ry$oYKu#K)$#Ps03pI@F&DSHtHG1Ek;u@XDM}=!o z#v{(E`O^O}gZcXDaJBB++~TE^t@^?>`gz*(!nN+_NA*?bvA6DDzG8i~@`dYLek z{qL@ruh#q?9gflOA^(23=Jx*nE8!YF7q;@H?nj>=zaFl2|GirERsSAeZ{2?@T&;7V zvTfyMs_sXBkM(=un%nok{$s`Zn#xz*zk2tH6MuWra#QDt%GFz6r-rM3PgLDb)t9<+ z{p-}?XBUTGaoFi( z%dQU0qxQY^_26)ArK338<=U10yCc=t==T&Z4A;8PtH$+MiHhv4ug8UJ^f|3n_vY~l zgIrHuk;}Z7@JE?P`-khj`TE6hjh>@hT;}lxfFgUjo)NCm_k}Gk^Z3OWr_TV-6kBh~$9qrYJ9 z;)mXS^>4y8dfyg5;g`ZZ9$8wp$MrwMweI)P&a0#K^-tk?V8&%$)Cd3UW%$kSlk5MM z(xKKpzw#^p{~oT<^~g^Nzx==R^*uo4!ejjW{lM!Ml{mNeCRYyEfyO+_gM8D;mfbv# zK6jiFu1%dMDp&73-XvUeJAd6QT%+ezb!uKo=dTYkME35hw+z>+-|N8P_cE_TfDtomxOEdJwPju>VEX^ z`n)_`o7yKTcke#&nsDt+qWLji`Ddq-ExYr@Aq^vY=kaafTK9e1xSm_Mdh_+saIO1u zcljFq-u9=$)!M)KZ_QWB-n&oeuf6sC7vUP+C-^PXwfw!zr<4o5_4UPYjqVf1SHds3 zM&IZBYq&wd1qRlkqfo39&%Yk$UN{OZ>U-)wbO{HMz4^K@T&?#2+>UaM zexK>5!nHR5;@c-kcAR7@U+Vs_hLJU2`dh#B|0`Uh`#nD;{PI3@q+IA-k53BM-1e`Z z3)kpA(VAcL|EZF%I*;bBey;E<6ZOSyD}HMp+3P&^ay>6xqxWsR*U12mja2u?rolzW zFD~?Q{l<#XHuBS0X_Hw;+#eA8s5`OtTn9=w5uL;+xpVw9{b^nNRp*LS|4cGqk zH!u8h%T6a-cKy#Q4SKoW6E5$AxZ!syzV_OY`X6S9?A=#C6s|q#Cl0^dveU_yUH?DO zFtRsapAFYp=_d}q+_KZjR^8h#NB6J4U$MSgT*mbr1BmR+*Vn@}dX8@8QN9iquHO3k zUbr@Oe;j>}d80QhGP{*=8NYcjTy{D+HIH>Y_RiyJ;X1HpF7s&rF^*oYn}=)N?+bC& zeD&7XZNs&9Fkh|tUG=_w$8e3Fb6WLEkL<1c z`)z0*FnitWw`d0`ck+2RsU0Vbuc=QuMXGT_OHvrwW;%c z-6wkI@y+2{_c_Nrj(%_ZkHU3!sV{z~>Zo-evVXmegUH@};!nd>_xtVEz5BL(3zy%^ z-0uIk`pAm;YOPaT&mPRzN5eI@`_N~?HMjl#hks{LiLHF8m)3l>>Ym;C$#mAb=T|-J zceQZU{l0fS@+&T>zMKcxD;FN)$945^ZKb0)>*9JZpvYdX>xav@M!DAgIn0g1bzm@G z@?O%i{QU7yxzL-hQ^U2t!KF^*v4m;wXVl-8`vq^`0m08ZP^cyzIw=pM&uRAv*QWOSI-cHqJvv;QdM|gO z1V;AO*Ym@*C;g1CcOJDb;g|1CM&IYWAY85cqj;_OQ}uO5>DZgEw}xxR z^!H)@BV2ovZ{s&!)4)zA#ix_(M;Ib|^L6DnE?VwSKk<9Hv@hY8>v8mZ3de?PwC?4l za;f{nie8k_X*D{zgM_==W%Pp`Ksr_-hBN;xHh$a9U9EnL&9Y}+mFnn z`Cu<$Hun7Z_;8J$zgoP;^>bw)z4diTxK`c2Zk71EBdycXzX$WGa6KUYjNf<-mz_?E zPbXXV$KJZXEL@wa`@=vZd*|^_!d2fh885d}`SO0mJnBFCIr8ViHCp#m^Jw0TuQy*` z4AKgBir`+VOG*XaGRHNVzPtuOgA&t!eibfwD{nH~K*lvDZIGJ%o3TqlKVQ}1(5 z9OSxDxYm7MH7}#{cxt#>?@iQ6tBzWBbz}dLwz?kGf%f!s!!@_>)jm92dom#7mnY+7 zr;{za{-fU;dh~|s%e;)9qn{P7b-$m$bwpw#Ykg^Fov5!%!=*mWi+SLdoldsw`j7s9 zzSo88>;w{r->LY{|$-!qM#@821&b$_m)J`WY(-g$gixVF;Y`1$3Qoldsw`d_&;=;iuIxE|Qxvab~P zu$s@0xa#|yT3`HHzt;DsH#CppjDBzXAHp@a^Ve6yHF}Pg_vOUq2l*PkKYlA*t^1HV zQ3o~t6xZngb-Ub~7AvI1g`>sAzV7cS938H`0T4%?dbzYCy$3qP5ZSv=Trpg8dvCHM zTyxv+Zy2t*?f1LF)p{OM9_o%xw$`Ki8vTCsDd8G@er)ktHwO(MvfhX6bMAM>xjS6- zy@~eRw&JsU&&0ocjehU(v<8>)PH`EZcBF9~VTkPACw?Scb9;|@Mz}`r+vc&P#^p;wCO+k`^XOn*Pb{OhhK5n>Eu+tzM)}c?>s(!LtOUNiwC)WK3t=9Kb5c1ef6Sn z?a#Q(SMNM(U(&Mt{%ZYPw09o=?{KaA{AK?d<$BhJxkmrb^>e~Cx97Cyhimj)*s3r0 z$H$h!>#eU>hU@IXy0;!laqSc4;o=Oyb@fLY2S2Y4*WN*{R=#i@8qC-0!nLX2w>n(7 zde0Mo7Ory!^Cj;k{Br;L`EsF`>wko6biZ%Sqq?crd+X~<;ac_mL`lmsUso#^dby7K z-9^jM_W)D#Sifi0%XNIX_GDc0-g|#+aaoU}=ZTZUHTpT<6xSomJoeW8DdF1GdG%9; zt9Kr69Ij2BCr0nvHw)LgpF7lNt^3}2yj{4q2J7BBDbuz59LBu4PPf02_5OHnxXv2n zYSq1bjou&czoB`wPh22Tk-hWym~f4rbEf8T^mEathil*9JWg>PDGhq->xJQ3_kCM^ z?Jx7#%k|0)OLXAra>LhNqJXCyK_PR;qTRk>p=X8uRg?Qr<1to zB(B2@k-hcx#YVn*x$v(0bL20DYxMru%9r{Y&DVEU%vX!cdi>zvJYMe2iy0r?zoxiG zKNsB@u6-Godf9G$m9#AHL!;-fJA`Z9&$X@f*t@UZC0up?>djYc9`%1mNo4Onu_s)k z&)Y37`Fi{y*E!+ZGgx0OF6(jhy~%^awW*TciL z?z&fB&l===t@%>->ptf^EnNEoAin))ic5R#Nc~6miKmC_>_IO1 zBGt8hV)S#ESFXs_$``I@OJF2!bwWB98Q<@&$i+_yzkGi``u|~mKU|~d!dAZ2*XVP{ zhr%^_{%UcVpU0I%_Riy{!gW^qi(|apveU__c|4+FWG~kj!nN-G-nd53IbWWbNB&#+ zYT3=B@k(3Gmwana|L1Ux-naP?mlUVIzpq@{)qUby;TqjnTltdb?*WbM<@&c3^EJga zdd@le_ZA}gg-6w7pu1)Rt4>N$s-ugNtT$}nl zZGYkFeSW-cxa>3LYt?ylo)~>Uu@$a$-yezYUV_*UWSt*^rynnzrt=bR^oYfr|t?tFctDx6)q0ffjAU@RWAKU&pE#lF8SUrm-g~R+E+b? zv)6f)SM?(QFWykT#5)8ivX|>!D{{5+WgZU?a=kZP4@`e?_~n+JPRcu-l&|{zoZfx) z1K}Dy=S*>V&yBZt9zU~TuKj=_d%6BHT$}oy=)uC(JCFYmu5~{Tsjnl2t2bX?3D>^# zSKq7Fz5d4za(yjao4OAjE?m9y`0a2VSaZHsoyTL|vY7Ezx!iB;8|Lx$DU3^YF9zOQ zU&n@P^!|vWOxN-p{k3wTm+R^q<{Dj(*A3UY*Q5FxeUEwb4d?4lgY$TsaIL!EACvgk zk5u=MPTgPn(8Yz`ed5e;og07K-B-0w_oXn8hnJSc54~LX3D@X(qQzyM{#fDaU61F6 zYg7CEs|#0ez8(~=b-&LskGCud?d5t%xb|jT+^Ac_Wv7$oi%#OI{d(u|$t&io#>Hh^ z?`DYX&DYO_Yu)d^a8>T!eEnLu_GLW$@N2m2bh2gF-*~m}<$6)L_NSvb{PM?6C#UM` z;TlHva{cCt`I_Ro_8`}L!nLV=;zot5x9;B;uF>;XYks|-I8wNJ=kbH#TKDIV>hrn< zyqD`^E9PsezPz`UwqCAJglkj3KO}G4<@)n*?Mp{#um0HiCtLZ_|8F#m?9JCVR;;fU zulH=fHOTd^;Tk>Xw7BH!$)!Q>KJncZxmxq+JTdy-_vp7SW_+~1N;;OahU-yF%i@RL z`Z_LLd(vMV^S~`Tot(M&`Mz}`z`zfx`{p*_H8vT5GitFVP8`(RL*IKc@rnsIr z$aRZwjXpn4agDymyi>U5cHiC~u63WkoO9~;CwlkQ`-W>%>+$eleLXl_qklK5RbTeM zUF8>g$&QMfP$%Gh7GKPuk4`x9oJXHIKM1EFF8f zo)xZr8{&F{0Yvt4Jv&@`*36~8O1h5f<$7+o)_uNrUd178bljx&>f*)W8odwIe26dJ z3m77;PuI!H^_v^Ymw10$xO(&T+u_>O=g6by=r@Gx?2OC2tZSbzZ_?JAuQ!EjYt3Bd zar8aY+ru^boM!#V2Z^hG4kLfAleO;OA1>$cbi0w(8Y>n;3jPWPX1q_Ag&|@dGZ`@zcq#hO3@)Do52@T)T5YJl{+E(5?Qz zT>NQA$Mx-S9f&{I$r_(d@*`i~r;7LHG`#L9iywOP^&jD?^(CJ8m2=8&|GQf0h>ICN zUEHIOS=3kkJWac)`21)`ig%D9vX^W3id^{3;~f&;e)PWRyK|wJ>yF_Xt$X=OSs&$d zTlqRaT*qE@@dGaXw4;;v2v@yt%R}5Y|1SVPxNe#b>dWsd?8q-_*jM?-Md$B+;i}Ku z+D&n3cS0KOj�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

f#=b@{^ZwZyj}gP(IQAPn0i${d1AUy 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ðqpCrwoM<7@YC48A$1=|TN?bj9vR=EZbi?>g|Hb;K3Yg?QB*>WTVc z)z2?};yDlg%f0rRq}7jH|NTqXL*M$@k0+0xd9ixx>4)kSrcTA92a}iijPb1pw#5&<+uD-?z{N+zjwG-o2;jse!CuX=&ule z)o6WK>1RFT?91z@_}=f?zj)rye{J8pPtxkQ`OUTxUbVm7FFf;^%erDbh`+6H?m9hw zUPm=IzxYX)-~ahBo2ORwXKZLnc=}uR`v3O!xBi?{{(7a={@@WPJhjU-+q4{&v}&BBX57>!<yo~Af z(DlHIU+r(_!8I>#UpR3;t~#VmWG=p@zWc>*buoQ9iTJBR`g8S0`}hp2b=3Ju-_ILv za)Wk9)Q@>?Q_{!3)UW*3=RR3a9;RNEPhQ5oK6RM=qp^Ni^{MmI_3yaeaLMOyKmDIN z9Xv4|afSMzJ~5Q9be7H2pWEN-GcT<4?L&Uj_5HzrdQ|&lR{b5jHU^&aDD~;){uk%_ zA$l>rK!468nMep=W0dYkXISB7tpSQo;hOGjLxz8X;W+`fTK+)%~b+avi)x`cR)3%4h%9#`Ndv^!m&TD}6fr6nnn>`TMsQ z-0J7Pp)u%szm)p!qw_=E^Q7~}di2EjP+i=^Q9d5*=~i|9u;R({)Af3P`QGyvKmXqE zXIlpz|L8z|rr-L^1?3@~RpYYyy*}3gtGVsN^xMC`^Zi3#I<=~A{OPtb-QNDr`@#Qw zzNA-OmwI&_nIGlj!PFUz^+SDmJ@z3#)qP%d)?c4JwfcNzbNltcEuUBMk3O_6qzmOM zq_ZltF1?K96Y*fy#p+N#(e?6E-Rr?UU%uCrazFm?fsH{o_pg5YH}8i&>q2=*2UdCY zQB0j)JgVzDVAWUc@Aj|GudRLhwu_%v?fffkLUUGr(@np{&M`WPIj?%=N8_qr9**iI zkIyjcV%Kf|TDQH+SzCMGJIK8nqb>hwSTc5e0JT%{`aasLdpX-3t+#dJ*r0YIx z|NHjMxt@>eu<~0z{q}nBZGYOFSI2i>#G~uz>*1)L^?PW&9xv;oeddL!BX*whlfLu! zRcE#zpsDM_6Zh7^)7QgMJ-QWIr>ghzqy6TEl`cJgik&BC9RG-^Rekekni5`he#__j z?yt|e)ak)$-mT3qtKaL(>!{|e&TsGMU$k-Y{rt|4Z|`ud&$ra4n{&7i9zS#vQ%^nf zy|~w>4tsi4J+HIkd4FjCT37yU&uZ^9)i++)81^Zoub-b;ul&X{ck0oF>J{R_WqH}3 zFaeF*)m;~?c&?kD*7f=KmwxIYQ>*%pUu{n6!%uzZkvepL z+z)wr)x1?c*QF=l>r;pNVCu;y>aY0nke_0oS6%Y__RTh*uRQyNwi18vbA68wJau!) zS1~@U)=^Ka_}0TuUdDLYClA%3e1-h9p4TUv&s_ZZyna0K=sNm(II3s;3awMsd->6R z^TJBkKFs~?@&4M!J$`Cc|62R!FuUJp({CSJ-~8r+@(_R3xU7D!&vn3RZhr8SuGc4T zxcjbCtNQ5gH}w3rj`_qe=dJR|>&2-fhQ0o(?mA$_tM<42-@Rk;?+?EBGAVAs=4{aPrA2WZ`kwZkDLC_{-=Xi#qx>zMlt*6#@3nJhdT@frJy&k;p zV@{u1)b-=JU-Hm-L|~)s-OForbrh*^*vuE z&;3!?j|Wo^FY~=vA56YHtm>K1*xZ#4J{~{uy`O*T|9tU8Rljuo2^P=uEj>88zpYOX z(%Duxcby*Jb--$F&zJn9>pa>1_XjKI7V~_Di`DBRkL$|shrH#aiLCyjyR|#wzEu00 zE}j*g=k~vPB0f|Xk7BP+9ggZIkIy|F*UwM<_EUznwcGCX(n(tV%Jm0CPoJOk%|}0x z9>j-LUOh4Ovo5Z5tGvEMe5jtWeaKI9dVPHEkx!gj)vvr)Q=)@UANKs#Um=}UqxI(M z_WIOerSCl8C;#~Mo=+Y+wfg%gbnwKzb@24{F!k{1R5;4hU-7HFzC`mv^-71I>h}Hd z=eK`P*ZoKxukYlc{?YTqx{%JQa9Q2aK0d>0Zgqar_xk&=cfDe2RsYC_=Ab_O)OQ}K z)W z7sJ$*PqZG)dN0m-@N<1Uh?mG0ewvHl&;0x?rWW;UpV}1Y@+o!oS98hJf!0sGRmIks ztJmvOhn2qPMSi+opAUZXe>dlRo^_v3H3r?vZ}*X}bbIFsUe0U1%xA10W<6uNFzchS zepvOnetw$M_cd?-)uX3Y_v1dtHU=MI>dRXn%Dc~*7pI=NGT)2!CF-m4qjfxU)00oM z5BaHXe;;`MW2cn+ao*oF23@Zw^+CG)l^5%ylZX%1#XTJ5)cZ>bQP>s{4HK z56?VlYE{4XRgFQnI=`J4{I9;;}L3x;ZtBRM^@AbJ3*qfK1bp3wX-H&Z2vHIobHYGg!HT5gM&7+S_ zV(M*eOnTPZ8y69vqpNI#uE*_oV>xlGQ{=f$={=E3` zSG5(aZ=YHhIxq5mm`k6S9;CBsTvor==Q?0Dw|$s?JHKE0mPb#m>Q`=PO4jFF>eEfX z_4P3ImW{2mt^4^5d+YJje9q->p1=6>G&*?V(RK9oF!k`QH;TQ!tYC`HV+-`tZ#G)kl#Itor!EPwVmfiZ8V< ze5-GKT~osII7t2KJTZ^^L?Gkri3s!vRFF&p8=kt%eL;KAV^|wB` zDbeLy>bsA*zxBymPhOmQRX%xQd?=ro^&fr)mgc3c> zC{KUIuk!j5%?s62hu{2Ecm96$g2nG6U%qEk!kc@3>x1SKL-Vd0m(}m}xei#(&9B_w z9`8T#5BpE8>YJX~l&tT5rM~;+{7^TKJe@>*s4ni|C?5~@bgR1Sf)!7mpVoDr9RJoc zCyMWb)M+wZ?31m zLVSoPo{Lp~<})5$htII;;|D*j$FJM|$yrmY`X{bxO8W5Wi=9Ww6Wjmz@;R@1=B)?u z#IV<=t{>{}AwC@_59z4$Q=MNY-sZrGs(#UKtrL%L^kDUTsSjFT4C!nuoV!ks?>b;L zH^2BvmtPOvrTqhS>i#_#`w^eMSU+CY&4rIApZqG8PfVSxkH)EE-t5PNSrQ={i$niSm{-M`+n6=e&D33mEW7% zpR?NX_hY@TOMUmN=ePM_UPtO~ZA>4I>Y17aysb?LJo7~^>_P>65kPfW! z>WQO#JeWG8v3^+fRetZh-tgMLeDU;uUZ3aB(>xARUq098FH{%9)LT`&tbVUAucJ3F zKk54Y%Nu;Oy`Q(g|Df5wX5YfpcOTX1n#(?y$5T%|yv%1@@p`&dUzNA6I(;Z#@%V}7 zJo(kn?J>2edq0oIH@eXKB|OO2s*XnoR(bUbdwp5YxYEsh<#Y0!qx`g<{e9?@7r+0V z?{_M{bAMZxeh=|ih1Q#^H`>QFQ5?_dy-XyU%nI zQ%`^9{~z}5J>J@~tQY;X(pV{_rC^Er3W%VnsHm8rxH$-Fhzg39`hwy4Ksyvq=qm`m zcmNNmESC}*+bqpA!6;AUq0F>crEb<1jg_q>l_sXH%rf@p^W5VLYy5Us_x+4Hu>9=z zAAVfd^}Bxe_57ZD-s72bjyc}Bv32yNZpIZ)9@4G)@mx1Q@#POZ=}8mS{kZa=*6Cb; z^s--F9_D;_@~Nj!9#;9Po_sOXmzed8%?-0(tdE~!=j)N5dcZ_g&pyu~@=!k?>5d{D zs88I(Q9d5*=~i_2dgiNml#d51 zyN;onsqQ|! z<$lYb_n12Fi#+solYZ0hA)UEMcWz(u=j!koR{i`)zr8>9+yfpwjiP?bt!=WNuKPvb z_2`oqC*s3ue)S&ehxl`2eTlvK@vHsq{q`4J`O=BX?+e;??|y!4<+tnWq50N@)|;z0 z+P7K1_5ata&R+8S%Dc3Q)$LQ)VLtbWk@#=i=Oc^TMiMJ^l82`4e|Ka~f6ss^>Q)ef)Dh*2(ik9^d&c zPYagO;L;M~l-`ue3SLY`k-(T)`UVNU?sZ9ybxsdw&aYB^uKDOeI_t(&^?QA;16FhM zgP(MrC%3x%^r=<-qAQvbUH$a+yASH}Vs&|XF!k^GzQT%UZdmo(hy0`` zf517nnp)NA;E8+d;8lLpgQ+J^r-!3_JlNB%>gI(N&(}3St?TpI&iL-~``NzrqNarB z98P`ri@*5h(I=mndg__4;!%F`GOqM9@48^t#jZR3_B{CDEhkJ=^=&7$PJR5OC(gR- zFb|$QzB)bi#N^>ad~t=z%Q*YgbA2()KCwDK>Dk|J{e$J_L49~)I^qiT;foX1E1l6i z-BEt>M*GbRQzzr}+c|TCFSnhjzF&MkgngNMUJvbKb@S*GyDo^oZd_Hr*XKH5H8&l8 zianp~^5y4Dt$sfK!S;6W+3z2uYo7F59xvyu^2zJP*1=2Nj4PfzqzmN}T{l0i=k@OR zJ)bbOxF7oQ^vgr%f9`L6J*2ZL(qFY-->43sq5eeY20!WZ>zt#{n_ARg`lP0SSNZLJ zrQha}r<0g-s%O56NBMZL((C127sO9p*Ue9Len0%STc=j_N+;LBOQa9=iJ^S}-5kFTlgxjxqy=YFOKQ*Yhz+;w_< z*VW_kg`fER_{{c)O_!>^_rh=bccY(=t28F{nnTB$wNBp##QxueR)0A-2CPzUHRR2kI%1KJh$`u{Z_g3AzxnuIPTMk#s&4MQ4(q8`sIP~qhhOQ+kMb)1+Gi3@N}sR%OuxVRhzGP*1}=o~==RpZ(}yok zRL?$fg>bsBmdMQ??lbCwy znXlqeJ|3*}dU-y>ieG)d!;hc%%iB$@o?i}bZ7gy`3L;5`&)uUUXb*g$VKiY3z*wf`Fedoy| zZuyd_)%kw#&TS{!kNEV(?w8mc&L1xa>h$nZFY{R!CsuuU>QKIi=El>Xd39Jl5Ayp# zKfGn4x*r#u*%ayWj~?`O!$*1i^qU^UhgDv^hgH8k?CDf>e#45#7k*mT&tv!6t^GZT z?}siryeZL5Kds|FW}UwN3hAsHtq(`_%~|o)Va2oG_-PJ)J?yfFO|9<7U!B#I=%%06 zv469!&$`x=hjccJ^i!wGSG*qI+%R>-)&BPWlY_tTOB0pf+iu@FtU@S3k6-+>uJ7kRe*PI#tNQ5q?e)_BrfWU-Lq0L*RnL4CkMi+grPs^z z8CHDf20yLKk2gPg>(r`#MBDDy{{1}vQeQv6)y*Ssy+nMdF7Dwd9}o6)tGerg70-3^ z)4I19Lv1E)Wd`76{b$bqX+Ra9?k26`k?P4`7Q6? zi~js>&ze$MPrX&Ik51yKo_TvnzsJk^XrFmu>WG~O{G{(Z_}d zy*}3gt2sSK@RKgTKeJnNO#Q+un-X38)c5tlZ*{TzOeYZ^R_mzuP(Q>ML;1vBy_!G! zTsJ?}-G_fZ;`C{0b#vf}ttYNfAJiv?@|Di2dHQqvdwu4GmA*PZ>3Tld;dgV6FFham z+2f_(`p*BQmrt$gKe;%+oxeGUKF>YQ4?Okk7pt2$`{a`+4}10GSqG|DnEGN^_0i|2 z*g5l@?=8PSmQM2IVRe4%>mi-FNOx{u^5^RC8CL!FU;6Fmu{-}C%b!2^$$gs=J^M1} zO~3K2XaCD5rk;A{dvUK%9rpC9I-g<1<5&94k1zgnJIVe0kWS9mTPOSUSD1QubSfO> z=}&&fcs(>PtoVH8r}aD!-r~jkOs($6(I0C{cRe@AYNf+%W6( z)%l5clLg^^Kl7C7|J;w!pT}0`x333v>m`~Gs*8I#%EyB}-Kx$^hfh@df4BR!PCWOE9`t(4S9yKr zg7Of5-MFfLug`VBYHoh;lP*gmNuXk6xv%Nst|CjF7?hsw~EA@SS&{wzb-A_8u`uLm0)XTW1 zj}I%p`C-N5D?jP-<3sOnC$#!aFKkM9)%nd=e&d_ReUeYidDSyN8dv@Da8xgOt_x;e zY#n}z?awEE`ymrm-M<%&SNSc^?_AGZkRHTeH?FGR>vJ8jnwuZ|r0egCKYQMhQ>*&u z=WDuO^sR3m``>zr_)uNk!%=?nGVZOT?z*6SB7J^}y?^jcw`sq5cnbp;0(kT?mQTbR z#q7_RPUf%gIM?Mftk(B+&rdr3J%JscZ{M8bOXVYGEr@f4IZcs(#@CO^L4Oo7DGwGwL@z>!;qj zV(ZM+>-DL_N?)Cyu6LJ3!(-m~lPk-j;#@5l7x*1nId8iM{C;GbQ zr@H((H)sx>ut<3Sx4HSJ&wBh+mlr#S;;P^>9>=ZV#>3<7Iuc&%7{o#P%6K>HB`?4ljD(L{;DNiN>I-pB~Kf z8Q=Nk>&JTPVmeS=Jc_+ObvUYiO;0Oy ze)RK}{Qm4&jiHaPsn1XQSY6)zv>rV%KCJTU71FKhbYb$UeDbUVvo3c1>9_M>$G^YR zoa*_`XUN|~I_bCl{ik1h%G9F1asQ^M&+BdKx{r8JKKHjeOuZ_fyeh8xP}Q(E=TSq(>NBhkS zt92^Boy#wKQ2XQpesBNLrbxG%$Ja;hswbwNdggm^uTLHJ^s2h+>*?@?pZLy`TRyb;?dRhk zzof0mH<)vHT~McME?-w-JaOvjm(N%}F?m&-`MEmghS{GnKdtNgoi84}e4f1HYps(% zd`mt19*?fr>uKi2saNHbC$^5bLb?!d)VJj8gQ<(3`U z9d+?stor5QTwQ$gLit4dlb_agA5MPNou(G|h=26 zVWrO(e)7-XOYQhP>dLw0JfGom^}5LZPQT@!^Ox=4C-MFMlV8<(bKg>Qzx3m&o5wkh zkEf2Gb@@cRDqq#BbyKH`>6#njXN=EJvDeFk?{e_e>VACku*Pt{cHAL@hp)%mIJ`TI$4eAz@**N4Yx0%8L;ULt z=dRP^yAD{*ZJ+UzuKX(xYR@O$KT8Ks+*=3FzHr{qNlZO;I&-n=mxpt8@y!e66Zy(d z>w5pm8{WKSYH>dtn{6m{N$ha5B}Ui%imva zJgqrJ7p8vtjpubz-FoUU^{RaG#G`yXsDCc%hrRyv+w0{i&%D>vs(zEjLBJ>er9MCH zV|9H!#9tR$Z?4{GAD>~h4!`(G-@jM)g}W{Pe#gI0f~PCKc8Xhh}HQ?-}B(MefFNH z?uR}+F&%M*`g)jp_?52wC~sB1K4?Aj!mNv{{mt(){&Q2{_uhN7PV4-johRmY{iE3H z<1_5(@{_Lf_pE!KJhl3H)p4Q{gC2KR%S7i{^z@zx~Hg>v|qM`~K%lt?F0& zV`I>@k5k_{L!Yj^uOE4O;?z^m{AgVD%Uc(Y>f+IX@`?2MDRzFp;(@zPE$aI5_$LqD zhxA(?G@lsKSvRh#-|KT7u$r4M{G@At@Bi@Ar&jf=zSorSD!-jm{Ix^xopp}M$- zqx|G$+*?PT&rm+G`hK3@uRC%1`yKr`S8tu{(_i7J9^DG*SM^?gwBNk2(zOrmU;56I z_kXiJkf>kz&ZcCa;?oz~_hNHE_gS5udg{sJt5=8zD;`}~@#HH#{Z(DR*DZeHyAO~0 zz&;aIecwG=CmvtvLFWY@<@NOtf3s+vRdq)D`3(9RbFHNmJpO6locyt|oJxo1(>y2WsPaTfxCeOSu>tgHhQ|$L?JN|vC<`iG}mdIzQ zo_*fWko()~-Qzy^x~WzDlMil6bb9AW*7cbS%0u(QDz83@sUwD?I>~b#FzaIHEI+O5 zJUROl%deN`KD%||xnJ}k-8{d|<37+yq_bJ1J6C6JpX-8EzkKEQVJ|v$8dcrg`oz`| zSE#RtsfSdup6Uc3DKec6_#gjelvUmv-@^+EHAp?R+_ zoV!ks?>b;LH$T#E_u-U#w!eH;_wzxzeh!lQ&I|hZV)xT^sEboiJ@ZvO%EyD1UN5g7 zR(#jbPuJ_;6ZpfkUo^FPKj)A9NmJr8K7Fw|eRZ*~i{$6L>iX!&iz}pu5Ao^Z$tSND z>xY$&eaKI`o(E4ps=dM9@7q54TTKa%Z}cHu*CTHp_d`q%;=?MhK8mT+i$`@`2dw(| z!cXfvPj-9g8B?qMf5hK4CA#>jU!C9lug(*?saNHbCm!YFxeiEY6zM>HsmB+7s{47> zj^7t^?%zf}!-{RcrQgnz(@tA{zvgB4X-ah6pVW6B>8s=GuMmITXuY|*y*_o=(@($s zeDH2J-(zZ3_wP6G&3Tmi&ZG3(Jo0oBQ%^nfqjA+Q4@dQq=el6l#a{3DX>96=zUSFblp?d1rXZ%!O{IiQ2H~(DQ>7HLkzu)nC zNS|(^`7&?bb)$9W>h}6v2dwn@!cV%+gSY>;<>!-kFMeN)=j$W&oipjTc{~r&NlZQU z%vbR!9}iY~y}av!6`x;up78rkw{8EPm-=pRYf9;3>hs(Bu1CyoF+FkW&5f;>eRK8c z^!n8Cb6s)ex8E0^bJP7ND!=umZpL_tc%zv8b7Skw?ZY4K=Qq@!XkYP@zJK5K#Ahx0 zee-LY0-k-Gx?W!CSFhub1aDtoYVRznv$K`Y-MO)DL)2Q_|<_ zBlVpZ{Kbdn5+|mfdge#ts$U+C>Lt(hz^seyPkvh0^Y^YFZwl&1yu5X$kMv;fZ+Y`r zS4>Zg56vYW#q1L&(vug%YObvJc=j^pspST&wCaI)8!w1{mz-HjtA+%)DuH}Vp!$X z=VIz*OwZgU-#Ai{-L)#b)u>tu=quU`(hu{gT6j` z``dirK+9*0koem;K1ZQ2SwU4G-4i#}9O#9tR$502{M z!>Z5xJsw~AiSPTJxBqn8(AOX?7R0lD#`1}HqnQ1x#@3tLKibD{Sgk{kpJM0CX&0{+e(HI>rvXq{DcM*H~;tM%+de$w~%JAePY=Dhmnf4(Wv#ZUdpZ+-5AI5G9Ab&^-b`XOE~ zu6XjWH$R^1=BM@T?;9WXvlCT)mpipiJm&*FSoy6l=aYwY){U#`_xkdBs=0f9Uv=UM zQ!Br3`iiDRUw`UXe(Q6e#EGf5**N{mxYEai6>oHY`8v~DT zIfvI__api4hkRn{sb{{5NBMZL((C0%{q}mtPwTo5&wj~Erlq`|S{F}jJ#mHldYF3n zm9G3Kug9OOV_sP4tMk)3eqVg?6?dLm{eJVphczWU_lrKXuc5s4tg$ppVnRcvx^(Y-)+l8Rln??8Us&1Jt&`k^ZEKdzs+GzaiTiR`e@8&SoJxl`DtDG zuRp1M9*6JuZ`uC&x@-Ua9e&c+@9RU|9FXt$@-X$pP@g!lS5JNwS2~$DH>~vd!B0B& z-+t%adTMn)Hrk(;bpJg@_lv&u&69px7vjSzuU=uVPaRe|y}av!72iJOr*--LkQY9D zYWdLliTd@y1pLbuZr|n?bkP|!)K^Jksd$Ae&2BJ58q{KQQx|IQ^0e-QrG_N`E5RW zF!k0Iud3hca~-gn*Z$-uUC+Z^F_ zu;R&A>+6U3iS{8s>3P4;B?s&=wWwcxU{kSFhua|c{u;Q!p z)4KNOr%r5tz^%S@af9^Xr@s4W->Tz7I*It}M(fPg?e(d{O5eG`Pd<3PJm`;~Ftw`h zxmRQ0Ige7m@|*vj&*>zlo_gkcaj#Du_VlVcpJByw-TbsJzn*Yq`+>JQ9XxSw9Xx$K z9Mz-SL+kZ;Ss(2)FH9YAwZHj&?`wW;qW+-zJ?a-f5=Z;_3{yw!oaQHe|6ay_ddG1S zRsHC_TL)fsp16;A=D>G9)f3GJ)x|v=<>SGgZdG@EJsr;%{KRjcQn0b{`>+3nX$e2C zJ^1vtA|C&84t_hY@y(@AtPWFe-SOOYdVJRbt9k7+e$wUFy&t@FYIQ$6pWyK=_383k zT^}@`7~-!RSJm(Jxei#(-Fv+}?}^Lb?>zEuP09MJ`fV;ciPgMw^K|F-;g9ya9;iRD z^4mFci#wh@wbZ7%bQ zVdgW&>mi?E#djX?(|Yd1sV_f#YE{2taS-6?PksIAx4s^x-ezO#&D9z0<1_57!%zBN z@BZiO+R38+)yFj@yvlFqGr#f8lX-R6n|HLnS-sJI*9EKj>_dLi_x;XSpLojD>iPVp zUvEly)p_DP;;Z$=?x*?aLHu>&s`|Y?*8!`!`NB`S`kwtiZa1~MAD4f=DdAQ7n=XH? zFLs~kB;rGLaSuoNc(A8i)%gW09$)xrUGHam)?TMgt?tLe@86W@;-@}c{;K2auMmIT zXuY|*y*_nV>D!>QISXWFBrrx^ZRrPy)t^-!{x^8~b^?uDCe&|D|R`pAMuPN!nr!V&0gQspD&x`nY z>iFta9xqX!e8%#0)uDQY$)f|SKIb<->9lugZfv~taqaiT&Uc^Ji>E*J?R)Fv^VRd9 zd}8XUXTBHr`m&yJPhUN+Q$Er4^V53X&vwMSwoWbX$CE$O6!0p)d*?~+Z+bB2tn$ed zkMi-L{<+9+SoQOTpVpQCq1P|}eeo4XHzhvf(--S^Kh@16k55k>Uma#%Tp_*e$Crn_ zeAdO*fvJb*y3_9)F9^4~{PgMn)GxZC73k`xXC3E>y1ZCjo*qm+yv&Oelb>}l9+a=J z;+Y#({a)AjNzd!$K@WTW)T(~ORgHmXU#5QU@8qk?C#Ify=Bs#=j|VHgUY^ge$LFVY zJr6$myU&_h)wkZaDdCO!ZGC;_g7Of5-MFfLug`VBYHnZG{G{vsgRlC`AycdRaW8I4 z&IL%{dUVz4s=H6}c+2!@y3l%a^+x;n414SFlRm%y=~G8dt?G2}#G~uz>*1)L^%Lo5er`OfV_sOT zY1B?99%v%Rm{ndFQfArHIJW2EWq?&bi-S31eZgIOPq^~0)Q(vVRWbdn(}h(%^BL2xbnw;rDfW8#H+wC=|9kZF!uaie=6cqt5Pw~0ow<6Wef);i zI_j0*JAPiZ>|EtPAK`M>b&+E^{q}rvmw#-Zuc?0Y;s@i_b?#iJ*#6IX(c9nVqX$!O z-SOOYdVJRbt9kjsPrA;7+dl8msa5^c-)KsB&g0at{I(u_^U;H;SLKt}i&Li;_w>|V z2b52&_P6|1Px`n2eb>rwe)8A4uCIsqt0MjHS3jR&wZ6Lji|6(77O!q6hWbt0+uORI zH{*V#-_~=VtS1loq%N+o$LrNMt7~r9TaTZ3_UE6yZ2A3~=N{4~!>i8k%5Qx+mpsht zuky(gkMfh3F})tT9$4|!({KCxJ6ragTGfv`uPK?!K23dj`y1bS>SA@6dh3c;)$jGW z4p`0W`xSoD_4~`qw?ArXRllhHa}FDPOnv?KJ)S&1zvc1N&8rUO6V+i=ukv&I=;$*y zlusUC_=zWfyMva`6F-lo%csQ#BX#N=mPj0fc_bRAjO53?@TpME=MKKj_bCaU_oPi&oZ zy{^)O)$_N$^jF?IbYPWNudwQqPn@fRZ+<9GU#!kgb>Cm!<0JnuJ|Dtd<{rgfU)D1o zU5Agb>hpDq6_z)f?^OGpyF(2S4e1zvdfu zeEwi%-BO;?!RSf zb>7g)^{uB~p}rob9)6{p`B6R|Or6o#ys+xyD?hDke;@ph%bz#1{nbqgueZPLXX~3s zADu*es4kw1Rlhu(tBdctpnM{~bANk2dEoO}hx!%wZn63Jmb%W9^jn{~pgc^yb;Yad z_xfB1tmfqlKk4%ORqty*5VZg4;8n4FqCRLoabotbE4I$4UXSlOV5Q4%e$w@M1z*^$ zeFb=a@$aG2#ZP^@xxdBED>{j(x9-?_qq;r5I;?bEKR@}$uPe80om%aGI(XvVI(Yhe zII2gtLhDrZUVgOSys*;cH$Um~`;kv+pZsY5zdC+CsGq)m=eoMQd8)j6>gmtCI5GKI z7bh=cJXqDu4e=B8^Ha?4Z-23!C+^49do%?+&(W!?pRelrp!viQf8DsMey`7Uz-n$k z=jJC}e(k-n|J15}$>JAne8i_O_S}P)bv%fd^Qyzt5m%Uc$*XiT-|Mpu?Dg{#k6$nQ z>Fv`}>hF4aTN$s~-|izH@y+AB5v#+zzIDZ`>i7Cw2dw7h3qR@leZwoBcF(C*ef00S z`N?nVo5#A=OT>rj;tEH3`thOsTy$Nq>bF1nY2EfIj~g3r{M5syR`p}P(v2y}U+ZA%I7(w@&ih2RezVr%q=SdwuF~R5y9%g;~#-pVoDLzj&wi zFYCG=m;ZHR(8W)^+~0Wiv9BL_dNB2>eDcI}p?o4;nDxG^?f*&H?a%4g zc79vmT+qDh##QxueXavmb2|_CN!R|o^Zo}d`Ar8;+*=1vUk^w1=vHW*s@}_w_L~>> zbklFo-(UG+`@9)HAOF_jZ34Q^`PBD%YhLJl@p?)J;-y}dPhKy!4qobJT=C=~-KrnY z{^ln>KOV5h%OZ*wD`J?M>7t2!M#ac`aExes&_Q%{{v4@ddQ%ec3Wx_O~|qB=jV z=RO>A!{y%>e{%6br+B0L8-EmY&dtWwovSn2$46MLuXor|1Y0m)$jb}C*IDBC2sIXFP#2Q{rzXO z0v};cQ@%Tm; zdL71t=FumgNC&EmN3qwJ^^8Z?ab3NB=LSFV<)3p|`+w3L>@57e;zWUhK$uH*sJ+X6LtZp88>(NukSBLWY z#fiz!x)=}2_b}HtH|+JN-#*{xT_^td)T;i$g`oNPn)>cz&ZV!1skhnKdUJI~`}hp2 zb@;_k`p%QjKjX})RsGzvn-ZRVnfjyWxB2M7)LU1)s(!C8ucMk5kDqkgZ{;^OZg<$N zrdD-2cvUQ)Xg&L$PKDV&8q*u)SG-Xj*99v*b$-(K`<=JnZ29kt>EMa!h%40B!%@9z z9eL}B=i+F;d11AVulw}d`TM|wn)B*MAKsMAQJp9H?PKffGnY6Kf3rw;Rh`j(*8{8d z)cHx@_bZ?Kt#+Q=fPo7kJi2tm73%9@>fu+q@}s;Sf3A*sVWrPkep<(Q@)r+S{`vQL z`!@!j*IW9~zQ=?1`?c#X8q^^jSmo7wSoO=po=#Qgb5F-}NbYa>i@x)yX$f`frLOhV zE7aG+QN3!N%v&$>y?C_Gys*->&-h7S{)cYVzTfftdpdaH-a2^t@WqMh*(a`$PF2r* z#jo<&FE%ffPaUt9{8Vp$MY6H+h#zh5uk!QpZS9}0+t7zk-+7@fRyR+cH~8xK^r5;K zFHs$4U91k}Vdks#<)QvWe(;n2;@83(J3eowImVatPt4~Y&$*H3iSzpgUv76qeZOZf zuWz4P7kV9aKjqD(Pi#(zziwPrzt`tFU^O=#eu{r;(eSNr96$Y^`ek=$4$^gBQeU3m z_ZxbGibwf)u+r=0^}~wK7k;{4`~Dff`m(9@e2Ik+9^KwLc=~#ndiZoI z9Odb+_*GtCqIscu>ey%eRA=u!4!`Hr>hG!E@vf$1-$MG(eN?Awo_u}8Q>U|8v@WEV zu{_K`|G0JX2hxN3)#-|@Yc6>)okTjB&$!}M`Ro_t zC#pkpL-`8%NtYkr`_Ut(7U#{uZ*B_7OWn$E^Vr{X5>rn-^Hn^`$Agt#FYme_ex=J# zb?3}U?^ynQ@kK9bN_g%IeW>5Ql{b&qO*)DAP+dHVy*_m~s+&C51+y;ZSNiSw`(t}H z=hR1^2XlX0&pdPe)<24y_0Kw^I<5=qPjubhux=rAfEa=j&BNh`cpSwFV)Rs z-_uFNhw9=Uj`HzfPq(VOE?DuZ{mt*+`TUEfR(|jI_f5%smEYC==Ck`jCo$($&wMZL z^{K<2URCEa?D6<%UFYw6zP4>@RX=dW_s3eFJ~W>g;;$Q5)$jGW4p`02SANo!-|_dw z%{jhQKJpn>JYV1GxA&jiZd-eDQ{TG#^7_uB)OWw=tD75|PYm&|FPythkMBBQHMi$r ze$sV*KlN9Z-*3O=h{oU}e(Jjq>gLerd{>vpOT8+eJbF+)V|lt+m&fZNJ{_2Oar)i< z)ys{IcR&7=X(`_iowxY&DSCX(d8++wz4VtJOuco*qxz#fbGa^9^}B9<;`w{2KYHo* zNz(l|_l|7^yvlE1H@Ut(c_RLU`AOIQ-20(tOs(pdf4(X8^qm*< z>5APad3rGCt@6nekMi-%0qKk)9oXyRr}g}P{_6J~HnplBxOY>+tDe8*`D=ZB=7RDN zf8DsMey`7Uz-n&$n4fg{{n)=bbZYhc&D}oP6zQg)*2(=X?><IO@0knfu%K+b{dAe_V*-o@l)UZ;wzp$=ZZKn_4H?c zG}aIG&5iXX_U2E1`tAKE?|z*DZe1 z^?LcDyFYDebw9o{`26ksx4*4V-+WLW;;$P=^+$Q;a~-hP&rdvlKl)*hnIzRe`K`vF z>pZe9bY9Sd^6oPoh(`xjdG!jjuZlCDdcAsY{?y|qUEj~Y?&4cdRQun*-@rF~dSZUs z-}2_lJU#W)tMYizIvHnPJ#{n2OQZ+&L3w>*b$-fwz5J7}?=iKwA3hIHA3j}}e#_(Q zuMi*V6VFBcQ2*RmUt(TI*2UK2r`UON*OT_0sGiUFJ-8{;$EOGFSNk~g?uUMQ5Fb`~ z^~6y=9!#ClSU;@#JV)@;^?RQC+#B|qTHTKW+T%+1^TPC}-}KFuvAlWJ6V-dDui~l0 ziYH(3%nk7q?L&Ujv%jy|=`~Y}`VY$Q=kq#USLS>1XdjkaKm+d(F^&V1_eji0X1`Tgz>H-`6z>&FxK*1^-) z!_>p4Q{gC2f5orz`V!3x)l3 z7x!?Kj|Y3YRh`eU;<;{qTG#&G@%&vmx18rQT&`Xh@iqPSI(qwe+;t+W>vUhly>;;P z^>9>=ZiUvV>b?AEzjapJM0liFY_;YE^&X;*EuP&LjHJ*N5|3USALKA-=eWqx_0j<*Rl14DnKjul!V( z|L?D8|6ZM+XH`164qhUCs80;#vwvN&bw>4ieDlIem#_S!>+fCv=lzz??^j>Z7dM2ci|Io7L^}Kw`+ok!4;(SIxF7oQs`K0HEg!Q_e}(vSk>1?C zXNGpzd6`H5$L{_WK-pGHyN_JKATUgfuQhTr(s^ZmPg;%2X3KV5O6I?VcL?7Cpp z*E>&s=$xacR{Q^xi-U$PUsK=x;-5ul_OCi!b?eIGtD93D$|vfB>KV&Jx=>zU=G7ryqU%c@Kln-CzJ1V3 z+8;Ei|NrIlzxf3DB2G-dvo6NtCtc^kBe&gSqT2t@U2H^ske>dzua|US>d}>lRbCxd zyv%2;KQVdchRGM(_xz;uV++DQfAfOr|J;w$KGq6!@##VL%hwN{*m)#R57L2n;vV9| zimwjwGM2B<+_37G=ckw-uX+A4(-P`i@7q>PUd~~ETVGwDJd}s{>&8{}dwqEw)!cmH zCtdlI4s2fm>U8kL))QBV=j)12A|4%eafPEi{rFIRE}9qi`uS;H&nI6w=;x+Z^_}k1 zl<4B8ex4`l`1&ivUpHDGR{B}bIQ#NCD!zTjPdwjWKK!;@Cuw!-;EAm#u23J;Cx-Hs z&Z>F(bNhRJ=7p6$9e#>E4?gnj<>&AH7B>RV{Yw4b{?=b%>fvR+7x((qVNb8B^BGn= z&kO0d_s8DlAD7>Ma`d8LKCi2(KkB#n=)u%mcRY8U9^ZAqYTn9kua`GEdHM5t-_w5G z)BSrX``(=9$T;)r)x4X{ud3hc%j>A-#LNBd_426OoIACuA8}P%Szk4W@6Xkt`(Xde z(}OuLUgk&RQQcf8<7z$nJ$|*no!`eEwEMIKzb_p8_t2d)^v!7=m~+bGuNzm@@AbJ3 zSk28>e$w4#(eS3HJ#_j%-+vzc?N-3EUsK<@P@VtQRi_8>*Nv;{_xjXfHMf1pPdw-M zqaM}%AkF~>P({Fwp`V)Ih6!q^syLDL4KBWVlM|9-PB0f|XS2)Vk zj}PVNqU(ZHzw73wb@}x#pK6YIzl8VO)5TAH_krJdP=AG~x31WFbM<afyxZt#;I z{QlmbwL7AI#YIhtE`I8}k9djse4h2x+iXmKuFhy5Ugd{%ec!=PeD6Qm&UU)JTJ`PPlA>i7E8U1v47>*goE{k^f%?o+G#@yY|65`E`I>bs9wr?0=l z)LVCK-BH~hUmf;z`N*AiCdXJ9>@hjw~`TYF*nmgWWYEi%G9hw4NNLRo6mDjDVFJt-C!^^z5huM#x`Hb-r zlV@(2e6fAVPdfZM;hXITg6fAJ-#YQ^XL?XSzvay%PbU!{s*6Xl*QX9gb(811VAjRX z1%6sr{@ld}D5>ki^V}y7E5G&ikj|<||NGU?XV_bxpLpI6vCHGz0iwQTw{`_|?bEz2 z=W*8QC(?tdhoAW>PJYJvGT)2!C7K&%UCa-DTF?2t9`nQqKkdN8r{7t_XAFIA` z@y+(0TGT)Fx~4?eK2BZdk^8A`J?D+N=!x;6y0}99u;R(nh4OPzKh&Rdi1{hz#X#V9X+RtZLt*6dU`uzUI+a5f%s^8>B89;V)^F@60R%U6gG zE1tPw#j_9jN!RP$8}@q5)T(~T_NIha?QiD=U#)K*eRLA>p}Kf3R{ipDt}edog7S&3 zo1fOTKhM4O1Ev-~KRkHxc}jHgQ@8RPUw?(Ex7paba8wT;R(*LL6_2m{r0ZNi{|T?3 zTHTLV@6nX-+^^K{`K{laVwigCidWU|^|=n%o0p$--G`sQs{MXP{gM|pMZBKhco5(H zFrPZ?&AV!SRDYCbj?wGDvJ8jn%n;5CtdGH{_=00G_|T9 z@P|!_E~KwtUJUtfU3q+%dU*1%%B#bQClB#^n0zs;`dv3a@tg-g^)H7_DfI(i*%)-I z{jHzB`25r-pJ)!KF7Dwd9}o6)tGerg6|ee!$M1Kp{PE>~pJdBDn-X1q(l>XWC-P8# z#`38rhWf-6j`HUAr>?nSrN=M(myUh^haYY~x$=BY2T$Bv2TxxQQxBg`g`+(E6~D^s zOEfQ3PaXT4pX$zoAKUY&snz}1dyl4s=f2Q~p5NV1d414)Vu-(PTvfl<=Q?0DH^2Ew z*Lm_UAHK`fs(#ht_r-WS{BF+j8CHIK-Qp)7{l551SGLcGP`}`B+a1EwpZe~j{hfSY zKk|vGr=Iy<-0M?^J-w>VXISz0!B6Y*Fci$e_d!@dKt?n;=!zo^E%`cz25PYuAf)^`XQ%H zE%yI~f6^4_o%vxAD`u{Qlu@-E^X= zZ~dFriN`m3kS>4a#l9b+lZd}6())h(^BGp_^M#*y{C?&K+dHk)pZe%_1$dR;&Tl-| zZ63ah@gP2|^6I0QI=y&QH?O1WbKU&3e)}ox#>PFLxcq*fOAl>Ic+QQ~=Qn+IeL0^z z#9tM^-#V@b_U2B%?fdVYcF@$SPA9KtbRB&?Og((-k7BP+9ggZI&%7|}V&^D7t?N8_ z(C#Ns)VH_+i$**;8OtZ)jbip!F}x4y$^VpW8=ApShuY^7xW|+uy%^&GP4KUV4+J1W<`_4@5Me&V|ik3ZqeNm|`Hcw*~`E7aG+)WffIt9-97>ls(NnKv)Yx|py0 zw4Ud|Z++wViK>3t9oikDYai2t^6n?TbH?kb_0+|5pt`t+qkKHr)2-_IVa4MMKV7f$ zWUm`8Kc5_MWmBSypZfgdx4PIlq@Nzdhj`*q)CaRq9#-|tXKZemb#b-7`EkHGx0eo0u;O{$;-_`J-{)`7eazIVe#xFq3D12={c3;f zgXR;%)LU1)s(!D}b--#~e({s8bLOIJ+R37R(Qh;*x_GHy`K`}=ww}B=_0%&z8dv@D z)`g?GcyyqAV&%8j%bg$d^HU4If8l;@B6E5DOWn$E=ZSNSPGah*XTFL@`FOC>>*e_j zD}Lp-pPzjFl>Mhx-|z4E_cH!<^qxYVu_V>Q`ZOV9jr3Za| z(3i*e{E~Ch#a}nBs^9B#9k8044nM`-KX{YJpE0$n@ARSOAYJ=3_4&zfy6Lw(J#p%( zXTFN*z=}r~R=i$bKg7@V?L&U5d;UJ>oiChP)!%c+rbri`K1`qSdVbTT1FO7xqV-_b z#dM+kT-4uN&vOJnt>gRoxBWls{rQP`V9uF&^Q;T$&(-Voxei$A`?}{hUB6Fz+;i?R zwW@o6EM502^?lvsKG$cTTTfn`dR0DoVtgo{s1B=o<})@w%(|E#{Io8=ZgcnT6IERw zp18M8@~lfIG4<5x%*Co-9?sRpH!qY=w9oiyU9XqF^EW>;wWuGwb90I=OkMXYeYKvi zE9=pLsaNHbmvOI89cKS%tRL#j*md*M^?UBv<)h30-q8ggZc2FkqYvrwTi*K4Au&CO zziwPrzt`tFU^O?t(r@SYj=v|VoLkQG87^0^i|oJj+w;i}eWATy)8DUMbVciRzx^EK zI>q*Xd7h|SPoMedL3~){)kiUPdhw{P>ws0C>*lBRo!>h?f3R|HInQUfT>bw>xxf8A z$@|{g-oWPj{mbstdfgZ2Q7StB?Pq*wE^%V&sb{_y_xjXfPp_)G9$4{w-SgAB_WcWf z_Xq3!$mXxEH}lEQIQdn~XISy<&uV`k`Wo>UnX? z$xRV&)Ngz~yC3FLht<66%FkV=$9EmDn$vajldk-GzO?-N;?aL^RDX4zSjT)~Sk1fH z{HprBKGy+zbLRfG??3;HGpAPdbK9TubpM_tUk}Y|9{XIZ4tw*iT3=Pa*O%8(&49_OX*Pj32 zsa5^SA8AVFs?Klu^wm7o{->MsR{7**-0M?^**_Y)E?D(de)H=QH@xT6%I`h@ot{ss z`Fr_QeVp6JXIS-De%s&sZd?9)?mdrbS4hu&$?Mi{f2*5Eo=ze@R2R?1s$U+?)x~#R zP(IN)YyVo;&j*jc+m@+C{jz^*47|#3`SjI!?|#rp%sJIF--~;F>aeF*)%gr7Ugfua ze~-)BZyxylgYtRnqyFK+%BNnQ&k#R#?LU61+n@h%!Rw}0^*6U)uXUdX?O*!2zI7_Z z-z?H!RcExH&#+pL9zVstzufuDkDFT6>EMZb>)`3@;iw+n9$K%*%lc@ad12~^>G4zS z_m>}f{*x!Fy5|$R&K-KN+TZ$|-n-|I_+K2qK-r|_vIO@N(&r|aA@gMZRCvp9* zQ{2NIpU<$T$4|Px|NDzw51Crsk8i!URq&h(sqa4KKE_w4lbCwynXlqeJ|3*}dU-y> zitqaQX>*ZY+toZ!kr**v_ z`P5I}Z)#QFd)KC@52ijp)y?7lxF6O_Uh1i5zKX3!zv9t_6|a}K9>h;w&)NJmpXb3Z zZ+*hls(#$=O#zR8^kMp$e0B4wLprd^tB+#p^x{$7ypF2Ra|l1J@B8^z++_LhNw!_w z81$>>LHEJ^#h2%|_0+{VuX^UIm=3IXbYaEo<@LjgU+r)C|LeWW`}@_8Z&%_>r=Iy<-0M?^J-w>VXISy%bANmP;QxBa^6%5OwSTUp{Cs43_Ah^3k9p+9 ziF7uL)=ix%kC*j~@nFth@#WKR`|s8#wO7tJODq=9m+Ofu)Mq|%qB>vJ8jnxop^{J!|uW-!0saY#Gy=~i=iE?0-u{-y_WUj3P`;z}p^bYWG` ze8$$Rbnu<0{KWVElb5|>`SX3wJHK_}@sA#KUeK2}m)B`JiTF@m+`~~m9_;B>b=L(e z9>3CWeqHc?ZkbxWo{s+eB))FaZ`Wn6jOER%4&`B$U$x%Tt8~o`@e=9tQ|$LUuXyfV zrxy3)q$8UGp8J)$?$@Z_^kC|3HeOYCw2#lQnpd5l^gVw+=JNJ_d-vl#XSEKzQNNS# zepp`}=DbxtdA&Gw#IV<2)m;ayc&?kD*7f_Hw_eoV@1y?J_U+HL-_PTxzSqm%{x)A? z>TNbQCmhwohgIL`{Prh5>E2`^e7_$#Yx+O?|E6DW1wL9Q=b%du>T^FbU+LlHI#rxH zRlht;z0}FN7_ZVv9zXH@zG2t9K60X}>%*&J`9yuAnEj(M{ZW3!8`a@Atn{2G{G`A5 zwr1n%_Z>F1svo#_Q^K>4Q(xYGR@Y}PC=c=1jjQVS`dkOB=C1tq{rm&&|CFhf-+N!s zl<0bnPJMZPn>YJ1FHSvunXlqeKAv@9rCa6cK)lqe{Pz9jahKhCYUTH~1DXOI`cReu;O{$;-_`(?;W4V(VXK;E!yob+QkiPNKTLRgv!X-EUr4t>@?5{4`&C>fYFR|5eYJT0Jj5q#ZBapAXV?epsJA zG@lsa!z!;nimB6!M|E8XtorP4ep=u2;MdP+4EH1dJx2Yh?{y*l7CXnRmza8+jp@(T z8SUdU?5)F3`d-i8blc_6uiEWxO^I*#sqa2mA5ZN0MNChedUIpzW#3$VI=w!1{9IS; z>puPVdU@4}CrniJo4%rT(&rOBSnY3pc|YVKopoXQka4vR9;|rgh852~<|kcW@2_9H zUr_z>+cgGVyws=be$W-`lcy(6J@w4@;$EM6uAA}bI`r^Ur`q4%&-SGszVp<|@BI5q z`;%qylV|SC<3szKpZLDNe8tU|KmX*(gTF7X_BZ_=ns2jc zow+)5`}hs3e)}x|qy?K3Y-9kJKD+~0nmcCX7HGEv=+eecma&EY(v2R(oI`jOXX zE+`N2*Nvn4qdfDu4%qAGC!Y61d~DCQ)7_8J_YeBINx#kGK3gx*b*zf?R_)g}s^hw# z{zTW!Px`*!`Se3}n_ATOzg<(n)1SJ|qx4%JG@lrz-n!yd^?QA;16K2@^OLUqz2omm zD(9B-eB|G9^}5K{efsVB1O6 zu225n58gVZ+>fn`gM#jk^Q1Y(Z^*|)zVefg@_+SPZ&>pCi2E<=`#v&t^Zcd@&6TL0 zdh3dq7c-^?H5gg_XWKKk52; z)v4znKeejwcKfD;=YFMr@A+H5^~Est))lX+-|KT7us1J1>3aXkuYKvBQ>%LZey8%= zx$bq5uGsyMPt1AMGvAAQed@5MSJn9pE1v7-r*)ktue)aX{66~dri91W)OY^#S6!dE z#EJM&T|A1tzN}|Fx{m9CRUco{Z};H^mmV^;s()wk?*p2v^4sfZ`YpCDoy441J@ZvO z%EyD1UN6sQSn<{QX`#7L z*SWmQa~?mn`uX_2cWw;2F!kN9^i`~`KQZ+-8@nz#8OtZ)!K{l%=Xc%wq|5JJZ}Ah8 zr2BDj`}6MZ`%j$z{52PSXihQ2UpJ2GkMhjtI$+hW&QHAN^2Ww<-+aO(sebj5twW!4 z+Pctv6Sn=t?ir35Qhxi%ugP-Q}_dEN1_pGT^{pw?z65sGs zzw+Dq*40l>ot}Dyc!{Z#b#dy-L-mX^Z#{^g=sNjH$Mg3;JnQ*Wi~2?F*Y($)C(fPJ zweR_=PTySOM0}_&?%^muc^UWCQFlF1KGAa@KdtBY#lL*O^1ttT*$o;4ukt(33w-`t z*L>&2-fhQ0o(p4U&8{}dws40 zR&(2*{G{vWCwKbZBc@h$I(XvVI(YheII2gtht})yvOd~pUYI%=^OL^cr@iL=PoJpj z`#q}dcRcqi^_(ZUzr9|l(@9J{_00F;UY|Pb=~Z<;!;0tn`DtB$4|wC-Y@J%wx9{8- zc%y#f^WS+RpP2KiXTFL@`FOC>>*e_jE57Flep=V_+`Hej*VL-M_a03N&;3e$`{Ly|sL;Z=q-uX%2^T|mcZl9;*=PNt? zX;Z+f&J*VmeSER|Y(90Eb5{A}_2SeK!(M+?cO9_eIcNE4UC$>w|Kh<@tNTF*PuyE4 zd3>jnn0o4TMzPnY4o7v9XI`jY>GD(E_w%oO+{sg`x__U9Ec)xyU$t2qXVnF zdWH01^60{>kH-3|b?ErI<|jTs{`!&!PgM2S?$;Rf?NfUCd*_KdK0TOvc=E8y&#m`# z=)vTh8{#F}Z~UaA@9=XEn_ASbd~H*}bH7qozx#nNZ(aGs)KkxV#-lua_)vZ>>UVur zKOR5v?7wGSzW3YBBzAtI-hj2gk-mfXr#ZP_b z1${huuMcz*Q*Yg|^+t7je05mq@`0b?pIkJ&_g?Mq)7+1(UuuecgsIO@b#tWO)=OUM zsb{{5NBMZL((C12AFTN5{B-@EzaQ}jkDXf8w|~1ihR46ucb=#x-~EtJOubcOy4QC< zA7QngbCjRv(WU~J#{*Xy?XM+tM+BTd12PYuA86MxBs^M z=?~WL8_e(eR>iqGe1=s&Uvq!^dy=ny^##+&>iPb`>iL9jo{Q$lx#VH`ROMIY=}X;= z@p|ZbV8wU7^3!^JeC`eRnOe_Y+<=pA));gvKP$iS{k+$D^q_gw#fiN-{wNP0R(+W_ zH`JfVSANp*`}uS3_{^!r{kY_Yje+NWrLH`G@x|%4I!wJPpFHs>9}nuEi+qMvzjgR2 zwm+|a`11RaPdc|L;qf)~z25Ry-8}l}B;v0N>Ce?0?Q>nQT8AI$x97oUesKBygHLUL z-raq_kNcJLn#a0gJec!V`Q-KD)ak`NJ@vc}`9#;9e%qhhe(U~Ii~8c*%WM6%Pg9rQ z^wnJ-%sJ)p*Nv;{_xfB1tmandCtdH?{Ow;qWNKC4@9&$EKA8IHH(mbc`Ar9=-m0;6 z^=B+!AwH~l=7tr|KISJ~Kd<`yGtQh^)z3Y<;9I;`g1Y<^Y!USD2E zH76cF={moE=ci7eTGcPx(vG=Kp#*e&yqN?xs zd~hjygZ-`@Zw^ z)ApEJyhP7H>h|XqzqtJGd2MaKe%P?D zoxAj{r(c~e#1qq#r>CCztmDC~ixVqdywuCQ{zN=^I?j#U-=2@Z{+caQt9m}4xL@>P z`Ymr=nDfZvuNzm@@AbJ3Sj}x;@sqCm@S0QaFtw`R@_9`Ouku^J{cU~oSeH&B{$`Qx zsyd_nt_xP{*@yh3@8^TJ|MPoIt?J)-N>ii@Q=e}7&1ZG%(SfN~<&#HGK4W=E7s}5? z{ZPO4q3h;n=J)*7L#I~tub$GBoHzKXZ-4XG98g}r=Ti05%loFU$5T&TdAjO!6Y*iz z^(W%zI{c)=uiI^FPN;9)y(!>T=ZX7h9qa0Y<`YBw>kH?u)8o4iSj}x;@sqCiL)`AL zTTQL%bnvQJKG8hx1Dy)9e>A2y%CC5%I<5;=dg}b7Z-0OHYd=4=s=qWopUt_D`qlon z9>2xbg{il$cvbygpX-3tyw(2p@74X`C-#_H{r>*V$228=aRYrDd|gnv3@+d>X{d-n^&JW>+*@&m-W#&b*lB{VWsb!FI;LeAdMk(uEZ-^Esy&;-{|jgrDm6=dO=zpGW;Y ziG>gz-CRdLQC|;J4?p#)c$A;Kj4S=jn-^wX?EK}Y_1Jsh8Bd+4Z;@Cu;?e1?gQpK) zoT#3C;tJ_h^~_iNDxdvg^FsO5@paEnb-yqE9}jxq)N21<@VD(N0?+FzedzY!L451V zC(?oH;!*7Nsl!p-g!?Z;a9ryqr4t}u8w(O zrSJUar*+u-%>C{&dUW)0A>QKG}YKykYK2Uq7Gmp#BQYv2L{9T-{!uI;`~h!cTs*r@)PkM<4p* zQ>&k6z2_$F6_zf3>StZ-{#sW)G4-l-l2^t0A)Xk@!z!;1@t}O7>*uGsbNQUp+9z4L zAL^;gFZBxb^)U7DE8Skc>d$<}JzaJ4Lit4c{1n@NfB(Gp%BTMM_T$;v=ZSUACe>}?Q>nQ zT8AI@FMZF?d))kvQ>*v8(ZLh<)=8dq=_IC}I-MSl^6_9#x2l^LRy<$#{Ist9{SP-k zd}?(+9`e4Xlz!3I?|$K_o5%AvKAt+hdX>kkF#D3H4zpgx=2F)W_jvmGiSPZISG@DsiQ;}-x>q}a=sS<-Kz{O9 z-gAksA3D(b_?yMl%ebeH4=cX;Va0RK@{=yVcYMB2<=k?f&v3bVU8J6V`~0eH|J?r1 z{@>|7tyiD>bDd)QKl?ZLH@^M~Q*T|d_2H6YW`Q|UQA9%YTPrs^l z(5=oB=Xd%oR<|BKvAJNCS5Ksu`Hbm8^^D~!v>vSb<@qVLKOeYjdxMYqP7iM@;yI6V z4*mSaH&5o(Vd_=+ehk(FMIbMers8ki+@$7XlP`HWQq#`1&^qxs3=PWB|$|L@q{iB z6%`W@P5D70qAt&hN|-0|P#(ABAxSMs$!KRkv9wKYZDaZ?OieU1Dy)57?=wG(Yu>+x z&-2bTmsfjV>#uu_G463c<^59jLQn-|I_@|&O5wZHHA|D8FtxF4I^*EhTG$GTtqwJv>g zRfxZCwBB6ZUY|Ow^ws&v5Bu-^Uuf^7IBzcB+?1R{m7nfo)~zRRJ$mZqRfqD4>Q#U8 z#rniBb>tKA67%}hy}sb5^>4fo{+GM&IsME1`2I~=0nh7 zIyX|+{oPQ3#~U-Z?uokuv&*-{G{*o$)@Wse_njy;y(_=)1Ue~{7y$&?0%%* z_WdJ2(B4V;iG_hz{`qpBe(LUn`(=O2i`^%AIxzLbP@lNMQQqAC)HOG(^!UP0I{be3 z-phaA=ghsE5?%h$*FSfE(}Ss3#qt%Jr>a-^s;|nM8&*8)<^Fabe(9X!CyM&c?c>a? zfA7&gr33Zn{V&QCl)&!7J3Q>PKVK0oP~+r)T$%j@Xv zZ|g#OF!ic@@_KRV^x~eLy6b@QiOvmvTAyFP`I#3@E$)YYJaKOwJbn1$M0I_uBHiuX zZ(i72kDuoAeTX+7visDke%Y;>5?*zlxLvJ8jnvWm+q-)<_ z@Vo6VbExlgU{j*&oKF2wzpY14oO;_D(}kmY_^|3TKdg9k_$l`D{MRlyZfaG()?c*~ z+kL`MefKNZciy`n`spQ~Zk1Pu^u)00m)8gNWh|eVI$5WK$4`0RkA2`p?Hxq*^IzVS z@T&8~zUQO$t;csdiTF@mJQu5ec{o=W-*rLxMEi`N*7g0tbI*R*)S|xo;)M%d?btLua26GQxU`g;bOpWY^;U-{|0P^X)HnOArH>M---3bQYH zSKn!EpBr0eZXf<=KcAugMEjVZ^nHKu zg}?obsYU(3#X&bWjh=~w$({;V_FPUrW7p4F7hQN2F#_~5Jc&13)5NyLZh;!*7Nsl!p-RY2J-+LJ)!cmLCtdmPpYzhGRh|cGUDX3rY#Q8<9o*uru=Z&n3 zQ%`^9dvUK%9rpC9y8dc@eID=p#P@n|-%qqxcIuZeKKRiG>FM|QsOoqS&)lhpCl529 zamCC0s(p0K4dv-V`;DLSe*f~@-+bEC>hsMbKi!n@_(~r-zwsbn^~opFf$HM9SoO=p zxw`m#?&&xW_=&&x8r6oY&TNYAhyTAA&;7D4bYAdRUhL-~I*IsDT|5`7et9@o7oXoy zKG8npr*--Do1fWpYEj?3DBz9yjX#Px=eEYyovSn2$7firQ~7QGJ@(mmo?7|6-{SQ* zU5}5{r)xi(Tc7)sd2#Ah`Q(Z5p?sn`tm>K1*mc3Ii}}G%>-zlir@wr^iK>3UA2tRa z-{?Vp<3W6nANfQ&P+dG1tA2SnR~MhpJssE0PkerD{^Xq|N$2~9OWF$dHNJJ-Ke}Q( zaXw$tGdDeb8OuZU#MH}r71M=y;?xyGJbh4|pLo9SbL)?8o~Z7JK0GlUafSMNII7oM zr>c+U&DrCd7gl`xil6lTyz_-`wKrhZcm98x5}tjU`ttU(x_O){bQ1BQx_B;D{qk_G zF23u6@`?Q5r*-*t=&vpR{*Hb;ac>tAR}?nmlb$NBGm;)(T}&*Mx?2g=it&sZL2U-El({0eg&dh&^`pPzJ{C-3`I z`-MyO%iq};czjD;_Yn^|N2)qr>Q(vVRdLlX53BxOp3glUb$*I{e)+9`dc?Gpx^?lY zSUyqTC}#iM*gA9j@JIXk4fQA5XZ)mZ-~Qo;PnlZOHx2%O0)Afd_^~d1Xg)E-UpKC* z-|KT7u$sH_n_vI^gcnS${C?MIP09Msht#K=ew#-hoy62r&wMZL^{K<2UR8Hpu;TH9 zpVqa%-}AaxO|9y5@Wj1!l4o5yiK(Yfr@~R5etalD7tIT+e*2i8*1gf9;h>}1-#<}5 z?;fo{7eDp$^@%#Z{tEHejn;>ie%3S2zPyeepPzVMpSMgD$n~r9 z#5tl)Co%QZGvAAQed@5MSJl(+is$>Q($h!54p zJsjoZ!JckauY69veVBgp<(_Q?ygVzd`-W7-tf5FFTehN&f@#B)*pR+Vtx15x^xopp}Ke! zdwuF~R5y8ihFKT$EB*HU*mM5)aT8TtU(VNCC;RkQII2gtLi$y`mmlpnFRXOc`AMJO zcX#uyzR!`jYfAc3PwaY=r*1AWo_x-Wmw9o8sh7N-Zr1f@-Z~I3(K(ua+n;wn^r=&e z`XR4v3f8kfQaAlpH;+CsJ&3<XUKC<5bfWtIOZ4UX zy>+q=AJT*J`qqW?=j!$P%nK`hI{Xyd-+RBY{mcEhba9^0#ZP_r(SBCP*Iyz2y3u-b zb$fm4u+rxTKl#D0tB=S%CkA9NCP&TWn9&(#_2<1?(*QRgRp=gBE=Z+|dW z{fN6ZCA?9;@u790Jj{8keDZp6>h$8Cp1SLR@`?Q7r}e#l-tQ?#OfBjgKiCxL;-#*< z{j8pS){__GLv?WvNBMZLr(4za!yb>HuGi1$2mkHz?>F3HLBMmrQs4QXe(URD>a7}E zXKw%8K0d>$pC9SB^ZSag+-n+D{er!lB3=D?J(b_?kGi?&!PFa#=_S9)CokjCdGKKB zSl{bxe#-M>eiq;~@3*=l#$J%_oMbx9)iEIz7JYfW3M7N!NYY^@RIO zt?CbJ+kNZ*mtnuAzSkd>-{!JjV(M*cOn*gnY@5i3H@wQW|_YdjN#g~WnFMs9nA)Q40b)$9W>h}87VNai* zeDM9M@4o2hsnz{>#4j}^kBjuv<09*HE5y%vD}I&F>%_~vSRKk&h-c1>GoL#6{1p5C z$({e(^53J*&v$ySPv}AS$Ky&qv6?gUy_jyrQ->9=m+$$k-|L9H7OVkKgMFlcf7WAJ6$A4@b{$ zI*D{vMS83D>l@V>_1n40Px}0R39EviT+!zb>@iT)ojgKErCA%5OjKyz#zwo?7{R z*&~~hzMO}@Roy&z`tjAxt3I072lYdFNEgcY(END%t99_~V}9a$ee(FTZrZ3e?4l8m zPR8h4xYHTPV$^XbP`ifolb?L zJpK4kelD69_WJp0-5V|%t~{i@QdXyfSH<#)`bIJPM`QY<{E9cK!)I9OIVSi?-_Ijo z|JmjL5B*(_Z%TO1qtw@5opbVZ5>rn-^Hn^`$Agt#FVAOK@vW17yAQ|TeACpbzVmTS ziLU&0|n#qarj;5Y9* zwetJwJ(?2T+`r~^AF4VYqzBEbE{0WJ9ggzw6RZBrn;WK%*gobb9Y61U>$Z29sOmR* zW9!6o&eMbZ#*;UX^F)jX@wXSwU8l#->!{}D7eDE`4|h9!^VF)o>i;KTefluxl*eB; zuBzYba~-gn+d0cmx<0@B)gQF~@Y3gP`TarnEA_1l)#=Mad5FKgaPB%izB;Vt_PFOK zp3g5gU4OqR<$heaDA29^_V^e*zv)49tP5Aw9qqGkU^TZoKk2&|gMUBP z^RRQ8Zld)wU(LI%dF#y8>Gin|Sn1Q@r`YE^PrTKAr&jgT7f+;kKd$|4z3MtLKgzFo zRX*1d^BKygj(yBeb?5q9PilVqe6!NYb?_4Dr;o-y!IzQ`R!aj?wHq1t?E1eKHGopB%k4b&blVcPk!6? zN5Ac$iR8RFe(}PbZnb}V=ZSS7J!mp@F|6|HaFmA+dwp3qH_SR+b$;U6-$&i78SH-e zd`F*ihaRl_*5^K1PhLz1R(bVAe3*4{)_bwOMDxR}i(NNAt!IBf_2cdPK7JlLfANbZ zboJBIU-_+`NDro-IFU}~#l1RSg;js%%?&F(=Qls;_`LWL7u|7cbwB9fRk3`czERBn z(U|@yzv7MR@EKNm&I5kZ_xG!A_>T6IkNOD<0$r~gQ-9U{ZT-|+H8$t=?zax?t;bK- z>-_$}e=NU$LI=;|LmoPhs`EyFg><$R>949Y+RtZLt;etQn_s8=;wz@sw=Op9F7f)i z^3(lNH#am_#`5M>PgGCD%X}5%!-^+g@yreJ6Fu(vNzeO(um5=Ka6c~FqcP}?o+otq zuTP$yIOkN)d@t_xspq;GkFG-xKXvRweyV%FYRB)pa_&bXpJB53YM)hpKjotK2MW}$ z_+e9`>-jeIom2L+^~BbdrzcK5^~`5H%F~Ar<>#V)SoQOTpRU*IlQaJ9;Hg#p^xHQj zy8NTBpP%X;N9y`BmQOvr%!^_2(3 zyl6P&XZM``rGC}3TETs>Z&RPH{jF{uk1INf_)uM3;V4f(K9rw}t_xQEuA86MZGYu) z!-l7S>akO+pFc1BSX08Q{Lb^l`sQ)&(@DgK>f#DVdHV68{9JThux)Hk-T6K?(D8Ox`h80r&OILe#b zpStFTl^#F%NyqO49&x+FTdMIWR! zl<+*SroQ{=@uRNKT;fE0s4ni|C_i}__tsH&Jy1T8FZ{Hg^Lv-ux6kL*FS~zp5O1#E z_O*5CB<7sznIDa-et9^mmpnehtczVYKdsB}tNzyuCaU^LKi3#^@##Uns^i=59#>h% zqXVnFdJn69dDzpb>iS(z#p4S<@!f|rk83;F=Mkfy?^Le`>F0c6XkL7nd2wR$vo20v z#(0UzGdE1WSe>7A_X2c0wav%Efe{qhhW;)zF5AIv^^Sk*J1 zvAJQ^#a@T-)4K9+_|qp(RNn_W@^P(`E*(v@sGju`>1Td4p4)F;SoPb-{KWI~{KK9)_I)4g^sa|~4^t0+ zRKLeluXL-tzC`mv_0$ok-`8I>eD&(~NH2JceQ`{U)Aa6yj4DV;!!>x)IS&bJgSp^JJ)yp)%N%8 zeI8+s)U}>^g?N0YlZZ!0T|5`7et9@o7vH>4KG8b-6#IPVfIVL|wYVP_ZfOc07kr~@ zJ?BML*Ka+setPOqK6&yGAIc}H!>XS7Twe^6FXjtBtt)@^m)c4Emc@p@pnd&k>-*oS zufICK)%9g8Z$5RHc`;0$e1-V1;>lM$b3^<@=Py6$IlsU5+UHL#?#HG#H3dBTGIiar zYJa;QbP`ifJ@ZvO%EyD1UN6sQh@ZO7X@089U*~1*3n%JVzojYR{kZ%#|0tSol%Kr0 z{d|U1zkSG0JbqvNxxJ=Q)SuJ-yTGmIiG7;$(#`!%Ke0Dw>Q^zHsy>>ZtAn4I>+y@9 zeDHeh;&+}swWx1=LsOuOm%7d=e&a)r3+u^?Q%^nf8E1dS$)^jmJ{s$XRiAyxPxIYq z(QudBKXm$+Ivuwa3FzPT#IUpHDGR{B}bIQ#NCdVGH3 z`Tof_o`2pXt^U+6G-dkuxxV|zZ#>VF>hknp>Q(vViAVW(Q2$)i537FXFF#$s_Xi(- zP}}M1TVC22c>GI!evbN052jv~PhQ5oK6RM=qcNXh)yFS>TG!`0yWG3IlcIjrUX6jL zKlRgZb^beV=p^Dpb#V_z`FOCWTh;jtD;{6?X@sqCKzxS>R&%KHlV1C)_8T_r{`Y52 zt?C!ttSRAD``gd?&JFA9gVq;Ad|2hxM=^DJ@u;rrfK?wq_-TDV&+qify{1<68y(q{ z=;Eio_hsng>5~^Hrrx?@>&(^b^{K;3-#+9gUwpo^gmtCxWep9 zUe+^CzBqa2hIke7laBNEXOBH~YH>d{KddR>@ile*+{#yV^T>j-LUVRi(rx%avx(-rd`8QPuUO zuJzO_)YrpNy=tAzTQBpyc(l*Fu+p{9_(|XSz3ITsQ>*$rZr+se+^^KvU%fujZ$2?h zy>-Q_>i7Cw2dw7xx{aT7}>phfk-%QJ((fXN=cF^TLYHSAJS=*G0oICoca!$;Hoa z1-$A!srI*dJa3BW!JN0sC$AT$PA~51sk;s+pJ;!l-+cV@FSa*K?Ejq?f3Lutc&W=z z`s!lm6`e$Us4gDGUY|M~)lD9sVb;a2pP$zC{i>56`izPCW)Fl#BOaZMn6B&#=lx!g^^D~!G&j@_vyY$ZzRz~=r?mq_eV0wGGj&t1@|(W? z3R7>@*n0Ximah;WR=m89ipLjz()InScmLY{Q>*$dFK!Iyvd=$LUmg#7-C|wqsf$xj zJ@ZvO%EyD1UN1lDH(&VadVSvTsi&PYEv0Tfu+q@}sGkq_h85qBG5oYH{NjJSdTLd_ z=-8%27t+^P`Hi3VK^~?a-OP(C%)aEQ!>m_v^34saK6!r9aUV8rK4hY(AF_C31<&h7 zI?(>*tGqsFJ~71KUO0E19^ZAq-rW48>+`hRJ?XYnt2!M#@#s4GdN`_Q{T^Dc$IJR? zpLt>Gh@HRur0@UB*ztZ9r+zf@8CGnM_w?I&@Wd~*JEDHsBb$=@;(n#R^C;`)u`Zp& z)Kky=Xk7Kn!%@BDxh|M>arOGddGb$hKY60^dzbKg?)hyVev4f%G~c>$RsCL{>wwkV z_8~v%+TZ6M@xUd&Pg?xPC3v}itwWcu^7^3p#1MboII2I&GoR~#Rlj=rZGZ3h|4Evo zm4B7r@`?6w`tAMigU;V)YVm&UMYm}R?o;kx`p&7Wn_GW{bk>E|rI)dMA|A}TIIlxK z(fP|yy3T_Of2U>qecAV%)fDLCr*8UagPZIgp>`uv2=-Pe`26l;~Fbm##Wpef-Y6x_Q;bu*#3t@hY6FZ*Exi+sFLGWA8t{d-?g&e0XB( zh%40B!%@9z9eL}B=i+F;d0}szJiom^`0PuU|9`2|+uO5Sf1levO?~%kbbq@pn0i${ zc~xBX%fqU_m*+FA_PR|SKP@nTX^Xk>StL9hL9qq$& zJ=L7fS$^VsJ@~XgZa>+(A2;2*DdF)o_1#DNTU{T_Ipy)!jjQVS`dkOB=H?ea>H2*B zkw+agwfg@Z=-^ece4;*ci7U+hRb%>d`$zj+5A3bOPx`wp8ZJGlo!?$B(!q1T@>8u-9)$jGG!`|Hd_1$*cFeqU=!&Ix?_Vy{2&%prCT;mhNt9v)Qh zVd^EH9#qe`m$weA`kk}<#Pj;(WB>e$X({z1@7GrD={k@2nDgqd5P#k1`rxR(IV-+8 ztau*x{4|HxCr5tou&Gu3)IAylulji>_cuQO-Dmm4YTi}z^yl`E_VF23>$q-y()af` zPCND?Q>*%!Uua5rmEZEYKEItO>hxeW@3!Vw)$jG?byRa!e*1a;J_kKzYUTGn=Qkz# z_HpXV^VhuDmw9pO>C1c-kMi-X3oG3!PY2?qUhh2l(8KqbTKS#-AB=rWAJXNwyx4s< zPa^)RNbkqh&u3VzFV9c0{k{L|A3u$(e!$z?NkErxd0my??vJ|l=!sKLJ@ZvO%Ez-V ztaPh99oXaX(|Y`V#S5FG>gRp5DOImO_?hR4_06MhJ+V5zy2phWPmB-EA+8WF^((%0 zA)Xk@SM%{xouBXCyZymI^)vTwN_5Sc`u1;Cw+^HSQ?H8UE2Ll5t9;d0<;@K%p7r>N z$M1iA*25->IzFD*I^qiT^)U7DD_!|fUh?Pm>4)Zp@|7+>)qUUo(EHwQYE`dvavi)x z`uI>D%4h$!#`Ndv^!m&Td;0vO>-Sxc-2LfOtDpBT`^%<;=XG@I=Xrwfe9U@S3&voirwT zzQ#{|uY2By-Z++jr=kn)iUwD60Qun?` z>eEf1vdZ3>pR(z-exCpGdyk%4)i1hb+tGBZ{q4NS{q6p`pLC%0t9j?KEtb>{ZrkM{E!>Q8hYq~HD?$Ifqg z&D5g)zUwyyy45`PZ|-k=NGCDp+}7B7bTXDt#DiHE&z+y2bou?Kd$n)=sME(2TTfh} zz8;S1_13BCqj_`o_~wNbU!9-yoy-5@wB^qm=-`Rzh%3|w^@*W;rL$_D{@ngvpLt=W z&)4+Z_cix?T6^a6{-O5=J@4fl)~lW`)txi?>8YDny~?ZOCuUz&@A0ZWdHqm6(RK3E z{C>VX^zJX8THKHP`?2~{ckcOZU6^|7idWU|_3;^2^V(gMsd64QhD+Y9He)8o4iSj}yJ@{_Lf;GiEqd1_UsgD38-gQpK)oT#3C;vUi) z<&E+yJ@dkf=W)+Z>&idkWiOgq)&2k6c(BkI&s^ff)Z5mW{#>2WKGy?#>+qAl z|6l65$F>*T>R0aBl<-#dTYrVUdFR%xv%UKG414SF(_DV;{LRlS?{7MI;?Z^V;foX1 z>92}(w|BpJVQ;&A_hWrO<;dS} z41Ih}UHg@<`11C@JhXo5sb{{5E1k^Kg_T|}uOC)?=QKafd#m{^~6x0xWZB1-2T)xH>~vd!B0Aix4Ac5dRlv>qW+Epni8J-mHPHCeRX~2 zg7Of5-MFfLug`VBYHoFY()IcLgEuvX`n7(mDdAOqdp=CRt>+w~lbG|WXTBHr`qW`h zud2H)h?lxzev0}1mp|1uwEF3P&=_>_>BDM&=lQLl9;5@Syn2PbK6P`zO0UY(ffdg_ zFsa(-g=2SuX^Ttaj#Du_VlVcpJBzb4nM`dKX~r7 zpE|XwKk7Fc1J8a%~&85G$zdf$->A}3eQ!sB1+r{DPIai8Q9Q%^nfRXobagOy${ z@48^ccTV%uy52we>AjczKJ(s93D4sr_1&-PJTVuY#MD#Id=-!Klb3O&pLy28{+_Qs zJhAn}72Y zuP5snSAM_z$mQ>U@A8h;X+DpG)N@Xy-{!La=_IC}dggm^uTLHJ^s2h+f)!6b{r2}v zuXn{0CW_DR_j^|Bu&;AY>*fB&Q_p^}y1Dd0`Q*t%d?=r&4y(F69+Wo^luxwJ_({k6 zC%ZqP{pOVWaq&Hy0v?}Ix7y$O%mw9P>a8nYRlnEgI$$-geaKI`^4C7~n5k9$g7)Lj z*3WmU^Td6iXO4_BuU^f&t@%~;dwqEw)tq?zr0esvUBCA1snyRz`Tui!e5Ahn<@}~= zJ@-Ql@lvnKC$AS<2QPIqu6Xj0E|gDn-Tbtk*WaJ~_K{PI`=KANiscjajbirCjjc1c z4}Y|u&rp9NJ${Pq&u84LeIZtTQ~SEr*7L;uO5NW1Z5{K8Vd||rp1V$u?>b;Luk(PP zbiF=#)4_L|T0Ng$c7vuwSAXg|*PSQo^b_gH-1Dk-N7OIe(v;|XpC$F}U-wg8 zAK$GfFUE)J;tEH3`jcO+kDrK72jZ!V`N2WE#56n^{4 za>=_J1J6E9{XC!Xn;DhbHc$*39s7U?qja6Paev{YTk9_ z=dRP^=XF$b(n-I4zv`F%=kn)i8wbB{?>@S3)-hM+)nPU7-2AF~bNgHu?DbcEzwVC9 zKVP1IpLWG`o&V{BbB3?#=Fnfo_z+**!%;q-Ip}6AUm+ccm$Aov`t9}Lj{VKKAB}v4 zz5A7Zd%k?$PRpOCUG|Np)YG-^?QiSb_s$!6dN8l9$|p}u7s@Bng;^hs^~0)9J^kKw z(Qw7bA3goc`-g|^(H;@{s_WtFy8N!zsje&Y=FL1^Sk*J1F@2bIF+cceUHfy#>yzdb zU-*{D=N?Zz_cy=yKlj8*T792Am)EzCbA9K9`>Ad{m~+bGuNzm@@AbJ3Sj}BMUpl|{ zyH)$-nBQl9q^(R>f9k87Q|$h@A6XYydYK=Mt)F~(nDx=v+_37i4nOhu{TH`8bV_AC zbu*@uh&PJa-;3$?c%$`EJwC!pU!9-y+fVTuHeCOU?VT&ph~y*}3gtGW5bPrB~Ij=%R< zIk%kWGhD9z|D*KVdGNRQUH-oPi5pvQ-j`I|vA@%i7JIy>-+sQl!Da3JD(B6HGnW1K z_(*m4i@y8g{_7K~!_-?>ysCb$&vn3RUgris>Dr%r{$%@pmHY9evm1jhe(HN3Rfqid zewaKxn0i${c^T7(S*HuLJ{s$<)}e#PPkf)xfBggJPE_^7+Q$#Gzh6ZcS~pSMeCtN* z&DHJosl!U&{^qCabspU1VeMb)zJG$}Tu6P75Bpi&Jo@M);;#$o&($04b6v1nN1dPa zJ>H*lz^PN~TO<}jcy#HAE7XTCPE@C(F7Dwd9}o6)tGan1Uh1mzQ{Ddl)@|=HwYncS zdSg?<^Z1|-_2bD~&;AzULHzB7bJywd^E#@z`NdDV-apy#_fIP4mh=3E%hhWl{-)o~ z-(!w>{zO*avbYiY+^=gC+rRYZ`K=F{PYhFUUGb{=y*}3gt9kjwPrCei!7G=~lZ$WB zl<@SYzT>{&pMtbJ$}^biBnHK^Hod- zRy?|Ru;N$w)WJ(VKBV7%o`1((?=`jh`SZ+gHzhiJr4Os~#Cq;i&PkWfx^Y$gUZ3lL z)!h8zCtcr6O(VB@zXs1 zztL}=(B7a@-|2o$k80mh2~eU^wjC%Wj|gI^+SAm&^gM_%s=P6 zUzu9fzkF6xGADlO^EG+qQP+>Bo_gX${h7}=b+cZ@RbQ@)2P-{&{1iJ6&fIsOsa2g0 zp18LTp1vNY9zLB4M|t|^^7TRe=7m`oSLcbJFJHCG{u8z5w{_`Oh&PIr&S>5`qx|HJ z_VXF0j@aXvpY**R-1L#VO;q(=Zq+*RoD1}z`$%72pSi?|`0GOYbM;31To0_)sm^bo z&wupnv!+(BUp7CzDbeL?>dU9!`s7`Yyg2o$eDcKjP(D!|R`twhY<`$^u{uAk>paZ(ZEiBPsGq)9Q^51OFLgcN z(pT5lL;P(;>#V9X+RtZLt*6dU`uzIvZ|^&`s_$}2Q*v(O(-%9Z(r@d@i|L6|Pn>AI z%!}0%)q7|j{rDN~A3)sruV=7#c#_Ax){`@HzPV-BBM)bsPpx#x*_>}TsG=A7!8ui{ZY9<20wdDjK; zQ&-GSvG-4Y^36}3TGbC&yl}y*{I2%5K4?BMH1E1`RDYCbKGy-O{@(uH`)dze@_W+( zZ3TMvsdXV;=Y+gIb3u8CzrC=xPOqMGxei$M+lTfq9=~4mox4xc>Zd%Ut;9!|>)ZG0 z=75>cIQ2$j`W4^xRrSo%>-FKm)DhdC{G{tX9Q6y!zu$1-?u|hgpB~gtU!Jdi9@0+_ z;=?Mh-ovV29`gH3YlXX0( zp8D#Ec={5puTNbJ+2!@s!0FG)t^51*5@al z{rl^Oylfg-egC(#E1-{`*X41Oe(Q6e^~=N5!^?aXS31e33)M50uh4q1>gNkT@jTz{ z_6x6^mQc4Ip4d9#3iaWO6V>Uciz^)E>BooibJ4u8*UwMux(^?HYx~JZ{nE3W5?%W= z_2sMm?HsZmJ#p%(XFg;7FzXr9g;^hs^+SEt9Q;)G{`b9a@|3An{rnd-1|Hw&L+25{ zldmqHNC&EmdpOF+gFW4<&SzNhTsJ?h>*x8e{`9_6tNQ7in-ZRLC-v#_TirbJVtNpN z-MFfLug`VBYHrufPr6=TT>ipir&jgL7Jpw8Z*+fK-+J`vB;v0N>Ce?0?c+17)=}pt z{jK|RxAuJL^N8Jk(3JG?kG_6>s>_Q#KgiRAsfU+&afR8JJaw4$j4PhGVfKmn!cTg> zUv=*5&zY#|oBzIb;*IWa{K{{2Xg*lw)e}egcrbNFV?M*G&-u$w>-u}tcRutbQ>*&5 z7GDIy)1UfVA0JWB2hAsj(YCg5RsCL{>wwk3{Ng8F`7hn#%&FCRbHxvv5?-~xofr1A z^~LIR67iwBcochm>Tpyyd3=WIsVnBEm>+-m`>&f?)epF1Q=$v$L;E}L2Yr1R%hQ2) z;vQBy*_XP#IQzxcgYt>=`6>4K{Kt;IU29?BLI96m#`1}H<`P$!{c~fwbNlc|`&|#z zpXm8=!@^JcKHvG`f4q8XaX&6eaTaYS+C;c zn;TYr^8BRZK3w&tq^qwAO#R(qhItNIq+OFqz)@H{?Jzw+BW)}@n}dg__)#l1dt*wd@( zt_xN?`;(v6b$lFu^G#E$`p(BSCA#>juRqUkee(L{Vd||K)7PJ|e1-V1;+Y#(Jo}8F zbouqQ#UEtz`}K72#JzR!^z|_H@aa@I%F|!*tGvEM^FsC1ac=Na-Rtk)dCKzZ?@C9X zdWCq2iq4Q)GyiCP6E2s93D67_||j2XB{u+ zt@6nekMi-L{<)~%^;G??o1geMUNpS&%^RnGc|YsJ4;=HGZl2TWw>cAYUUOxBG}aIG z&5iXX_U6ZT{rt2(zy9pvJtnIA@s9TLu>1cp>gTuh==TtRThThJ>Wub}`c03YV!!YD z^1r>oG^+Y@E^iDz;=3Mko+t9|ulpfS&)lgekFVZCeHBj~Ry_G?ef?R_7>}Ro{P@Ox z?FSt7RsX*X^IDI;)=R{P>f%xC^{K;A-Q>A0n02w&ef+d8zYl!C^7|*7-qbqr>{oiw z>-zN7Jk|cEO9xhY^~6y=9!#Cl*mc3GukzdXA+G$;p;N2-ao|2p$$XXH^8B{GK662N zh`(-JRlnEgI$$-oeaug~J}i$9_^ChFZ+bBG))kuvU;J3WK9^%hM zx^w%IKUas(ujVqm8 z9}iageBmb@KhOW*EzAGk(FN~s3_Op6)aM@_bj~>6+n4M)rPL2QxiRSW{7&D^m3ey59NUW9syEv2x?nZ8IzRFJyz{ZIw?9ze_4$53 z(8A_jVU<^h)`9XB@{_*D`=u{x-vn2G#4j}kJdcmm^}JB|t>1iNn0o7qSJm(Jxei#( zYajBHF28PYr{_(rUcb=66Zh7^)7Qh)!>3c>C{KUIuk!j5%?s62hp+rpcb;tdtL48p zlb@$m``hDV$NuJgL%!tx=HqF{K4c=QpZSH>=e~G+EURwVAfNY}|Mb=A!PHw@^lkN`FJpOMq}3nt3GvpTG!`0 zpSX7W{}|Qt`-9u+xAmaGJ;Jo}iR*7bSAum8u3r&j0t>5pm* z`xZa-^E@$!KF=GO7pGpePV&Ub7sKRbezcBf?n(#GrN?!#fvz5hgYKQ3#(?y&Xy zC)NI@%U`AFT4~iKBcxm^!1e>w;CEeaKJi^5ciQ9yYbAZ)txHrTg=a`#{I~ z=CQ6g5g(dMyeiU3q$e+iRbIU}KOR5$NwR;ZyDd5@1sayH24_aRgQ*Yhz+;w_< z*8!_}`NB`S^53}b^8OzE{dxDxWAI3b(+t)Ko9EYue`n<;%_TjXRglNK0d>$U!9+LUJst|_2t(m?^xU*JohWF z$K#{txAp14)LVBvcby*Jb--#~`!oHvKX*R%$i(s%M{gF4E6F zd6@Ol*u1dnvk&=cUG_fwq@SHy{k{nuJaKOwJbgV(J$yP9j`H+Z{3@?6(Y#PSb({zM zRQLA^?)JVXPObL;g%58^&I$a~r;BF}D6e0vo_bXtufpt0p1L_;@-j{y9hiKveauff zUO#;3g=bGx^#k^3op{b8deGyBkMjC@h`%nh-dw%WK0d>09s7`<^zH9Ye*4I&Rh~H+~BF3$KyhbC&q`> zI_ec>pEyy!yckw=N>Xis(<#oZ3R5OSr^jfqrBL8Wj=ZkA69ww zQB0j)JgVzDVAW?I^3(eKxa8=&POa+xo)X_6ed|^457NgI(~+mA4&{?450fvShzGM? z#ksy1R(;j^?LIu~pp&Ooe;#M|G2DUYK>Ub@(Z6uevvE zc;dAVo~ZWs1-rKsgsy!|4|;si!*~8TZ?Z0?1NDnXvDc>#M|G2@znUN4_45J z2l?+Dlcy)ndDSyt#dKiBqno^ptA6=W9Xx(ohaca0K>JMx^&1}6R=^wG-{#V1U2!5l zR2R?1sz37?kFMi-VAaQ0ep=7|e&|8>oLbelyrnVd+Q+Hy`PP0`cYmBW)}trJhw9=U zj`HzfPq(V;hZRqrpRV_Yi^7xs=swfG)ba7ESUyqTC}#iM*gA9j@JIXk4D~14hy0}9 zzNNWg!&5*0n5o70XU|`}aG{&~S3ljVjtA*MbE=CIx3!*n8CUw5H#bZjvBx(*>3BW3 z(Biy_WGPoV(P6cw$5C=UY|Ow z^j$YU`NyxbFF1H=^?jhxzpqJ`zt*R3t_tzjjny2>bqb3#uuy0C#Ify=Bs#=j|VHgUY^ge;;Zx1y3X%^e@gpHZT9~mA8L;2 zt6mQ}r|3b?7g?u=m-DJ;zKZFvJ8jn%n;5Cta_!{1JaKOwJbn1$M0I`JiuC8| zjP{uqR_mzqlfIuX|K(-vBv;>ZUhBZC_P28#&pP<_zj|WMtDgBP9_8b~O0So9eLbCO zfAjm%kGu1<6u^u!g?g~_Y($+I5Jy4XJBCw=?# z(~oKY^7`elpKA=d`1BxO`Hd%bKjU&LzuJb?j z-N*D>Oy7F+#HpvA`Cd#9CZ8_M`e>{lR(-CUpXPFYUv%1;Q>*&ApD(SKSk1d?p8nka z(LO%IY90HKpY-{CkK<05TGe;Ee^YXf*st`h=RRhgKAxDKJU#WyXB`i!!z!P;cu;?$ zI-~>T6I~}i)txi1yJY$Ii!c6sTNzJ(&Lhune6iQ}@`&tq^J$-dPL-`8R zZ|@I2cvDkQzt(Rx1#|H&bvec+&=eqf+ZhwAyujZWke$Q%? z;5j#PKK=YvHsKN!pJ&#w6WLF>7$9^$VHtv6S1w2#lQT8AD##rF5R zj@xf)RX^;ZO$pEaO8x5mHji^gOb@2sy5d#!dws40R`b%~r`Y-X`G>YodfvK79Qe+r z#4r5R&(|O7=1~{Z6XUOnIhPoY>YAt5H_G#T$xpiW=M_&pZW_(~*yUwyQu=(#`SoXA zpT~tbG4-}Jc3pHbmQTckSr?DaZy)oMuJhz)4sCznQGM6jv=#99W?fkMtuN=3hjiAB ztLpdq@_MSd?LU6f_4|Nt+jaT>JEDVE#qx>zMlt*6#@3nJhdUXuj{k-$Dw|UXD zwEJ9`f?pMy2@1K|(-@5YjVCvz?!z!;1E1o>W?_u)I4Xb{2e$w%I@ln@q&a3aY zcs~}eI#2k?U+atA59`x|`0K`1^?QA;16Ff;oxo4J&fg<`qy0V8yDSX+>z`~4y8NTB zpWk9U$ZvIeF!icfzC!v{y_e7ajP;wls^h8iQ|vt1^@RIORQKay?Z4~pe!q&}{LS@p zKI=mKb>XVIqkXOiR&(2D{G{*s^1~NDZE97ggIC4!iTXw{`{%~incIgy+RtaGKhgQi zPx`wph@ac3{mb_`|6Bb3mtMc8uJfqc-_|jo7^dF3;#Ku~eXavm^V;A1q$_{iKi+X_ zbw3Vyd{e@6zf!;Q+dS?Aoy62r&wMZL^{K<2UR8Hpu;Mvq`DtC}_wPUWPE)Ho9XxSw z9Xx$K9Mz*+p>?WyFF)FEURdc?uTT8{QXl^QnNusj-*G@wqFc>V?QeYj6;|_ZYuH2-ZUwPtz(-P_@Zfq;!RnM2Be$z>;u4~mi{ki?4eWQMRzT+o-&zJAJ`ccz}_P=@Q z{? zdDZphI{0`HPYmUuIeR+zbo9w*f0ftAPkM`w%{ILG!R`OQR6qN#8>7;K1*m^MQVs(C6*Uy)K^`*v8zu=9{F@5$WJ;+yj`0|zC_;jFt z@hJBC)ZwUZ^7L2pFKYYFZJ;qUmm8OxI#Ls)>rMb zuDPLnrOQt|pU3P*YRlb^?OdO>KATlN_h5b>f68eH@lu%(V_sPG z*~k2}p7#eIf5(ketNNw?));v1OX|z#{#b&r=Iy<-0M?^J-w>VXISwnzdet> z_CL;@TKRqC{hE^b_?P;0`K_+cT;fE0s4gDGUSHNT9$m-vz^acg{Is6$`|SO-2TrZ( zn-=fK;%%$n=A#FzdDoTit<$UL9IgXa{T|o+#Pjz!Hr}cIrm^}($210AuWxdFj}QAB zU+g|xPhFgP>Y4Aw^kDMo!mN+R`eD^q`OS~hPCI&P<@Y7+?eMLi&pW46pKh-2y5+@* zsW&&KH@7eOqdIv#sUx;O`AOgF@4wq`+3%aat##7ZPY?3je#VFTtS2v~1J%V9>W39i zo-RIAA4NK_>hriyzr9~|`#;z`wW^VtiQf zu~QCwaY?PK8xJepTg(aCUbT+Ab;NUVwBNk2T1TCq^gUlb-6|}9o4*e{G{vqCy#yCy{A_7oi;Wlyq@3Hd7|I?Vpz?)t@%;?QJ#5R2kiCp z6VLl6ce>s|lcc)WCwR^y>%!c>^7_mL0> zemhTk`pp&3mybLCK5}!e=c78T{H}i9ah`ne4Zl3K zx*wa~w7kCills;EHjnd0zdTGmyv$c|rIUQRP(5S$3atmLe$RLO#B(2>{maYWue#z5 zZAH3xIY;_USD$q=FHXHGpFFX3#1+zo$*c0ovmVU4xH?aKfAFC5_nD}^?{?8`S|{B+ zpXu7aSvSA_3hAs1txGRs`9wUJb#Y#Ye4=xjpLE+_DcG=Kx9>k_YEkDmp8nL${jIJq z=aYx{+Y9He)8ps$RCDu-pLG4a^NWWcH?=x%cKVaXz|)`lmEX=0bvlWur=I!IxayaO zqk733^;;i5t?Qh55tEE z|58uCtNrbM(Q%#hp*h76e|zEFb$Wc)0js(B!B4v0|NhE(drz(Ek2t3>@T&do`4SJ} z+yC;3Ij?%=dvUK%9rpC9I-f^%_=#_S-s+C+^J0HL*?M?l>xnDW*TdAquXN={c|HDI z9rMCUU!9-U@qD?{T~C=>-H&JQ+ZcHED}8AHdi>yv-B0;MI#69aioHH{II5dGKEkYv ztMl9I?@zs;oeW;TT>0YGndb#PerDbEn8$sR$5*GPo_Vo4exf?edg@fX>~o#+sUx4r zZ+=?O{yh3M_nTVOFFLj<(9d=C+rL@QK74s-PW43f3h}GDJRX#^w=o{XWUxy}vz^sc(wE58-)_9(2F>D(||W`NR-^-MFfLug`VB zYHs_GpLG4a^G`S2cWPCqgIC4!iTXw{`+G6n9&fZhs>f$o>D!0=r0<;B@qAf1x18rQ zT&`Xdsi)t5U+N=Aw|}X>>%Q%L(dYhLquBoCKB`0KkNYW452hYo=Bv2UNj_az)ia;5 z^(q~F=PN()eV%shi*7Md)lXR52t4NzJ?MFXKK>}uSrx7Gnp5BSh};!*7NWj*83bzB#$`sk85mUX@Q?#=SmunEj(MpL;sqx8NtfpD$1R;1j1M)la`kn*gu!Tb{q>)(3M= zdHi+bs`|Y?*8!`!?L&Ujb)Fn}(vzoF^~)E3e~^!mzJ9vu^3YsjJo(halZRElswZCz z^(AIKV{^mo7whAv*z1$mp7qL!s(!@}TW95u^_=VUpuRj$}Hi0>u)2YvIJm~&Zb-dK8^2w{>s$U*f{k=S&dphd;6#M&Z&)jm@w3PR2UwuSd z+2bGIJmTv0pnA?nPd@eZWnNsNKA61B_hS9YGdILbv_JVt$LqmeZom9}etG>LB zif4cFlP({xdv*K5pgJ8qvGv3i;<*oW67lG$i+ecA$AdlHs%~Cb@v8kT|At3BdTQnO zx7x3(4gY>IU#;)D%mw8k{wmV?_P;8lws@X!2sQ=;n}rmx@QMjh%?$CrnxCx-gO ziM@LAtGLq1yt!ddkDqkx?+5I)`3@Vd~-2>ES59;#K)-UGqY`)T#Ej z{MDasC&pVO7DB0$>sT*QUk_6cKlL&m<>|wR@^jI=P=D%fJvSD9s{6jrj=zu0xgU*u z=3m8jt~xL9y-wWm`#Y6$%Xxmov8O?PTG#vEkGuUHC#w2$7Ju&^&;6nYE5AJ+)afMBf$HK>?DeU`QQhS6 z8D?F~FMe9r=a;{;rTs#K`c3z4op}1`L4MlD^7^3p#1MboxT=1y&vn3RZgqarwZH%V zgC|a{>R0X67Q{dA-{Xo-V(O`9zKTcrc(Bsz<@pFJzWvEh>+l+01>Z~NDNw!S`di4*amx_B;D z{qk_GF23u5@`>KZ;HP!H9{eAt-E3-6*N-Ret%Iiz>JvkGed|K{bM<5TF#-dr90#MI{(KV7fBzqwIksGt4hroc!1 z)UEuctG|jK85w_JjM9 z`nkXHM=|y0#@3nJm;6y3KEu=zSNq%V1K#z}=TB6A>&rPZ#!JK-#q6IOTW4+`{%Aj+ zq5edA{1n@_*FS#K)S|8*PuyE4dG0fv#MD!#Gm5=FbvUY7&8{}dws40R&%TKldj(f{P(9UzaM+ZhuY56=UhmAe$#{QPuA(-rJj1`tC$X~cywXK z>*e+L*4w&|7cWTgy*~NSjnA2uazCt#C$^rrLVZ0*~SNC21yjWlI`{WaIP*spP_u}@-_YT`Ge~q`jtmF zB|7@)Lyr&7i}L1?r;~^e)x|v=<>SGgZdG?(5HEF|zx-79`ee^Lw9n@~pMK(bO^Gf( zeP~}NPn>mmNN3%+s(!Cez4F6#xNd&pd;k042emf@-H$CVZ45m9^q})3_c6XYokTiN zT|A1tK6N;%n>>EQtczVgKdo!u|KmyR8C6{$UKPtHnkVlAy~OMvjp>Z?E8eJ%>w=YD z_4>sA`}==*!qnRHySI+@&ELaOJ-QWIr>ghzqy6TEm2S1aJ>Q*phtsCk|J%R+o&Jpa ztdGx7K6RWM{8V>-AAXbNpD$C#{gQ_sAI=SV`aQ(oR-(CQfBqp;tNM8# zZAx^V|EW(m{nqDxSWjM@dR0DoVtV2VQ&$YDzRX(>R{iq)6g$74@%Z~p(#{*};EAmx zu23JoI8mLBy12qop8kqo<@F_+7phk}{8YDZ|Mk$eL*0)fKi!n@s-G{N7xuUH#rn)g z58}fruRe;YBZi|o$#Wer>tff>PwP5Qe*V-=6V?0UUq81g;_;0hbYA58&KdVZogSnE ztGxPLqzCn9EZ@Vtj*8E3ep=7#ljlC}X;Z8FvE@2Vxu?%>`gGH8d3rGCt@6psxYwr+ zvwt+!534@D@YD7C|1xfH^|@24pZ8B%{Ksl|)&7>}xApZw^NAt;x^Y$gUZ3lL)!h8x zCtaU6?D+d^m2=B^KJ#z6dQGIB``h{YypJC?k<~ZfqV?)?K3t>N{-xjjRM*!-{B@!A z=IV|1@f%j_@PnW9cU?4m^}}aO|FZv|^ZLfn=R8V%uP5=$;r_TE*25F0o_gjp)^EM6 zXG~Y0`e=XhvoB+Oep=W2-+#XQV-EWZwzM5!|L^vLriABtG4<_xJZL|8eJ`Jwdg_@UjjMinII5RCKKFE7KR@xk{{HCzNR&4t!{dS(*{JDort?HlsZc}n!?Bmq;d`Mqi>^w0aJ%|shy!t4nPA?wSbsezk z^SI`x_2obI;oD8E>VNsvri54dt)IWvH;+7>MErFj{keLheXa{u>)40@~Hj)4>z>*1^+<59Oh}zI7q}xq7`m^TJBM z^4sgdtA4)ye97-i-rZKV{^ad!3Tl1^x_xfB1tmgE1=O$QKs_?M=U)klAxcfZ_E*G(VheDe6)3+JxW z}6t6nJ>GC`I>hg&>uX^UIc$AL^E4^Nx&#>b2i=Wo@`sDqWHfPi?y?axli=X=Z zw`sS?o>afS-r#YP8pS;WR-&5MH-z)BZmg@7L~sb?c-{C-w9E#&;jAE1#Hp+Zvk_j_TpV zs&90DzVMT-=h0^#aQd`_`*G<{G)D4r4u0Z6``zQmoa!+3s(kW#aq5U+ufMA2byPfg zep;7bANj@Q?+v1F#D31^^8Y+ zb?b)miTvQFd48V%{r+~J4fo@l2X}^jji0)e-{v{TtdE{J_4H*vW9xzHVkmDelwXS0 z4YS|eYJYEjjnBs89`lga%5OUQ#nl{jsBeU`dUQu<-iVj=**@!qsUueBCw=?xqVGPi zQys{gjd-aeu2A0yt&0x6cos)}S!i_9^?x@`QZINulS?>Ne8bN-*=17z^l$Z_lw{7V)sct z(fXjec!aZjJUG&=>LZ`^@r$44`u)k_+dISkxa!>AN#E-_dNBQ#*JrMI@?w0bE*{}5 zA1~)-JUfpbe(F^FTmJk@4`{9YzG2_4M2BzmVdb~J5z>M9;#nN^Wj*8BIedmypZ&>C z^SnOU{r=d>xvhCV^KWbQJCS<&?e+H;U-ZyMR@bSoiscja!Ca?2%>H%7OZ7*5*8!__ zdffAquIID6zvYzHs=miQT?x<6HL34B^8Bc-4_coX;;$Q5)gSe_4p^<*`O8nbo-dC- z^6=KGP6tmsnuDirgtK~dD>SF7kMgtq)(b0L`J{Qwb$L7}Utz_w zZdmoZety#P{XXCIxz2Du4tjo9!gC&_zWYdD-Fozi=|TK;1r{qk_BF240b`9%AVpXRdnxcgt!THKH8_UQ_E)&BPQa6iq}2dz&G@z;&3 z>W}(d2dvi3FMiVH*QejrKl!S@@tCfZ`;$I@>5A3qTbG!geDcK*PrrO3Ugl@xsz0v_ z4_5m2F+Z)}&-0(#eoAXqf9igH5}wCL>bqZf5MSN;)M4sX`Q(k_)EUJiJ@vc}`NYa^ zuLo~9?c~%4H73k-Erq9o;>*KpPkxR}ThN-u%cvgRwXI-uXR{i!FKk=NWxBugR-z3%Z`yuq33+3%^x{0|ybvoB@ily!*9Q^&Ri;N_5?q)c3gIw|e%OCojf_>f#FZ!-^+QH~AUk zS4aoqWy}wLn&bCXdwp;J`=$D}yLBZz`nb;P{{PgOpN;7!Kl2$Uzl!+?D;{6@X`a8Q9q`oKwpQP_w8zK05?zmv)VJ^HspET| zrIUycQ%8P;RiAufrH5D5t$&usPkeuG_=!Khq>XStZuq;dJksYkfAQs=H|D9sysj#r zJn<|a59(iv`eD`Y`Hr8ipWmPO)K9cl^;_S*Gw|%w)aNU|)x{oHbQ1BQx_A~xed=&l zH+g)9Sr@y0ewyp`#TTyp*+x~**MpVc_Ah<&=#LP8SJ9kRb!Pkd46Awk;3s`Q?;Q5I z+q717I(StqpQsO7pExo5cQvNJRA!Sxp z>s_@zt3S)L4%Y#ze&+^1@w`4c{P*tBB-O9~lRm+@kDqhh2ag|hvATYG){C!R<<*m) zaq`sZW?c;Np}veWuRoEW^8Kgyjg2S2;p{HyK;9gHXI{qgiFmV^{j0|2E$yG}<1?)0 z?6h(7AN1v~{PK?0>V6#2f85;p^M}_VsjuIDR;LfGPYm(bjjQU9`dkOB*3B1w()DOS2; zy8KFA_c8tUcu?0*52hYo=4a!PZlz!4b1r?Dbum4Div7HE(bLXsRP}xSsCVM=j~;X% z`He63_>oVfvntYCwO`+?4xgd^MAywv`kpVZIjw)vSpD<&?}~KsQ#bv_cYl`dZ~Zx^ zif8$Fu+kgl^}~v<&QJdFW5-`TuC=Pu!4uOFSEz4d_scc_UudXZx%drjFQo%1`>9FL%Fxusg*Uz9sS*W?f9D z^7}q-I<-;NuiQM?^*R6PLFbYCDX$M&pBUn=8&}mI^|=mMt(y)%#oiCG*R@?i{n%G@ zB|hS(em-BSTaV{WF+DLptmdd!n0?|z{qka1tt;yzo_)wq^PJzu9N54ATYdY3`vg3` znG4fzd}v+bL^@DiycDZ`dAL*;-}ONGL_Z($(_DW2{&V}^8~pv}7oOY|=;EiY`)J>) z>ysBJ;;$R67tZQiU&U9470>To`Dqb<$ z?&>^oejonh&u^{#eqH~#+VJ;->9_Sl>k=pCdet*u#j|`oSm}-Ot_N0pujlw_uE+hc z@7Ug2)z7@9D-_A^z}Cv|+0QrReCtib%Y3!o zb>+>Q)f@5EVWn$-@)OVRV_$snU0X{1%p zb-FP7#npM@_26R(7dI3vweJq)f{zx(zpM1e;%-MZfl;8 zaBKBDk^NbH9`N$dytI+skIVaUzVr3B=iBcn_J6rw&JT6>bAEzrCOLuYF%vqMLr2<38fi6?^?HPNcJLG-s*qs88K>R{DJ9C%&IAzxa`3 zTdVu=nIGy(czjL$(fOV0qX$#3$|o=5QD4?GPQ5C2J+R`*^V2-%!~@@WS)-`$f1A#r zoBKQa)#b$=SMv0rb*YPCl~+$(%GY1@W!}1>{zTW!Pdf77dr5a*ea{zk1-ksBtKZ`$ z>-sz{@a19Zi7TYDYJJr{bFCZ7SGxSf^L^`w+~xSz;`QRT7j(suF2C_0pPe`IiPbuH zHE+&Rol&1U|0;cTev18kx%>0Q-8sHgKJpt@Jo_;BH@~la!UysqgiN z^CbE9w|rvisb{{5XZd)r(i`RZ3@g6pMShyg?k7 zYGdQPv(9N$^+SKHcjCEU^kC(u^*D#_X(O0b@5#nluz`y=BK&NgRkHJM_P;epFOB6;8pwE>&*09pLIcb znCqaGLIC-RG*=JRX!?@!XP9`i|+&rm+m<39cN`G$`jz4iaoJ@k~W zVBh9k`W_#6^424-U#yM~)x{Ok%lb$+>y=LGiuFTzI*>2vxBTwEr*ZCIM?S-ftzP;4 zpno{3wW@!uzg>3c-*-IkrM~->`_eplvHK5GZ(Z@K`lCMA0ju@W;is5iM}MJzp^rKp zJn?7_p1u*z>d_scc_UudXZx%drjEFJzV!Q(&mH-?M&$Hxsc`9>5#M#dYTcFJ_V>&G<_@iu-#-w!6Tj8q-_am;6~B*8@{0V}8>2`JG>V&1sFQ z?)}Jg@l(&^X4Y?dF!icA$;-IX!=nr3VU<^h=0N#G`;(vaH@^mAW6vLYSZh(g{*m25 zJiexGwZF|%r<0g^>Y1O7tA2SntCu`J!>o&4H$TnweD|<#ozbZ3`tZb~Ie7Z;#fj?K zCmtcaS>7za(z9Mz@hZRjslBoBf(w48wfcS52kz6A=vM1=zeeW?oy2OryP7v=sm`cR z9aj4M;3xl_->_5>sziWBN;VX8ZUIM|1c|-_JW2Z#=iPs_(dE zXW0MvsqcA|zj$JPTc6kK;?&azvo6+eUX@q3U$Rdfs#o*Xp}E=boaLvw{4c!WfY$m( ziA^Cqx;aNaQ6IiIQC+{fc!aZjJUG&=>edVKQrF|2pXz=e`{@1p_p`lW;I}?=D#V+` zN+g z5Bh3XqU&`<>dVuoEB5+Co}M`M)H7ejvwS>Q>5cOGVa2cZx97`yp0M@(RdmvqjPVlj zW-CuKjC2tLw8aaUwoc7ca%C zUmh;i#dkeWzCwPQYk&XLIsFHCb^Ul%ET5=v7PEiV*u16vvweJq)g1efpY*-HxYr#{ zZmsGs+Wfo}UFUS_%g^p_dgiCzuEzA2>df}x!D@~=KV2`sU-!4Cx7Ke-YzpDgr6aCT zAHFzIosPP=!daevd?>#Ztru4P_8&jZ^?uC*|NS|w)$b2)_*PfKtIltE`uJjxEBVA+ zuX^TZg z?L&Uj_4?$JBeuSujSimIH}Wul-mwmSBc!vdXwIrSv;D3QR`d9he!CC5zu%`jH}aj& zu=3mfOus!n-tZIs7gVp8*c8h7qdDg58)53<)0xFlU)D38onyVQ>Z8w3vGe;~{m=6o z?#FSD=t_9*SL%D*cz(o()+J6%y`?d|rG5BI`&|#L`aNIr6VKm^KYh`&+Y&sVo^(uK z(MZ>MgqPPLUm^aw(Y&R)qds;1Rr-zze&XBT&wp!w=5#+cUeFoz{alm!?qlw6vAO2a z6Q^F4uXyX?&+1q|%yo(R!cY4AzWbhA-#_@3C-+V~`l1svwH;dW7G&W~xAO37VpP~K=`AOgYeE88n*jm)} z1^I#2%UF%NI8>R;PD*zuO0-}<2SiQ#CytJYW5 zAN9EoSglW;pLCtGH~Z`bTl~JZe;wiQ@AJ-QI_8@vFE$tAuNzm@AN9EoSgl(<{dS&w z>@EF-SErNn>8MwT$8S1`cy!doBb?>q!I5rNw_aHBoCo|g*Uvj&`gVWj^!og`4|OGd z`1H;5x}2}-V$ak1>FLK;uMn@o)JtCKR(1J`Zw|yy^tk6IJ^TB+p1Ae|Ro>6_>F?9|^RsAs=eogRI8an{9&`Z6z8 zhw>Gg5ApOv`9$X|Kk4{+{`)_5Kx0(&gD0N&l1AnC1)Fb_Fvs&AJy`jz4_coX(pfjIsz2&;9k5zAzxYX) zU#ER}>)+G9blydXI^5WD}&wR$SJblT}7;l97V8yo&`RRJ??~A{yJ9F}8;;h?u23@|<*Kgme z%ZuG7d3rGQ#896&aa2!!6<0c$w{AGn<0l>8hxdvP9M@WXU*0=j+8KC!O?`gy7hmi? zTc0{iy(*u)QJgwrIO?zJt^-y)d48I!@7*tZX`|Txhwj@u@Vt(r13fa`0GOR zmg>#+@e5XS>`#8uw?CiwiRZOe^$RZPN_6p3-~LU%^|?>x$%|7@J@Xl7f5yqDiwBc8 zis`_L?|F@%=6QeYUiW)SYgMO%SH<#)`erfvXJh)a{E9cL!)I9OdEE1pzWizLKen~1 z)4}ujkcZX&);B^rOOfuIV?M$gXKifQpZR?0^TT() z+5KCq`*Hf_>l3=}PwG3T)S>g-{gkH%Q?JS=FJt;J>vUn(XJh@<96EUX#P|8M-QVxS zsec{$2>*5V#?JSv@!RjK9`}&jwpRCJq>b1OMQOwTU|`w`shJ?Smo7cF?B}qtgh>TRiAT| zpXU4h_kF(h=+>&f&!%B`&i~Y}{MI+Z)SHdz&hjhXQXM|Ss-G|X#N*?ue(#L7B=t+5 z-&dHtTsK|&S>1X(F6boU?<&$=sp#~Wca zC-c?~D_wp36np*ftdkzuTKzmk2TwejgQpK4%0qd5>qc{Cbw_;bg_W*4Kk0gX@|ipB z(^~ESV{g-y@SG2+@9|+@tLqyf{;r}qtLn`5^AT3_Jg)gkU;ZxFpVwN|ullpDM7Q$O z>l<~tVs&|X(0bKj=EaH0&$<{7%2!zNtQ%JSUPthgo}cHh{rLW^RsGz}|4)~$`;z)g z_cuNBQ*Sn=Gs~}dOLg!QQ=eb_biLkhf9~r~ZY}Cxc~n=Vt3P$!NBfzs^T+*=rzcK5 z^~_f>9a!<`!iqP_>xcLm^Mjw($FD0tz4iO;uZZveb}rC|{G=~WA6kzX;;$Q5)gSe_ z4metO<@Y5QpWa&4_ul*>1M}_I)ZguQI?`k30YCZ3ujhU8*e!nlpZfozZ+!LhS=Tk+ z(V4S-_BE{L*q`aQpXYafKg68xe1=Q?_V=Af{?GpXn(jw_p4- z=~Z>t1uLG{7yLBW>yx{@=G4}zKKuVMx?g-X-+J69^Ahpbh4g3jmhxQ}toqgYiRb-n zAAR+c+Y;5c@7Gt1$G5x=_bdI@2hA75)LU1)s{W|Yb--%9&RKra^?sjEoW1q;-*oWA zqd9o`MmVcSw?cEO`Y1o!Z@sY6^|(*J-G|dZ)n9O{)5&#==47A#3TO4`j?kPDFYB{? z)(cZdT%F(Y-*dlvH>%$sp8eas(>n0zLFY`)7kfSx(}Q$wDqK2e#Lw%f*6q6aN!R)N zh(|rNwc6kLebjtSef^{J+q}fo+trx zy8NbVJ@VqjYQ4LfH)pBNs81bM`qlYue?I->^II#w>F`fHnuDirgsF#5XBJ0&SgKk=L=Cq1lx zAD+5(;)&^qE7Ui_S-omb=FQ9eD4y-JUO3X_Cw=?==`VUrYgM0p|B3r$KbueAx}ZG7 zUpKC*Kk9QGuv&L@e)H*P{{9DBE5DEZR9B*_KlQzCp`$L&^Mqb?UFvj3ILpU_Bi*X5 zAC7qZbiMp|(^*HiR<9T7;E6|b@buw}6Va#!jX|D6&3-9c& z?A(uS@%J?QIQ8k;&+7D{^@$<=x^Y$gQJ?F8)w=Cte$th{<=H2;R`n}?xGN&Q6s>Z!M`cvbyTUtUMGUOax%_4~o+yyb${s!j(_Oh;Uyz7fvqRdX_LUgk&fY@hYQ zO4mN-Cw=G1t6zLTYgNC~Pj@9e{i*NuO`a##llz|@Oubd(UDccIcU`brFJJjdf3MAk zum7(z+P{3?(iP|R0^Y3Oc-F&jF+EtVcU}3Vb4GmE0jqVoZhq4B{=vK4t^a?z?#C6I z|DPA0{?s2mUs@NP#ME0grh8NOyB=80v+ww6efIt1@7yzQ+B_hx+ovnhh4l5O-|`jG zfvG2kbi|3Hdh)Bd(#gDa!;v08>G*uwE1&(q*6Mzoc}-WstM<2ib$yR_P6uo<|kd)TKWBse`D`QcHOJ`Ioro)Xl|l9Kk57X9zXN=>ZvD&^r|@X$yXm?t_PnU#Dntqu-e~V&%Ntk?$oIMe}_x{ zqIc5e8$ENJ7wYn2bLHv5)T?6o3h7t%Q9k=K)^FWVKGE|cKk4~-=U%_}u-0P#U-#$T zDY~8)Q#bwOvwdu?dGy4or=IyLrUNS;T|8Lvt9P|HuZxxpGfie zN+0r*zP!E>;_oV&v#QQ)Kfhr$&;H~medo#Ne|_uU8;x#`=syFIWhn2qlnSS%@^A~UZJ`UfH%opcF>g%_^@x|uKC#Ify=Bs#?j|VHgQJ&AR z;`62Q`yQ7+zO}0V+2-eW_?+uBk1l`B$4{iEzv9a$PadYeeBy|wu1^f*tp~~{R_6)7 zj=$UX*1~W7)jahI^+D?sL-|T))ja*B{i8nXg_S-Xev0}1;J@pi>{j2l`9BHJg{klS z&;2cSpUtBKQ?JS=FXK_4x^=+R8O3y9#aHL2dHndwyC2?K)sHx^E8#f@QeU3G>iW!u z@(_RBxT^lB&vn3R-S#m*>GJ!V$DY?()#>1gM|1G>L49HU8kLbi@_f@1xfzsh9IA{aK#=ieKgRC0Z|3PaSoBs@vZ`|GW2Wt?K^& zd42q&53BQ=zWxg7tcvDjf0a+3b!EPa@e`BJSAJTLbLL0?rN01Fzx>~I2A*>|b@k`{ z5SvRUG4<3lKZ-|v>Tsl2)ki+-vk&=cuFnJR{`-7)iZ6UiPU!9*~|KHJ%-0QH`s{Z5qbR|5` zd#TTF=ZCsJnCq0szo~HPoDtu3z-ryjUw+bc&Rl=kL9O)-5}QJJbVqaW^x=yW)w54L zLVBz6X8Y+|FO;uz_^EDxzWtHUZmrInQ%~(mbn)p!zcb^LWBeK@OcofTglRy=<3(>lCf zJO0yK_xJYmyAq!JlKPe3*5e!#(}StEu6R}bQJ?F8qxGiW{CfNCPin2|cRI39wr|~+ z)aNIj^@=?mSf4yz>a7}AdghLF^^f@Ku+rr#Kk4&vuS2)~esJ&pcG;c(-)ObJ`N>D~ ztw&yLF2uj7aOs>8-*v!h-F!*E{r=>j!|vW%)h|D?E16&U?SApqe0|m>PQ-`m;tFSZ z`thOsQgl7A>i4+kr@8k1g-?BCYgIq%(yoN3KlSDLjc+~b@`Qjd!y{hiI zV8wIY{503Tefi=2oAJ~y{Xkcu>-jeI-7kKtXP+3FkH0RY3ukrlVby2-u;TH9pLF@X z?L7}_t?GLo(3SAquhd`nd6N1|W9z-C`uGg1Irbqxt&3mRyztSjRsDv&yAodIxBKOO zny(L9pBUn=8&}mI^|=mMt(!0Wr0blx_06BvTGg+5TUWxX{PsM|Z+z>aFII=S-YTEG zQJgwrIO?zJt^hqZgJ+mv~@r@pIAMqf*y7j3;IH2%x{f_Cs@b>qgkM^%O9sd2&zBiw) z*uEDhR_oo>yg5sCMt$n8v(ooEnVw(TI{N$yc zI8lG*&B^@IxYEhFc(9t|y7@`R?+3RXvAwn0|Fhq>?tbyneEQJ(#1MboxT^lB&vn3R z-PL*G{Jr@9+4}hg@Asiwt%I&QCs&YZ}#mS)q1iYnwdmeS4=_KZQ)iXaESN-yE zRxf#chFKT8?(|#!ukZ5IM)mXO>kjXobe+fatlN21)%D|xVd~*Q^%0KvS=WB>{G`XHKik%wcR%#uRk3`cK4^X7#O&YInEq0oQJ?F8mA==5{G{vi^Jo0f^IEGq z9lR=*Pt-Sy**_c8pXFD)SsgyZO3&+Le$w~*s<-}!7qwRPvtHF1bn#Q4t~&I1a$d>P zgQ-{Llb11ln0319FnOambt=B==cjp|FAsj}BU-Ea>)zXyoHyps*Y9zIr;e|VkEf2G zb@@a*`B98#Ev)l7avx6 z^~6~|9!#Ctn9s24^Lmb-=6Zc`@eBJGvZ-HtLRX^8Kl=LZUv>HH%e;E(;bmT&nEb4Z zlb10btm@Vc@e|egNzeCjyy%aAv9+il^q0LuA74|~{^d8m$AkTEp1L^os(kXq_)tDk z9aitg$upXT!GPk!S3MpeJG|My2b-#^GddeD8K2dzh+SRK+?H?FEb>dWh> z){Vzcy3Uyooz(y3_4%>4?+ktTsn1XQ8&B*$(@9Ldb;af^)f@Gx!%E*iqdXuiyTy>ehuXhN*`K)kiqutHV|GtQ%H3_9s8h^?rzl-~5c$s(xmFdAQSW z{M2_J({JmM7bm9Px?}TZbw_-4Sm~)f>R&s$E9r9{rM~lmuj=&mS1~@s z7tbPnn0<6%)@Nh=P+!jVxaX(3|1bK{-@Q+3RlofF&cO5dpbzP$-}v@FokTiNUAz>l zetEc57oX2iK9Mi{G?yRW-s9D+Mg5@XcLhBDrEayqtw$f7#MIl>nEq0o**@0=t2un( zCw=)VetW;x>h;TY{o}^N^V>ONe~al`SB2GjSIwIXXLayl)tA>%@$6%M()E1#(+ghDBdwjer(Y1e5-+jz=;(PqiNld-e$$Ax6ee#J* zb?~j9o_r!6Kk=QvPdu+X<9>YQ@~(i#*VOgAfCtUX^I9FI-n!$Zb4GmEHOu2CKEHOH z-2L|b_dcIS-+twXetxRUL;V@crygGB#fiz!x;S|m<0U4~x?%Fg>ind`?<=o;N~5|T z={H~LLH+!er*Cc*<3l`gg>#F$b73!<%^2y5>4_0;ShWLs6;3qx5kGN?lydBW+v~HO7jQL3)e(w9vZ&dYt{-}4-#Y;V}KU_DSSY4hT zOg+5JkK#&CU+RuxeTnA4tY^$m^X%`J{_RDLs_y&0>B7`=9`RRQywq>~BeZ@v;*aXp z^<^I(%(_^epVsO1;7gzPGmYxJ+2i=$sSlqX1FuSvQ)qRCm;;Uio1jU-*g7 z@0TC^!;R{G%znPXdCXVy=#LP8U1;7?z1co~!)lJ_Eq>B>AMSN^XQ+Q>^9OxCLi+lh z7gZe(;^lhPp*b+~8CSf_ui8h)x}iK>==qtS^8EVTAKa$3s&71|E8*#<58bb<>$5I# zA|0qMp2bn0I-J!_p6h{G7xR^$=JIRrzdxr@)vw*JcjBd=ba$U8oY;!)2S54Pztv}B zH$Rw(=YDNf-PmxyoD=H$p!s5mziwPrf7ItXV6|?3q~G5Eefh8S&vWlI za8oGfXI#z0o5f0Jl&3%9jq0;He1?^-eaKJx^8fVs=eAb&gASf}GzU-L2xs-^Lb~Er zajA~=!m8ivK7Qgkf1mn+t*-~IAFqn#6ZOqv_Rq%jXZaOxR)^29(&KCTy?IP;JonPR z(|z9im``*?&y&@9yv{Tq>ht)?yg1ie06gL z5g)3HmtxhwG+)hiJrFPT)%mIJ|8MyAt@moJ&YRiq!?S-qe$0XD<|SG$R2Nq`%hQh! z<(H!Cf>l31_-U@6cOG@SLt3l*arS#VgD&4v-}#THZl3*b9-bH|-Ixf$bE zSn0^?6Z6v?=kIOr|8tG%_lNrM#B{_J>KkF|;a9ryv%C?1sgCu+O5f`Uewt(7e(xpS zdG}+_{&uUK?;otrXS!Lp9{b!pd04GiK9Nr5#iKf2;;2vEx}kjPiTR1=?>j$r=+@5{ zU-w;|f#-2R2j+Q!&;LAc)FB;M<<(~~b;NL1Cwa3z>#O{J@kbxpDC!69)s^`M=|GPU zb?dRO^L`|c4pbM9(7ch4bm2(1s+$M#QrG_Er}a4x-utm5TdVVi4xTvY$S3L>Vd~+h zUdFRLeHFjT>r1p=s9x#tQ{Dc1;SX&6{=t3ve`ma5pW@Ru&wb4Mt1hpfo;p2sn036w zSsq>eqqx$Qhx8I%ckXY$Ke_0*`?MCX7xy@+_dSyYj$B zRsX;-y_3)Q^vtWy6MQ@|J$ZWSVwgPnMEx-9qd4c#f%@^Fe4>5KPdc~WB%bo_=eK{U z|Jc(zgRa+gm2U2DvHNTuJ#p%(XFlUuo<4kYq53S+fmNS6Kh5)g`}^N&>-V#rc~n=z zvyW5X>p}YJ`sATJ#J{O<>6{TiucKNwo%GwjzxIFj&o`*==r6|?|9&vnld*NcTyK?6 zo_Lmz2lX#S*8{75zVg#t=l6%c`Q+BB?)~3*)&6!~I6utSXI)Sp;@?!bbk2zHI$*VK z&r|%Q>-FGmpLbMi_4*~>|Bave?qlV*KI=-Rr!dYGGtN7}$;_-u@*5UmSPkC71 z>F&n`ujmXs_bc_&@8qk?C#Ify=11|UPaTf*syd%x#p4%0&E>~W{oT*DR&_df;?W#D zeIuOJqdP+LM!c-g_E|4X9kDt;={uKq+-vLWZ+&=TI^qiTjWG4_D_!|n-l}|k&^+sf zSr>c#oPK-0JnAp++^FjNZ4NNUl-2duA(_hb(Z$=8CL!L;3wX#HWzr> z?fW-v`hC{>F6f1kF29o}cAvzFOZ^$~T^Ahj)$yTxqVeBmd)egCn&`yFvVuJ}x! zG}5Q*ep;W{c||7?A6l1qDe8y%#ZW$RRIk>beXg6I>h|{+-r0XrRNr>Dz7jn9G}q^T z@mF0Rv`#U^UpKC*Kk9QGuv&NJH@`l7-9fFD-{k!-DVrV_?llnl+$59vYt*_faB{C@h$=eH5+IZte!xI+3POg;Qcx5|(DvYv6Jn|bSn zSr_}co1fey%eRQLOn>n`u_-1PJQ z&F|Hf=;G6d?qlw6eEk*DSvQ&wXZ5YK;;X}o=W)+Z>+t@;?eFY=c~IZ`9$kqpe(F2_ zE5G&8Nld+U#pW#48}+HfO5eG`Pd>O0Kk|*OpC{S=;I4$X^!(OmT~HoY>)qA-s`{fo z*8!__^4s$vUElY(?+^C)L zcisGSz3#)sKfl#)I(XvI96Ws^Og(%$OR?(De8#hLtQS^&^wV#DzkK;$ZT;Ac~fUf7o)OU_pFZ{p#JaL`r zhuHbcPkDa7_lsM9AG_@~-AR5!eK21STF1!mO0UYBSK+8n9acJ{yncwEakam{Z?oZ7 zuj~Kv`sI#y>dw*OBYo&T;>p|h9vA9~_^_IzK8xny(}nVhqdMIYkKg>nm%sRjx3$r^ z|M9HLJaL8kMmVci&5<`pycB2qtrw2w@RPp%dEs-mz8``Pp4Tz*(EYNX<@G`96GQx) z3YX3q@m&Y3*3GZTtJU+K5MH~jY(_V0&KPaSd2kxw)i>Jvlx z?BCUx{!*P$pY_5@-?_n0x?Z0g_4PTw^;L7^@n%uqs%Xwl-Op!O&8zmebLP0O9@|>o zkF)pfSKvDIr@s5>{Kgl1eIlQjdb=9aU#c_P$7fj0aozl+@BDti!=Ke!)sOkbu7p?l zO_#ssTaP|5J&3<vJE(iK(}) z*qo($qds+5>D!0=bsBjH(lqp{Vz{XoOiQvG#{A%?>*dE?zjZxab z6p#AU;YhEl^BGn=zVOprKkvNf_6N3B^^g2wXVBH3`krsyPjx=q|K`yX<3n|Eh5BK| zlcx*im!f{CKi47Vr`XT)2VeH%tyTTNyLTl#{?Uj0OuzL(>k~sd>&8{}M}4jXR_nGu z`AOIF;QjvN#jREStp0Yw;pfw;{Y~Gx#EH3H^~}%4Re$C)p4E3fuY17G^J%Yt(AjOEzyIugaGy`#>zllv z?w9?nZaw-F)$!Mbbm6QnKCJrkIx3z#Kk54Y;P$ZFi&Yqn?AD<5NxZyXR*nN_x z2kF2nuU_G(PaRe|qr84t@jWl{)Ac)F-}ZAmTC4h>-L)(64WGVPKVH`HAYQIlT|cby z>Zv0?i}YdkW!^lPb^Rm1pY-(p|Bdo{`+mKXo^yj9%>6B|59a#h@z;&3>W}(d2dvi3 zFMiVPpDNzixa&{uXszmdyrC<3U1DFRzIx6VyU*f^PuIG{P#(%BW?c;NGp_P@&~=ER ze&}(}PdwkJbiv-+8&%!+QRDH69+Y=K@U1KJ>X6R5a8=#eKGy}Sb*uA}zW4il_~QN_ zN_jp#`--lF$Jf+%AJbR!a(~lw?vKowNL;>-EED?(mA%>V8~*Kv$xR zpZaw9t&Xq1Li}~3c}sOied@5%w?FyG4?cbTgU@KK>W3cF8S41-#r7|rb%^bMe0jXo zlgC#dp}vZz4lAB~HD5o(Pptg*_njww`3bFs-=F_*SES3=)Xn{Eo#v^_(-Ws&l}}#A zbTcoe3*}*zSBK_6`NZn=iO+Li^uZr%E&SG>>ln?+KK&KW>d}St#Jh^Kde#d^bNNZ% zwDzAUC!f<=-H%J|*co1z;M0fptNm;pVz2M@)5D_! z^@&G_U+L*jp7kW+S7;8@pL}_Kiv4`~)qi|MYw`2Y6~EgR=~{2tRqq-lbp4vO<(cyJn@CCRsGl-Is?z+BlSH#+|T5z%O|Fu zdgiNmmX8N3y-}XeaKz`QxjrxT{Bs`DTGg*UxGUjRemgJHZ++G!PR#YHXTFMO`FOC> z8|7UOtoU9B^V3|fPhS1E{Xe`^|H&tIB|gH`_xMn^4w(6jQ*Sn=U-4aERnI)VQ6C;m z9kKn%PrB~Iubj8_^MHH*=iW&dj~>kZjStn$lNZy0>f#D#dHV5lZpQc((t&uXV;`p9 z_V*L6KCiW^Z$F_c*^iJubRV;BJ@Qpd2j;rvE3EkD>8p5|&p!Q79m=N;U-+r+?+s7* z^ck&H{nF=lhWmt1AKJf_uU_9YrYo->=Df_S>xan`^NpYK{CNHY zk7$zW-`uxPz;nOMg2weHGq`}@nUdPHmG_dA~2mCVM-@HeDcJzd_1UsDY`CL^}B9W?zs8D$E)_Y=TZ9R z>l-0H#21fnmY=+gM|0Hq4CNE)^Hc2g_bInNwY9h(mv6q2lP-Sh&Ymaw>A}>)%X}4A zI>}GnQJno^^Pqg9=P7=g=R7!L&qG?vn>G{YZoXfWZnb}%GgaLjNDo@4x)?6a>zl=; z`qmAre)IW>=l9=F-{^mzxBoABb!Xs>&Xb&vZ~teVu63;&SJfZ&<#kl+ro&IM?<@G1 zXZBa(_Wv>O?mL#g$4Bb(lO9aJ<>`r2Pd)QhOb1pxy0GGn^7=>fJg@N+-{*G@dH74( zD0Or3s#rcz-z;YT(%77(efYEe{D%4y>G4zS_24i4=dJ(0)cbDH8F!kzn zVU<@;oaN)e)R~R-Lw&AKY=81oonJ>j>G(!f_xWNx{q&&y&0l$a(E7v>f8DsM{;1D& zz-rxm~DNZ#s$9dRNWUU)n$0$7fj0@wnzEeV-@!i>uFV zt?GaB*sg@<^+xJ@-GY~BT`=pZx31WnrFx^jypBr0^4t0SwAbFfwes8l7j1s!cb+Hs z)}>EAv0CqJp583K;?3%~E?DWw^OL^MUq1S~_h}UM%g^r&JigI^?qi-O`0fXtL^@Di zycDZ`dAL*;pU+S}kuUr-*ZF(Y5BLA?=nWE^0(kUBbMW+yF!k{1%;Kmo>lx3^v0hm9 z@tdFK`FVct@4i=S_4DU(x9tqP>iN<>&iVGcx$5*_wccILuc|-l%j>AtNr#_eKK|v= zTi?&N`}N?BJIQZ&xytKw1}>xEUn z$2~vsJl-#S@sG46yB{arqciaI=XE*m@u2%#)$vlV$|o=5QD4?GPQ5A~`K-@%^V2+j zpZt*rG^)BjJm-!)^!kIp_=&krbvox#{p)f@3$2ds3Rzv=h)Y&LxU;`_IMsUP>DUNB#O>gWE( zx38UJ@`SFhZ2FiXY$0kzvXki^ip43Vd_-8Q9gBY zJ^JyWd?H`@NzdPlPd~q(Ebhkz|Ja?x<6G)_-0%_KeNdNAOg;6?&&E~1Je<`_9-m>> z#p?Vt*WZi(#}m7w>L0#uXW;RV9+by3hyU)gdLkXDE}q3vU)D38o#XmO{e0mkzV{El z=tF0>kzSvl^{BoAJpH*Y`ws0Cea27oz5agC|0#{? zer!9mcaHSkN4}azUw?)8>qhgI>W=!<+1ak}tt|E|XLm+FlATnDW5)%i)+^XSz(`uCZt)4>zd5m%^hgtL0poXne- z`6^z@w_aHF^Oc`?UVoqYTaRl?Qa}6nu88Lx&UNd@gC0-j$|t6tdgiNmmX8N3y-|MT zvwm~Q!@+mvQ!`zBx&QEjsapH;hZY}n|empT9afN)PPbV?;)GJ;2S>A}h zRL6Q@rLWFUbDY2L|CJZCR`pN*epk|GAJd2O{KXfi-|FVmSrzH6+HW0jq+=ee`sDd3 z_Id7ocXa2}XV34^>l1Twee$F0+tvEgIU~O7fYti!Lw?fb*Li21(^}OJ{k5)yXP>6N zemrPB=E^6go_gj-@u*K7j`XU!>l*3!xrCqi{+{;9D_`D5sQ<&}_s8N@eph}vPt2v0 znAfGA`B6OTQ->qHsy^#C9zV_X^Un7jy7l*i*Zp&6=;Ldy!_SfYr7NHNpB`T7sb_u^ zkNVW%NUy5vhZWEDr{DJ9?*E^pJ2&!C9WM3T{yy>_hh}^}W8azt#1P z5Pw~0-cr5UK0d>04!zvpK7aYXAKkCDdVPMUKkrKVRz1J#g$18^x(Jibs0t zc^&c<^3#0p*Zks(9@<*m5B+#mET5%<^S-v`uENF z`z9Sc@n{a7z7eJ#KAjQH^6}tEx2jt&ta#2}ewxegH=ogeKd4RzPdu7~rw?D8sGfb| zrAR;fGM{nstJr$sh{sR!ygu3Y=gw=b_WzZKbR|6ZEA_qJtDY~di%w$d&Bkeh!RHb-2cz7eJ#ex)lv%UhMN51MDaFzaIbn4kLX&kNqY z_4VM@n}T@m7d@!I+TZ#sq_Zm0UA14|tPY={{zQJI-}d(#PkC)?QNQHwUBNo^r|zuZ z)<+Mf-n!yd^+$cK1CG|qPrCd*;}b7yt?E~7zHr9#Iy&|BkM_6q(SxbC?s(~(5#M#d zYQ5EY;`dd*{jpcIR(^lvx4MILtKXmG{>JxsPR-BV@4(|99hmzYA6l0V9-SxnVXMgh3T>JYox83@E zVf#F=E77ga3y&Lhx=??{^46;^hE-l2&hqdRtNzSeH%uKdKhkge^B-RGibhq}SItwe zP~Ql1-T0Mml^^wGJ>yC@^VSQqF0S^s-&b95((@XX-^aeWcUo8VeCa%LKh3iqbLk}F zLv`^|tor5QQeAx41?3a@!cTL3U)aOGy!G?NUp=fd@T&8hZu%|u_@R@S>r~Ht70>eV zV5K+8^BGorzEpm{_v*`9tNN+m=t|bNbbs^P`A#P>*Q=iSQ9SBXhabLcnE2al?y;VMWqd0ZMaMWMbT?edq_8C9T_5P|yy>i6R({b^UCt3UNUZt$S}?LNsv^HZT@fKIA8TufMPTFK4wD_3K{R74WL(OV5Yyr@7Xn zk4_>!R2MJBs$U*1)x~#RP(D$epXNFzKL5uLYAx#L?9&zSoC~SzKBnLLp!s5$dh3c; z)gSe_4p^<1FZ`rye?I(YU));N>EMY+bMW+yF!k{1R5;7ij}PUSqV+=ksayGNfA9N{ z{z)}{zplUiXXob|s{QMJRdwsK9x<%eJIbp=`r@Tn^=IC?VKv7&%1=7{zVJCuZmsHc z@cylxC%Jy{2wiW*<1?&y{Ng9w+if=d&9jed|5Cr>uXP4pucK36-u|ZR{(4-=(-Wtj zdgiN`4y<@|VZ|He^}~v<&QI&($NO%v_2>C-Zk{LUBYpkuWAfC^730aL9-cg`@>#EV znIFZeE4FSZpU9W=+xyvGcKX)uw|{E?IA(c%({~>6QQq}H>r2F673pVxl~0}+AIc}r zuHW-o`Yr$3U+z2I_jQ<)^UYJQ5YK&}lZZ!0T|A4UK6N;&n>_19$O{ZL&D&+zWiLIUwW!~9^B1*v&W+U7&u?{o(E7v>f8DsM{;1D&z-rz6;3r)^?s3ij ztyTSw{l}l3uLto{-{Xcpp7qF!6H{+pu{lfiMt$nA()T)u-L4 z=^b}&Bl&&M=J(0ccfWF7{LFJ>Es-|MDs_Odibd~ibs9waHLn&tru22`;DLG>ig@Ty`Z(Kf8~g-gr`6C-ADUdT_1gM zBL1!--BopF`&|#L=F#J)*m-dBdw#mLs_%aNedA8@8QwUn{{OuEeg5~K-8=dH&4c?L zv9H~q)OQ}`{ubN+)<;jAdipY-vGqW8F_bqK$}dIhhS_hf*Ae`b=hv6++uwk!e#c+# zO36!ox_Ii=Sx`)Gw}G9`p%==m*Tu1 z^kC{OjdxXNX+OVV)i2LaF~4^Ieg4MpG@n%Y4CNE;&-B~*y4&w`tjFv2+xzVwbc@s4 z0QY0||1b9VaDG^|`>SrPafL8{FT=?Li}~1 zc}w+X`}hp2Ief8y>C1oPs{Z{D>IZ$bE78SIef{eA{Lj}X>M-@HeDchLSrj;-y&i%fqF*_^u1eC%SHa znrr{P{OGNpC%OJlx&mG2bfwF0x?<;$JUwyhRr%zJ>4__(3-L0Ruh2ZGAL_FY({De| z-)qM!8&&;=Z}m<(?iW2sm%s92_mfT{K2#TvaF&k;N4iy=&#>aTetw$k^E#)}9 z^CqVr*p>Ka*V$#!vp5Pv5$rJjB1LaOs>8-*v!h-E{aV_IUr% z^IzRs)vtP6SHi3Iw|;)(TaVX~@`<@#^~`5H%hOl!tGvEM*9Fy6$93~l-S4ZO`|@qA zRsDpgbS1o5zs1)$xl4bcTf4`bK5BOe`}v_a6Kcxs`_j{pJAn^&QJP& zp5OiZs>->oc|O9e)$c^k)AZZ&s`{h8 zypC$!_9s8-`aH?2UwlAoRliezyVcI$f7_?2@BHVlx^=l9<|X3qD$-q5XSUyU!D?Q$ zzxjC4)6Q(I{NCq}x{`J9E%noHeE!pyPfWd~F}*gnY_u;SJ_n1ai zf6YOiL6?8@^pE^j$EOEV4^JLe`K9%d4n3HB>xOuV_9s8-_!rg_F(1FZ$JXCh9rXOJgy->*`pygYQ{8&>(MiN#7t&v9@b9UH9vcZ>|3Rv+?4tM7LUx*FCd-(}TI*DxbWJtq*3Mt~yNKD5e7|zMtp$X`cJ= zxg%fKTHTM?-^c2&{5IG6%!So@*Ogy7XT*0Muv({cgP(N$K6dx_pLFN=Qu)YdSn=$` z^xMxnH=K5IYgON~zdd5-=Zkayno~VrT95NvoJeO~n0{qk&B22eZ+88a-}3i8_Xpbu z|3A-XeyC5RpMG0c<+r;2jOAUIx)@gZ**ac@OZBZAR{i!FKk+=@9rC<~wI$i#)`zEG z9*+FhUm+ccCm!J}9}kXnt2&=y#j`*8X|CT_-Tb5rTdVpzHve#s_rJ^UYW=f$>z(D} z&Gzvb>Q7YXCw+du_IF;|T6`bP?EmlT^@sauj{9T(o2M?W)|vS#rdQSF@u2)t)DNqE zuTS{tdY#{IyY%AL>VD9{6OZO3&wZein0o4TDxBr%$A|Ju(RyLk@0{kRx$eVPf3yF= zQvK@9@54)8>f673Ro6E{{B@yuOZ8^^_zbH#mEZEaKQDFT&gFgJGraMxjSc6i{Y%Hs z^N)B^>#J3PB^QF539bsj*2JGPrA;7n}4+bf4Xnl zH1PTZdI$Z=&wRe53-xC#Z(Zu*#HwE9vwsvKD_y{U%o_gl1c$SZsb2F~y$kT!Hsl!)(s(bx?yC?SZ+v~+c zAKjJc;?swo51~B1{tEFSo_Hym3-!~5@=H-a9QE@PkKgCs_SBYA-|_y=;A5^|zq!ei z_qfoHr%ne}d38un46A;5h+pZbC*so)%e;7m*^i(3jPVkaXWcOQVy{E;JdwZn-p^@N_4nPPPcqLrOb^PZ-(vc766rv7 z@d#)6cyOd!)%gr7o;my!d%oM_>-`N?>R0rSI}G2infu#(`q28s5P#jcs{W|Yb->ZO z`AK)L&4$~3zCS@aZ}R;-Q_>W}(d*NEpj_=#_SzWUnNHj4X! zk0&-yT%o=Z&gxZjjlr*`ugP7n@@Cj&V|(X_^ABWUt#L)YHZF@o!LGfmzR(pXT}glMnva z%NkYvo`2t+q>G<=`SXmr$AkMpClP<$XwFjIQJ;F{hk5G!6!YV27e1v?)jz*~XV8bK z$5(oI&X>F&>M-@_$R}3yDz9H1%Fm)c>lVXGho5-9ui%nLZ~gi51NZNa(f4{Xb>->t z*?n+7)#-^-Pd)Ram>x_%U6}RRSbsH#4j(GN&-v@>B>X=3X?|%R(*WU^F;n}k9kOI zRi{(UQ?F3p2uJI#^sBsiRlUm3_E|5i^wjxD-~M~gll#u|{#iPBVmjgq^^Gv~@GD*U zS>A}hRL6Q@rSIn&ewyR&JMVdNeUSc(=Kgjbyz-R(_Z{_ZukXF?i}T?-iv3@7bAMZp*F|&^Q*YIn z?oHkAx?nYrFZ{GVpLad`@%L}7&i4=hdRM}8{-?hCF!I~_=)u&h^2r;;sWXa4dg`tN z$|v%LpXU4f&YK_mgx2DI=*JU}=HT()eWH^%t4BAHUgl@xrTx|mtA2HU;(g!d!oGC+ z3);V&?|c4jFW~VlucLateL8qm zET5>)y2KS`|Ee+lrTw#gt_N0g_{~rH_U$va^}o+MZ|LBOM{|;ApVLW9J#{*>IO&rZyb)z{;bw_=!16KNU z_$hY2e)g69`wH9-I(XvI96WtcpBT#PTQ{0Bt2^RbFRXO=%1^rfp7yw(yR@~sAD90^ zSHiP@Q{U@MJZL@o<6Ex7 z^Fp4}`08{LQ%^nfqj=P(4o7-bo!_wH@q?e{-fpvD$1n9eqD}{ofAWx@^yT%95PvDs zUD{XiXZ83DM|%9E?>spBUtZE$oj3k}DL&%U7rPH)>(J+UT3sG5^~6x0c!a4t;;Uz0 zH5ad{ryf7?{d{@oDF-yF`{DieczmS?<@tyYbA9rV4y^L(6^{C{o^hp{dDjE8F6IkA z&FilMH#W}vt^W5*^|O!fig@-ZJ?MD>51Qw^kx!%p)x{&6<>SGTZdK>=NJpKYVm==K z=6kjg>hJsdR=@d|>yXd=E&qS3-+WI$#Qf!_m>)O)*nPJ6eeN@RCm#RkLH+!ew;p*q ziTF@mJi=K%9vtabb=L(ep2vOq?e|r`_mYEKtKY92`quuFBcF4f;`Gxx^tm7S@_5#( z4l^%?6)*G2&$<}m!^~T^JgoTqRQLMi4}Naz-y5#(_V4_>YwGIfAK&Hit*=7-b))%k zR^K`+zB;UU=H~vk|Ms}elUl3#o)`BHx_(cR`uh28j{Bf4PfwhB>X{$Kqds+W;Yc^@ zbYRxSuA86c$-n|{G_Mf^R2pk_GMl@^~6x0xWZZ9Y=5O^-LTTL zKlw?|x&Fq#+4}lBpD+2E`uh2;ZawnmCE`PM@d#)6cyOd!)m;~?c=EZwz5agUNA_zJ zb^Kgk#(0T%vzYy}G5uM7#hcaPGpzJHFY=SV^W+K7dv0r0--z?X<0JLy+Q;hFBX6F( z7$2&OE1c!&$2S+M&mtXI_3?|J=6OB%s_*Ha2lV=7|2uc5=;Eio^8^p(^QC-Z>a8ob zUO203eHCAQq>IN-e17l#dvSM|FC)MCS@E13>9_ZvJp8c!2L}5;KR<83raoQghPrh_ z>k~u#b>ph~qdwOGt9A2(pLF?k`Gs50?^EOR#ro;%ug-6E>&jR@_3$z;t}y$Orw+58 zamBN4s4w|^O}~BK^(D{R-dff7`{S-er+Pg|H}8j7ADu+&h3ev^SoO=prMmchhVm8i z(_DX_-~IWt%CVm37oWwj>Q29%2lstce}C1#NNftEo_UqNyuJ~p9zLB}9QCQgS>5DW zFH}!mk8ggev-gZ&dUhZVodr%uIl{_@j2=l2)>y#IfU-VgK-kLV2gd`tb@zjXQSyrKi~Qm@J< zZxow@m%15OJb6gB>c{i?f}i-_ulc0goZhJ3kFO7}iscja&0_X1jm=rwhdYaQ ze$w~*!JmBd5v|4kf9xS$0ng(jb)6^nGd{E~C=XMw$|r9Wr_LxI>8ZO8D4)nLewuIJ zpa1rYTZ{Va_iI*uTaUTsCFVL;jp^Rh{jLjE^PIE%v_9wZyZ?0S_x1k&<@2t5sC>&j zpJCPS@t%I$-@CsLFP**dn9uOWSsNRCO}}rw+3lZ`(b>po5EYJE} z2dw)2+`(@=eqHpk?!3DFtuOs_|LotaTaUVVVs+P{PDhL<#)sA+t`INvE55l9PYmU& z_3=}kpZi?czkkr*@3)=Tm8$jWx9{<+1G-N9@=?uYf^i}7JKXK9^|Ibui;%3HU5B0Z?CU+kRbr#e5L z^^yx3RsE&!>YaG@DLv>u;>lYNf7KK5p}Ke}R{ipDsV=_j8tFJU_=#_Szw6#xU!UCf z>79Y+zL*R7ZC~Jv&6Q821J%W|IO@xK#&8{}M}4jXj@JGEv3Kubx1Qs<@Zb8fNZZ7&S`nMh>?E;LLJ|^^iXfSG5t>L^ zyGeo|G__3?oe_zNAa)U@DXpcb+O$~~Et)Et>IPk^p-Q?zTC8QMRfMp<lB%EnqUOZcd6fS2_Om*j zb4WjVF+NlmS2)U}$IreQ<5$QB;-!zed8&K7-}CP6??b5feAFxCuP>B`@|Dl3dH!?t zJ)QN!p1*nWHSaGzcmGMMzRTjE!Lv`(zuMn)JxsqrPv(wUce>hC=Hn{U2ilKOcYA3Sld z4;~%9I8i-y;vVuF<&E+yKkJ1R&%Di3U+2lGPdjFkI#2dEt~ue+r@z;k_BTGXE^%V| zts3*)(R$YdtA6HUp4R90Np5sv`$@h1f9t*51kD*g{qCDS{q%XwJ=SzO{{ODlT@ha@``9eM! z%U6E#bdU~K``f(t-f#K!;C0^Hl>E$zAGCkXSKhkx<&%gH)x~qM(#ylSzWA;S$|u@~ z_OHI?ef8a5FiF&pzFA}NHMhz)?}z#4{^qBD`l)BWibwf)u=4BW=^=hzk9}yK>i*vP zgSH+rNu4)mEe6aNroa1WzFF4?@_`j!9@2{|tazEvc+}6jVWqSG%u_%6`>J#9Hc7pH zdH2KG&g6@q{(RN(<=yA3VDxLQw=813KAAZ+8I*zvad1^v)$be(H4M9xwTHe4%yp`s4Ez^R*r*4^wBJ>eg}9rR|e&^_$$O zb;7e>^@qLlq*@nWKCsHG_ps8-!=6u7cRjG;xv%D_uh-vaf4u#l^X80A%?Z!rBmL#` zzS2SK6T|ddcRaUGkMBBQwO;$vJo%dUr9bh@lT`f;f7P7$!u0p}$a#z15BkLPQ_p-a zc0G6*^OcAAqsRwVx@z9e@3(*N;7Mx#^RX^^dFc7jeC6qSh`+t4&#FG7dUJ+VKl3tA z{^tG7&z&<#)z7?9WAMdKf4=JY=5POJ9WVW=eDbQe(#yk2-^LKbUnm}_~IUp^6_BLx2l^rtax5Wn5Vw-Z~ee8Oj74JA3Sld4;~%9 zI8i-y;!)&V@zr6)>*cK%;#a=rsqXs_@4fCbC#m}}`tR>l``dcFE)wettru2#^-)Y8 zF&y*=f)R{rWaZ?C@({P*R5?`U&;9_xOk|GLkU^xxjN zH^21n@y!vYUhH`>&u^~>e|FEuPE_~fb?3Ft`g?uD59Yj+uP&d+2dayEILgO^J>RNs z&amRSZu8XF-+O%J$1a?t>YEO0PI%S4-ABAc>&m>iTJLE7hwYQs(_5!`TBq+reD=SAAH*Mf9}VLUv34wYToW+&Rgt$@=473 zs%O3z_jKy8=U3H7=WSl*sju_*_-8zKYTcwF+!*FbPba3wQ@1X8e6c#ddZK#bD37kkS5Lke zS~rwWbWWQmf8QVck;B>-V%0CYb93NpPU-7$lXd>|8Ox_1Ugk4SUdE&Ro-Tc?8>U_i z%~S02!N0sko0$3=u4tVqU+bv$xA{AV_$2ay)+L^c^pIW*?-kf3jh}F%Lzwut@u-i;j``^zSyza(NKljo3QPt`BSr5Lt7|KKW#PrL0 zFRt{N7sJZeb(*L3`+V@BXSP>{>i%8iWrx(@#C~y||}ShdsZlP7f=d z>oZTW{e8$Ey>ODM^T8AM`ry&^F#YiPjABoh^^8aRSTC$})$`@<3&F#0d;97C%=@fQ zH%IHDPyg!miN}*VpTzW2&wMZL>C|D*ud16fta#>Sp89%!dH;7U|9pP)FEuA~!{@Ib zU*|WTdgl45^P>|(c_^Qlep&Cul|J)gSoyk6?=P+2=O;V=z0qoN%XxE#%hhWm^R4#x zk6m%}M0P*UT6{m2Fa5QS?f*O<+Q;hn^cCW-8};k?S|?t{@{ljA_|8%D#Pj;(Tlh!@-~vLZJzwhW9R$%YI4hYbB4>+ zYa?^bdE4JRzpvS5WglKUx#j;eXSiIwHd4=d`+3@1j(FfiRzLLIrlqUa;dQ3-1CP$@ z8gb&aD{TKqpZU?49@5Q?=@NVEPriBTZ-1YB??+8kbvit;e&P!0dN}G=^~t<`neWA; zI_rg%Z*`tH*I)UX_R7%jAM?p|^!m`@<-X?!@#y5mquA4_!%^SlSuf0b#^$N7^W<4C zICi4i-=A50FoNe?NI&<>T-E7%h`%n>Z?4~{&YWSdk9qR9@4xoGZIe{}-7VZON64SP z`n<||+$VfK>gk6k4}1Blp8AS6nzwG4{$kf@p8S3O{k%J#JyCt0wWa;JwhcV@g&*ua zU+T*zkq=ZC&&5hF59j*gn=_P8blv8uukXh`{i^*ZiTXqStvTS?r|IkS0RGk?c0c6# z!St*0$?L`GBZfVFRd*e*;^||aV*7jZYaTX9)gQ9>#Rt66{jEP;5Ah+scociOtYyKKXd%wr#^a;I&Ut!PjgCM`se;ur?W075AoNHtNQnJt^-!-%iG z?ee%us(#5^niC#<`seEteCzRiDprT-SLKt}i_=F8d-|&GI$)1yp8DFKJO6&w_n(|O zshTsCPqaVtJh{Pwc*a8>IsKpe@y)Nb0$=+yeRF@S=kuj?^AqDkb@3?jgQ?>SvpyQr z!%F9Mf_Yk(-*w;MqI}glL zU+2Lu-fQbbRsZI1Hzl6A@q_MTHE(_SB=T7m`K_v_8}%_~NME6O^7nn@o&UaTHL>Nq zIm6}Z|2N8i-_`reTVL_CiR`@j%@4IZ!q+}cM|t-T-{T?AZ*~0iQ_p-a?&;L^g+1S_ zr%%>1Hc$PW-&a59sEKO-)8UEr6IV#r!}Pc!Rz<3Z^uv>fRbCxdJb8%U!{m!$rFY%tiRbnA z&VMi3M1C;ZC$M7Yyxkw)M?UvuPoAXi$9vz|obc$=U*3LJw;rz-#rz=tj>5TpdVJRb zt95%lY@U2QU%qgcHZk>o-qf7%%r*U;M>%ggXnkUse(R3s_UZ9m2dvhsZk~L7{(a{A z+v{&Xzu)zv%?Z!_N`L2(dE<-SC;io7`c?Vl_2Tpq!=ApXyAD|K>_hX^*Xxsw-Ciw#4t`_Mf3+kZDaq`f1u|G%)<_=Bcme%j^7I`$@h1@B80)9tV7&d77`hn9ln6L3~){)kiUXdhw{Q>wuNc zb(^RDz7O%myR`$v=UG?YqB-%!Pk+w~=Bthm`6S}68})^w{?=FV)nUc656#m$%xmi> zmVbYK+Xc-D&$*HQ^f_xto(X;*99xS_Z@kjxDSUMd)OrX-Xig) zw>Bqpgz2Bpm#)Wp^cC|Fr{AivzK~DG@-Wvi%BO?;p!38$`Fg(m$fKS*QQeO%i;c?< zpC7FDH$LQ($YbE{YU4`Co%n2jrs0q zy*b0GpY@oh*m-i^hmN15>Q~&hIpH~n(?92p53Ng_n0|9(esgvBbM>wVR(f^w#53>X zpVe!>@cZKgv&D#_3zd)(b11eQ2Kgc|ZTcHyqlehzlb;z8T9W z;*DbJSB>?Xs~^>wGpzbtGseaL@b`Juv!2!7`Pl!fo+tYAw=O6T@nMx$AI0<$!%?5) zxel21jLlPD?|1I<;Pwj*_W!XbHKlVKpPzo7hrQ0rx;#BUdVF;#pFH`*9*-|feU(pN zF-#w^>o!mRUZ1@5PDe~s_v7$CZVWtg;s>4o{N?dIuJ|P4Lv?WvNBMZL=Udgy8CE>k zZ=U*^$20c%#Yw7u#Q}|Bj`;k=^myuG_XA%(*Q;LT`Bq4;o-to}h+kpy_`phMF6PO{ z>*_l{`Q?*TecyXFC%!QK`I;kNNSCpE`mGvouivO14_50n5A$^W&hMkXbp1){yzzWV zhtFSZ|Kercy72MjlaB}0E5w7V@=~8MU+aeQ$#6{z6zwOUmj=S$9_4iRevG^h6bBo>xS})=3$=v?7v&?d*CE-Kj`V8ynfZ|LH>z+==tG^6O*6yo<8{* zSNiNLhUp`=kIj?6dHu|fo;*?2FMD%i;MvFgpmPHc%DbQPiF}~Ccocg&bvWvqJag{( z_#E0i@!f|X|A)s<)_(q@51v>*afNg}Oh5d}SALY&yULt>dC=caRzr8X4xjsFe^}@>Eyv>tu`_yD(<5|0(I7!`)_qLZS-M?4hal;Q< zkG^7cnCq?b$?L`GBZfVFRnP0Fc;;fB`Z|}7z17nvsr%vQm;C9|-+APDkT0EiXI`9s zqcQ(cevdcmhX;GU=IMIP>*g;zZIY_*_KN0&=f0%B^T@o_ttaQr58|&2=lag&yDnJi z)y)&nyzX-o@vrqX!zV@@cm@e1J7avx6^-)Y8F&yn``6=xzkcpxRkzRe!>8w` z4zrF2)ghgFVpW&7PCC9~C=ccN$eWvas(Zftl_NG!QuWuJ-<RR@w-kjmJ4&VR( z%#**@--mwt4wFEg8n24w6X`}V^>br==IZcA_2vxe6P>^2$)D~gFK7()8@3mEc(^uYH)#m%|AY7lG^{LT-_9S^yy#iZ+vw=iRq`F`Ci=9sl%RM zRX1l?@$5hI)Ym*d{IKQkYaa9U=Cs?w5TCz(?iYV~u{wS4JfXwqr>{I7l!y4xI(q%_ z`HJ~k50rSKSO z^qY5`q`seW+Tvj6oA-_0?WyW`kRPnp+sms%{^C*W>9TI!FzfoNnF^#g9&9PvE=rN4jwIQKWc zzI+nXZ`GLZj@G-*-n?DEd0L;>b4Q*0%hOWTts9SC9?m^a_(4AF!c~1ob><9v>o!mR z_U9Wvv;6(41NLc7csu9a=3&mTnzzS6&U^8(>&7lGZ-1ab{q&RCr1?7M(_fxHzS#bE z-_*tFSLG|-_VP#d`ddG&^v+T9#51oy`OZm`rTPu`ZyoTQJNiQNG+%i->w@wSe@Efm zK0UtcfYrL42jvg^T zjW2dTDU_0)->{`l)c zzHrnRA67c+hZWB}%#*Kqzv+F?oTTdhy-~jS>ED~T#|1qFh)E z)ZgoiyFTG@lT>|=#S0R1#OE*Oi+TUV^lT)&=99ajFH@66Nu{Ji*u|8!iF8sNeR zk8j5EiFl)!`i%Kxen-dI*PLP1|C%u_{)dnA*qe&$Ljcr z{OFS}PE4Pyi_=%Whm|h-;z9NFHxKi~b06OGr|tXp{+_|<$214N^n6pVE)VH5mQO#t z%!^_2VEu3|30$oGmla1>C72czRnHvbA zlXiXT(!9TVWOKrE4y3=whyATiXISGgZ&i0)u;Q7wdFt!^{PmA`)g)ElIITJ1 zxi9IjFT@u+XT<6-{nj1N?bG9{!)m?i*UWQ4bbi0_Q|C?@^|$>_BhXdzwtvkP-+bL? z`NUkWdgiNml#d51zh2&T!HREQ=Bcmq`yF4|I!V=!Y#+~df1kwtGH?C)L+cYm{B`51 z{ym-RfYrLq#XR}ie^>0WX_BgM-n}{D*~jV6w>NJ-iRrg$%y&oY%^6nx%+)-t&;Gvm zuFc_UiG>j!-(DX)I(%`Wdg{cZ$hYEWJ>yE3dFzE)7pt46em<|d>ycE;F&95XrBD##U3tv67iwB zxQChfas3h}GDJYM={j9=yXz>04l=e+IDJD#=t z``phwt~v3o`qP_Z_D{VS;=`(sdWDrAe){%exq33z`i<($8CHGF%RKpeyubeP^CqeK(T6uDzWC|in>T%h>9?-fdf}+A zbyj?JSn)jG&C@!({{HVjwfys)J)Ybcc-?bB0+L zyY8H~b7trHZBjoN%^6l~bIp0%pSQWo4JWC(-*2$5?N|O{_fc&BRLC#an|yV7eG_|i z{N!bf2dn<_5I@oTNb}Usyxw-)*^|WYuhHX)dwuZC-+ktjn11Sf=3=FnhjV@LtryBC z>SLZ_^WOcmqbG^`L60Zy^+}$&^GQrUbv~om)2YKz-{e^@RIhx^Q{B(gzV`9Xsu+rsqRJ`guG4BuD>i9`&|8IR&Qn?*Yj1U&sbi+s*Y!WnkT;3gAcrJd*@?+AN;kZ#4|sBu-f0| zs&B57FQ0Yes{TEl>wwj|y{|D(zUHy>_et91%%z&6Im3!)zvaB0C%68_{*zSw4e|cc zeMx`k1%Gwxwys2V{B@&#bA5X{by)eUo2Tpb`N?ySdeJ0R|Kzsjgy;NE|C~3ze$FBJ z#Pn0od@t_l)M3xBs=F>&@#M`@Uq8P*`Vq_jo)i+F=9&KVSx=pQ@-Y4IGGE2X&zLUr zVu;s6>xPxyJ~U4}?{_}=2kqZ4RzKxsje+NW<$7}7$yb+8Oh5I^_u`&T9rpaHx;ev& z=eo^PU-SO)yBsh{)lWI6G4R~4^!NS}58Cfl9WVW=eDcJjd^|`$7tMLp$2{>DUxVFv z%$du7uOR<@l4^f@-)g?rBX*zkOU&z4&wR$CJUVENwP^FHyhjiHWj4*J@^_A{P1&l8Ua zdVZ-BXI)H>r=J)PTBm)0mxy2G^@aH2#GLoyYik=XJpIr~>h;T;Zr7aX_2&=W57#S? zPhTPajzWD${d;_MSoxZZd76j6fAWlnK46l%A6q}sobc?6^q0^6=5JrA^MmQPu6R}d zo-VJWTCdOd%#*MC@U^$MPYTtywy%S4_wSSB`t*g>dgtc1*KbtsdSJCq`_Mf3+uw(M z;nYd0&IeE2>w`zv!?}L^e^_Y!`dKfOPoy(XvCqGczg2q!ufF-p#^8&ezMlWoq5JFc zBhL?}A7181<5A!2lX2CLe~<4xHBWr6AI|xidrvL=yzQcUwT?Z1^W<-x{OK#iUpMLx zNBym{;;X}oCvTqCLH84XcK?aue&FMY@x>L=^|0qhU-jA1`Rs4Ku$yv>bFHC=W{_6P775(HPKExCEu;OQ43@hJW-MV4rV;<(o*Lm{R5B$O;^?H#H zp19Wsj}Bj)sGd6UDDtiN>agPV^41ISD_`?e_xkwWKec6&x*wHK_Q6Z!-^297ProW2 zcxqbFJ4vet)H$sPrSeU+AlYTd4KZ3 zP07DnM{nNl1D{0egH<2(9@0bnUR?3yVQ>9-uG>8Ix4*CV!lzDD_3vEVlz8?jKiHdh z_5PDDpH*>tedg+O-j&{5%oESN-~6HW1vd3fA8HK#KJUu8czuJ%*Zt8~K6&Yk~u#b>pi3J)P@-)w=nZr`Y~}%bz!;`U%%>48Hj3?|$XH#r)~{!St*0$rF$A z@vH;#8AU#@r!!Ce%;Wh_JZX}u^TGR%w!d@z;vTx*if7KS;+dCu^0hzz@V~TwKv(?} zA8t;3z0OE~dHWe(tgn9R;`Eyv^P8)~w+<*@p?xQWltD7euzu)lYd$p56{gjKE63<-uL5~kSs2`ntA|I$O9>tz6 z>lu&saa}#VeQcii=Jm`kF2CQo><(=Oc;=StqPK6=>2iJY5Fb`~^$L5ss$S)*KCTDi zrH{Gfy!}4O&;01}?~`2i=H|f1{o)VZ2RtaRuY4jOs4gDGo=zQ(`XyS+>X&?`b>d&`U-v=XI>hSo{GjVmhnW{ACO_+9JSbmb#j|c$>Aj9HPk#2_uic^j z`^D;K-l#G7;-^1f^HmpneNWF1;=?Mho;b?KvksVky_gTI_^#VL^^-sG^~-;cgEh9o_U$4zUJ|~C+;^%)j#l$&518efBKxacyxdBt@@}#>lec+ z&llp)MS7Te{j2lD&l~>fvPVu*^WNoC?GBk6pY-RO{oP;pgPvdV`Br&#$WII_y*wSH z%UC`!eX`C6&phSL3j3GK7KI$)*a97)8o4i*jul8@^zj(cys$c zoBD2hH77jhL;8DsRP&~xJnf_INi>{^tFt&mKEb)#>oW zy*_w!JskDp+e7_&ysVGvtQV$_*j&w%zt0DM?yh&4sOsA;ZVbM2U!OSd_0^A`*!rNl zxQC;BJlONC>h!SU$(yI^_49_ezG(UV-tfQK@*NHiA>!IV5nCn%~d@t_l)M3xBs=F@OdY|7w3*4;`Px^i$7#6_4`qVCC1#yDnJqtNrc$%9gJ`ZIb$Z zs?9HGPJDC!>ccndbmlEidYfBN3M z`6Q;_sxjXktv6>_^|KG17uM%IdBExIosas7uWwF#@zcLIZ++?c!StIO^M!Nu_@nE= zgY=2cS@X2M#mkV5KfCe6CW+sty5Wgv@9#KoF1xBZ;Z^hIiwDiw{g6+r);l-Pcdib9RBz5bzWr^U`2HT=!+(E| zskQrY`gfBETKwqKA^ z*9Xs>`NBMZL=Udgy8&*8$fqCldJUIE}+e}h*K6v6@A3XEt&nGec z)cI66%A>FNRh}-*B>Q{~V&()9WTo0`Jn74WI zH{PfH#nwscejK^jxO|;Q>CZR!E&oJ*^657><~LV|KUa^R$Pb!}dAeTtpWl7??h`KqrhzrTEL`{%t@{`;EN;~W>e9%y~*##Q}$ zI@bZKb(?F>+vg|0{)G0EO!a?0vpMmlPk;B(T-E91U5C6FA69ww3h7|-hFKT$$$8tKC;evo{T-jzp0@Zy)qL&K^rNrp`a*s% z{lt(?T;V7$^*vqIts7>Yzq)zi$zOQi`%P5$$lTTv$sb{{5NBMZL^6TYYU(d(&nqa=J}}ev!0AIuMT^4ydI{H7}8reluuMQPdDc-gn{fP-%}N!9&)2d_H6<;`1v>ya1pgZS&l zRsDN9*8zL$Hc!6oTM!!?k2~Y#lT>}{e$9z5e)>DV%~c&A@=3&B7wQK`eeq$Xo4bDV z`lT;&K%8$<5d6KVBydJ#r1}9Fw>W8+k%XUAn@_gz3S)6t0tDcAt)x~qM z(#ylSzWA;yeX7_z)y?ZIuXxF1sm>oyte?0-Jm(CbL_9v~;tEH3^!QMIE?O_F^xp57 zr@lTPyxqy|Bv#+@;5I3~^yzQ^nyb2ezCPinKR#3!k77@!4o7{HM-Q_ucKzn*dhPEg z|K7P1^x9*MWxg>tyrfwe|LI@tZ}-u8VtuX$rr)~aRsDN9*8!{bR`YgFob#N0CaIrC@X7wYKB=RxF#YiO z^l+44@v3~)*Lopd`k1SEs@sQG-Lw6l{hj|_G(CUVd%mQzK7An{Smo76F@3~v)F*ka z17=-puI8z)dHm|GH=d~Km;Xy+@Wtl`ofrJ^oImb|JU_?>R(bVAzA)=zzEFNH(!)xx zZl2cHro6Fn@nP-rLG?HMbaTRUU((+^&0C!g>Mw@)u*$2CV)}^Ts88}-2h6(IJ~U5# z&G7+$wfy~)?>wd{`8xmk!Rq;v&VA5NUd#tpd3A^f+h>B zY?JqTan~c76J7f0C=lxeO(8vbmnEA`uq9iE|;A>N$vkD{=PZkxnJoopZ(2WUv+*k z{nj1N?bGAubyVx+W1eFBb7PlRPEz;d(7l=yp1G#K$A|f<)4^P)JpQ_IRsWvOb--%f z=4zgNJzw770gsxb?#JQxZccph)8Cw-IzD}c_z+J#7xjhoe4+eYq=!AddE)u`<;R|K z-js4b4n3nW_dPNmpBUorD4g4;$9EmDT6Z;X^VU7z^*C%!K6^{Dxt&o3J@8w7J)(b1&>OArO@{+edVUn8n z0XJ=q)={0`?iZeQ;Ab5VR_mRc-(J5_z3c1o)y-4v-*33${PqV2)m<;1SU+)vbUjQz z{K{8;l$ZRuI(lfmP(IOlYM%P~-0(Y_&zU6Z^myW4A3QopCx-HL>q7o>{dzjZ=atW z@{$)$*4_^t`G__d{`A@3M;GPeDZp6`iNmqU)A$EDxSHRr@p=)d*IF64t1XF z(*F63?)PKu-`wBUW#0NF=6cmLKN?qhc{u8qJl6%YE;bMI)Ytz0u|Iy)L{;C?{``9P z--F3{>&M@^64mk7jr`%Lzjao8by)H2Z}YSc^ZNLe?fY!%SF}GrbIrWnFaGp-erMgf z@Wn9s;tKh!T3=PCuXRKD^sVMC|M5>fX_A=tE!wZYmh&1Lv#kY^mQ@kIz^5 z;E8*El4req64Ote&nWhE>TuLIdDaWFE;d*5)Yo}%;J+@PC(rqlrsRvy57Osc#ro3o zgM1*KxQF<#;;TcvjO8n|ZdmE%bAS7J+6ON_cWR;jy7QWX&N-0l$k&6(cR%D4(@#C~ zRXobagOy(|Z_cpd+h^vfZ~K=0#>Nvqz566pzp@?Y*L;3ruIX={_OrV6$cv%=`0GY} z=KA(@>ag;!&ToI;{)De>o}}jel)c(S%w_KWHfPVLd=jhmuA1jRS3jyVXIS-d-R8;P zdGhDi+jo+x^T8AM`ry&wLwP7qw=U#A*RQ9uURe28``dZ2^XEI&kkFm=gOhgq*;>r$tO@`k{M1r=L8&dLmxts~8_vJo&0WJ;YBm7xUz|+k&|N zvyYqpPkqye8iTKMA${FPe)wL`c>H8toPP9~@5McxI_&vXb^2=kbk)4g5 zEf!w{;%k2VAiaGnPX}|I^7!kU{Xp%fr!m>&H*b zXI13)!_w=QnEmZf^K`xD_3m%{!X#0DNBi}X+28lkm%nw16YRb=3`q-c5 z$=~zk(+_J0h`QH&kkM=iD$){rr2;|Mi6S``mth zf8{M&2Rw7r7wT({@^n4KUl-~(*KbsZ=enytc;<=cKJ59hCr&B%;}gHz7<69W&_U;g zdE;A;b4Wgs4^$WTaFmY+d%jiOb-{|Kk9mrH-{&qr@`_37e(b&Y{-8NR{`ytt34c5> zA9;T2u$NbdsgsB5P`-z`PBEgK7h=gYr8;rf%*{dm(?nv?n1r|IvU!Q(5o|1&R6 zzbcY2Q`H`$H$Km&A40FWiFXoG99b)G@zC2#~$>Xc{kgnpX!-^+g z^{0pUiOyN`9EYrk2de#P%JC+o!LFLocr`oL=5>ggxXPrX7sSn>G6iYH(B(N}eP z=Z1OWdwueSyFY%Ss-Ll`DeQyzcaxT=3o=Q?1uZu2ltzV`P;pZldr zs($#X&4~`5zu5jxp7ms2te$@I{M9SOgB34*d;F@-2Uh)Dr+M=8dhnxPdF&)r-?(dY z#H;qVdE-HO`(HjW*Q=iS(YVsf!%@HFnRCxa-8{v<@3ZX|TPADuO~&m=3Ouy}oSM?p$xh`0(SKU1M+keO1aoZ$S-@5qyG(2-nf9H|8s?+H!PQ-`m;!*7B zvYzp1AJ+pb9e?u_d%isWo%frh>U{93dAncLyy+{{XI138s-AAt$DAR3qSwjh$zT5Z zclgyw;`_7oc;a3kJh~pHA3mQ7M|t!Wzsl1kS}#;jA9FQNb?3nYe|pO#bw4Vf?1Puc zAJU1TeCoG1=0De`r?XyI`SUSPvHNh)r_P(C{(Ve7c;a54fB&MG_pB|3#lb3O? zkGl0j`9$ZedFto&$?sg=o*?c2kA1r_a$bDty??~B4(GkT_{mE@`pk=an0kD9DBnZt z#-oSwiRNOSeEfZgo&SDAPVD=~<_y0-YvY=KhqyXVZt!>Q_qo+C-LKsd`_%nOfA#Ed zA3LwqD}MTBT=mDxx_;_Cq)(kZy?W-=q3bqJe2@3{J>&F=s=n{jo07lhReq4(eC6rP zS3h|%K2#T1ILf2Phw^ig9@1CWW1i~n!_I%7+vFOW4^%a0D4(dF&zJ3|mjCa+PjcS! z=dl<3<8oj7G=1f*3)=5Kcacv_KlRLy#+6lJNLKWC)xi=&zP*$57?)5 z;%lzizdBFoV6Ia>{niz)>fh734p^<%>l5?j>pVGn_k$;?`nwlD_`!3((tmW`d=k@d zdt?4{eMWWW468os=E>jZC(rwf#DVdGr;(%F`uUFH}z-=eK#Pdw;q66PDl4AM}Ri$QPeK^gOB# zpdTSF!|OE@e)10&6AILoN%)PCyDx=XEz5t`t+qYUv=xDl3{WAo(i`^fLP_2xBZ{!>hg(riTcuKUEK3i@A2^(=AG;Nz*MUzNv8On%nIcu>BF){RFG<&!6Gp6bq%)8DiF{i>%tusP!Oo-gsBIp=wz z4y_ATdG*9mJ|0Y;(b$}OKI%Dd`}19&Z{LhiKlXWT1$5?@>!P=B)vb$;Pa^)hkpEo2 zQJw38RUiA)Jo$V5{hrfaK1sb^eA6SE6CQo~o3sA-Vs-h%^xNK;|6HF@o$KoHJx`gZ z_4vNei=MRn_cf2+y-lj;@4PTqeEVA6`qZIyz$&kv$QNc^%oobfMS582U3czpufOkf z(`Pj);=(AeBjc(c-Y8Z+qj`Ns`NiCdPBL2EiKYkg@ zC*r}Zi|4N2Jo#QT?_C}@S*l<1mev8U+TZrC^8??yoI_%D=sIAPS0Ban5yMfRT6!-|N8zD^^5iF;W2q>e9C&se@f{a~ec9+)TI;%ihJUp#90 z{rs*+w2nRBYTo?mE3B?-d-M9k$}j5~r!KFf;#c!_eEi(Qm*4Mf*}HY(=XG87@6DUu zI>j*k))lYn-_yAcSgqH*a)0}L@XGt|GfCCAJfS)9#Y=zZMRlIYyAFQh^i$7##-lvC zYe|RrCJbtJ`0uR=582(?`8Rx*m@DRej|35zoa@z4gMX4?pu1JJ+9f^=&4p zIv>31{QkeSzpcwUSH-zL<_s&n=QH!fYk#G8W8>A&KW$pF&&RL$<#q*l9v^vK?iYV( zJ#>5$(@#C~qj9B|hogSUb6qg&Vs-P>*ZZBv{PnpL)%|$GVngFq`#a~YA05@!)m?Tn_tzxr_1Z8*6Ca|PrmNM7hc|W zy84&fk9)3}w{tlC&6_`8>&m=3Outp*?e!bgyDnI**Idn$zy19WH(UPw%QNrQobams z?f5ZQ{pqYroQS_J-aaP`@f)br^nKiO8mw^~oNzx8uJ`6T9g zt3JuAVtR--H>OMMtsmccV4nJWzI;}EKlYioZA$*;$B*7T)jbZ>=`)s3KfKI~Ve;fF ztoT))Pu8s)R=nywvA^GS#fvAY`*HM-w4JM;`^6vfHSgp*Z{!pCKy~pb_H^oS)HiwN z4As-uT+CBl{-{g0Ow#X4ER68@W*_-PI(%`WI=#Ah6ni>#IO>}`>xEettDC33z7MhG z?zf+)&i6BK)ELeSe17)(==n{LZ$13f6Y=Enp*)^^A|A|o>hU~o@$sO1BE5Nv&3o@J z95PAN4}E82=!c)a=8k6_@_C*lFa6XrpRx7mm-USK!mN+R`awGD5xai#RQL0S+nwBY zp8A#tx0Ug=&-p>Vc^&dOZ+`mof$HK>?CI3usBiMCac)I_Nz8 zs>|c0UzJZDKlz?c9nz(be1-IM5MS)N%`@|l`@@G!60cugw>W6{n^Wam)%Ah=VETy@ z=`%0x)$uB<^qIGASot|m%#)AvnXl{j!&~lY*~vBYoXRe)#gv_pFQ4Z`GI|oaP?Ivn*)p6h~H7u%oa zsjtsZuDZvTiR$m=(cx9Gd?KB7i7QO~sxkk$`ca+hfmI)mYxCqUf8eukHA&qMK6v6@ zA3VAqR(^E+e%M&`w_aGS%j4QS`R%?Coc!qfPyeUBaaAigC-C`;J#O%U%MLk2hK$^)p9U z`MYlOdYBdeY}n^PyW|m2(J9~ zr%eB+{y!Gq*EC0SO@Ht6@%UPg^F}^->9@VHKKhUPjq3Qq%HMUGCx72R+2^lsJxSG{ zvVU{JGuQMtPyXt3)+J8Fhw9={?CG+e@n|2{11p_*n5TaB{a)Ytxk;)%`uBajzTvOG z^*D$0OT=Fl`Tel;t_xQEod@QL=ilf4pJyIFE!q412i?0}0pFab>$C6q$&2YL#9ue+ z4@dp2v*N4we9hZD@#Qc5xm0sBXISz0 zRP+Ag@3s?1{pg=w?r)!_zsHSztF9lkJ~6~!H?Hd6)42{G z=Edn(<&&3jPp1x3KN{!tR627pPksHp$Cq5Vb&|RtyS9(7y5Ik{e{+9Zm%LbCSgm(^ z^Q-#zbglzd>-0L=Jo)-O_Vj-~ZfV}5zrRz>n?JNZF|5|RuKe6SJ-+LJ)jFN0IdA98 zb^c-7(!4)-Vsp};KK<$KV|DA17xRPo>&8|6dpg$vt97gAyzTqV4_f{{#08IPPB}0B z{OIwlS3UD$b$)c>tji~+F6*Ol`c(bpVdd{}U(NgduYctv{f)%{AGz4Ld?A19tLCkq z$OopMIFZk2K6$-()Ze;c|qy)mbl0 zpNw;Vd%yE5FM9bzRX^)aO=%ro-=v>&-8q2|txKGke(IU8;!!>xto(X;*8?lQKISR* z_p5&6Z#Pd;|31&A-)l~MeZH9f?t^)&r%nv@$A?v3y~5O0apu#nSGRtcdNCjK6#v+Q z@T89}|2?Ji_h|(@`mi@*MSWKF8P%IJtonJ~miybh{`Cg!11j}Ri(l`y z4*NL$>Hlxd+xl`{qw!q5Im1eCF6N0Rf4j@~nwDt)`}q!E=sM`#NAs4)_qfQsdivqX z!zw?w-t*xHlW*M+FVS4hlaJpA-0hI|2g1DHe|P&hWrHt%`sTdV@#!nXhjik(NDt}7 zP(HC&&+ACN7|%TU`ut?`;g6iC_W$KS*BE&ADL?3(vA^ZTUWf8Y#E0tQ9**+yV9&Rz zn498rGM^keCzSLMm{n9)H7ejqkKGA`StRy3s!u6a^Cje z-~7>*NveMO;*0on)&6!)+0XjZLF*Gk{B`51{ym-RfYrLq#XR|X{eAA!?>R};`QTNt zd?KB7LHWegZ*Rg)VHNgA6#r)zBx~N_p3Koe16dS)T>xNF?CrVjngM%KGqH8 z6Y0%UY~SvF%<}J(Ty{`n;F)Lo=6Qne{`l9``Hj_Pz5)ogd6~ zR{7**+|#MU)Q`sWuBXzeo2S_G-95i??j%*`gD2KcTp?W#NBycknb$A#y?9h-y|D7N z&&-p*&riPfYwZ!Be(0;4lXD2BKi{ldk9{u26Q^I5PoDM5_hP<~E-`(wP7l@TpzAbG zd9NQ%d*t%@ef)mS3D4a4Lwf%5bUnmh7wR|HZ&YW_uiA@;k@1FR-bfc3N}di=Y0UFZrv}$%_;5 zVbw>y!b&HfIM)Ziw|+cmp8DJ0-@DBnCyKi7YgYdBd49`7`i$kROC4rj+{4u4XFg-R z#N=5wOupD$&6AJs58nF3`%P4JI=m{DPox{g)X$CenXAJe)tfVlyARJk_smIR z-iNlAhu!x(&J*i})-(6~rqA{F;+`(+8Tb6vT~|+UuI7pF@8O+rvjZnf^*zsS9q`Oe zU-H*Z+=u@B9YzrUw+(ouU)QuQ~)@9%hgq`$o9MRoqr`os`_-MFfM zPv<&dweD(v`}-%i`p(@Zsd=BVsX6IyuIZo8m+ExT`os`_-MFfMPv<&dwQl>*Jo(!9 zZ#m+DlT_XL%@;rY-ADd-bj~4hV*1f%el(_sbaP|6#NPUoZ=U))f4}>YHZk8%c|-d; zc=!7!_I=Kqf37d%T;IB4edhZ0bn3A3w-3$JJe&u&cys%tQ2pSiHYdL3$)Dci!#-B0 zlgF2b`09!3iFlc>VtiQf9U^j zXdml^m5#r8ihcfl?56ezaNg|u_2z_UpQgY2mHS`pam6Pw{nRr*8drLGIO>-?bB0+L zJ7>*PU*8}6w|li8==i?dEneFgcys5C@4T`;e$e`0l~*6d^bx~RpX9j?n02x1H&1=t zhsPfFf{E&WUAqV8!$JHcx%&Uh<6g57epq zeL!`3{`BUlE}y#0tEV4c=EaH0&$>8y8RNmKZru<+QQbWGIbXMZ>OPZ1ef0e$y?N`) zAL=iL`0K`1{d+ps0jqVJi+S>uf5x`L>ru3%5;D z`~TgGUxdZ;Iwt+y$KF2XlbC*UV}5gW$shGGXP7=>^Ds~TKL7s9U%%HxRX=j?#-MYK z^Mg5W`PAtrFUE)J;vSCj@p4YpI;y_(5HEeq%RJTX?{99}cao}~a#3@_%jXS!wwj|y)H0MzTVG2@V9O~N!8!|t>%blZs{+d^Om>&`6Q;FdgiNm zl#d51zh2&)Va34%qjafPW%o;u8W z6(?T|E1l~zPdvZRee89Xf4}%U=Qai%q=WXa{Vk7gpNkXmVedNd5>q!f=9fBo*jqoI zx#a$Ke&6*zx1Ff&$CX=~l7G&VuX$Q0q|3TIk~FP!T)x*oh zHT|Fav2j`};8m{&^@sRk_erb{=gws=e{P>1UmaHKQNL!M3!=X_bLufIp?=V}TWn6| z#-E?pJy{p4DU zb?ecWPa^)R$nS@xcU`dRZ!YGEXaD`hecK=4P(O69b_IBI=S>H5ee$c$si#}jzo&B@ zuv&+@dGhu1o#VE&e}CT3Bewoln=oEAZ|6FHi0?kj^MkqGDxbVwoIbs{=ck_6A)lz9 zd5WDUpTGBUlf?aa-Iv-)fMvagcm)ko(|Z=U*k{e97a z%Rg`UtB*H@a~Yoxw7>DJN32dSkH-h%i7TvlJ>S%cVbxEbKTMt2Ii2(N`sB?2CteRO z8qk}YzR>`yoDf;?X{I_)s3o^ItdWGwR#pTQ979 z?N9UM>-EWRzxZ~O)br_0j%-eR@zZ~FfAdL9zjejd3rBseui~r2is$iep4PF)Lh$cj zzvuLS>c=kLsNlI@>F@oF{`iSiJ^j`d>)-PmP*LV8D z-t%R(KE8a`g{%6G>Rb=3)@?54$=~PSkN(izCaF3fyegJYq#MQ5&yDq&tHU4Fn=_27xRJEB_2h(MEWW}TF0|)e)5U- zv3aUHzkmMs?>eO}HlPI|eKY2hh&PIV9l{ zL1W-muLs>8Jm~!KJT0G?>s8Nu6_4`qVCC1#n{&^{yv!5ddGcHPEuY_4v>#_)^ZUj0 zxgPU$Kh^nLAC!mq>&8|6dpg$vt99FlIdA95Pd@kDNveLsy;^7e?br0L=578SKYS9? zZ`GLZj@Fwqtopfr^RzxcFW%#%#!%;jC+_vZqr(>`s?&`kpHY6ro9kn}u+mri`(}%U zob=LTr~mW*^U{a50$=yby7l3k_0-|Z!}P<0>J`rA&E;ob>xNkun~Qns>;2A$PuQ{h z+xql{^47U7ES3J5B7Yk zy7j_}SIyhL-{(!IPEzyE_m``_K5>24Cx+E}w>Q74e^2K+V71O_-ah|++S~SR`twhtRKv}*j&t0U-NkR#ZQ>1e*fVq z`!^+D^W;bG^Y5xok1vMlhX>UwT$R_;Rdwrz)w;~ZJo&i~Z+m0gkV9xpNZSr_9$`3kK!>+~?|VtVrw`#!{tFJ1ok1m3Y*JHPRq$NZrCg$MDgIvyWb z<<%2M`FJpWMq_jC`FLNQ^LCzGaOAV5mg@TRwU6bYdFH;Q>mi@*MSWKF8P%INtooUY zdGhyuXXoFet|qsfH)r!(u3j6dSM&a(=d>qC^@ILb(|UfRzt*w+pL4_h#uIy7Stmbn z`q5>+ij%KDq^s7co_zU4>xNm+*gW~$_ix_smJ?Nd>wZnix4JI+s;&>@2dz)NisdUz zeZ|XsrK|GR4J)2L=80$jo%7il;8kItJPw61mGs=lK-bB5Kr&DA{l zdwp`|o1QsI)h{`_IpJ0FuJ*U}cpURd%=M~gz8Cj&>agcm)m;~?c;;oE`nnIdegBq8 zs($)0&B?yS=P%})Jay|5`X!IPT0g!ym?ysT_m@7fWumHoY*SO> zxnC`N?SJX{tI+ije_g2GT)$DBdBduYeP*8g{eHvWeD8uus_y+fU&x={zBg}q{3@@W ze)O3aSD3owsl%*Saq_JjRyuj}?x=~F1+*qHvI{Z<+IYasi zbKcI02b|vCX{w+2`sTp5T2FPJ(B=6q536;K=B*2k>XJ9Q9z2*n;_5uHzu$QFV<)P4 zU-tHNB9-X{6G5yvR>oeD{r&EWOzvngcG?$w$1SfpA{XU?d|GaJR@7q`Vn?JNZ zF|5|RuKe6SJ-+LJ)jI7n^W^LO<$0exZj!nmuUq_KE4=DFsm^b@9_D)SGhfA{{EAoQ ztA6GT@zTdWG*5LuFaGn#?mbD}k9Yi9bHZ~T@rQifPkHNcpTztiKCJTUbCDmU&se^P zt^-zl{^lvRKX?B7Y1QPG^X3eftJg;MS?+K9cHcucO=Q0>Na?9Db@>~zhy0|*O?a#fQ)EvzF^7hY$tSAAs$S*i>iE!EHWR5t^~{gP zm0li>`X$eGrB4-`r@DE4`JmJay|bZ!sRk-%&WXPmiD1QLWp& za^60Vz2)y5FiF+@d!s#n=TUWjTR)$~YQ5W=*JrLzPp1wmfAcU;bMX1enXf-PFY}|Z{`g{jPiPw|nYaRMKujLc11FDOAILc36#=So3t_#X1numGnXWn1> z+x7!_^(!|w2AzGHzRqbpzGC+y^WyZY^2rms?w(E^re54b{SwpHJ~K~sbG+{R&YoJS zU+}2bnXmcfddyRuZ|X9yo_=_luVQ@IFdAPX}{<%hOqx zI1wMJi$}4iQ-`Cz$#Xq0>tb^?Z+)F7-~Q}L6V=b}>F~t8K6rHaP#((DtsC_j_3iPk z7goMrCz~fRiEVbVm^s<@?u!!)qCs5 zGY|9RYu@jA|MJh%=(kS@4p{lC=e&JB`1%i@H%Zm`WPd*D z71H606V)r9(LCQ#e)2~3)(g`oWAo(ioH_DK%YX0jJ9{-HUyl!d^v?gPjtBX{^bZ})5e)8tY-~PPk z-Iu@5hL0y6?L&tT<)J+Pbs_(`em$M_!k&N5+xz(+`}~$ks($cOo0I-tXQcn=>k~eS z>8GCgUfk2E!=7JNH)mM!^f6Dd=gW6|W$PqWKXUN*8=OOO8pB5g;BbFK(CJN}trqxIK! zUDx%!zSsTx{_c6t!!w^}@%xrXv>&LcU-j+Apv%|P=j*87^vq8^_00F;UY|Pb=~Z?8 zu;THBpRU(=^1VGSm|E3GUk~c%xB1p%u6c?0>q7c-^+x+#7p&&+g`f28?=#-~q^VW? zYd_Nz=~nxjF2CvWInNV%c)4En%vUiTSn=q>ir35Qhxi%ugP+#t{pC}yeaY0S{tt&V zB|PU5eVBgB>$46h5Ak;u&YjcayAIe}H$Um#Y|-%OKYZNuFZDf-X^M35Q{Vn|Kh<-8 z(@DhNRcPL*evhvXD_!RSKlx{W-|_5&rdIWf2mf9HU(;{;(E7x%TJL7_tLpdqTnDVy z>2c3bx_;iU`}cP$=USd$e1`J6#m?XK+t24;eUr9B)xWfLd5(U%usTo7g}FX?I=c$z z>=82dvhu&QH4b_XqC$%Tuem?|d??(gq- zzD>Wa$MdOqi96S|_=kGtdvUK%9rpC9y6b`!uiD@C=N+$WzcA15tqTLK4=?pSKKQM! zuZQ?^k?!2SXNGpzdg!cRQ=bNAot%{y_!F`wZLvo<#D&-B}Qa_zSElOFYBp5N|> z$A$YtU#y<{TkLgc@^iiFFz1M2^5|tg`BmL|@nQ1Kfp{?a{FL|lx#{Xqq^2v@zr6)<2OI8!~XriP2M=Q+TVLFHXGu4#K64?Rb>ph~y*}3g zt9A2(pLD%GdB+J4nOfE9;E8*4@XYhLpp!VNM>mmP=11eX{niVseszB0IlnLXo10Ec zbl%+hvu(xpwf?*geyZ~ss+-r-!^=5UY_7U~nE5KElYRJbl;`^SN!NLD3Y0hcIZ)4tNTF*Pu!b>r>}>phfk-% zQJ((fXN=cF>xC8H{^O^4_UAwEcl6Y%{=vl?Aw2su^_@rb)%EodAL5HgvDc>#M|G3O zXPEVj`Dw2A^S9b-`S)Fqj-RJhetX>X{I))GVXkjo@u>bN&$?U(toqf{Z+@Nrear86 z=+krV$V2@-zv(2>f$HK4M|t}3q5NFrBh;U|&W-fj=O@p9@L^M{-(U0kgihtReV=|? zkJm3^deC}dl~*6d)ak{ex~>COeSG1k`TYKkt6wp-x*wyzU)UD(-p%G+9~{-Q z&Wf)NE1vzwPwTkVqT$~<7@5LjW_s-Pv5+JeS)W+_ko@|J$)78C8kc+#i=U~)ich# zc@RI*b@G#r|F7;-m$hHmQ9tzSO#!buzdbMT)m*VUokaY(NM~+e^5^RC8CL!FF+cH~ z-)DZg{Xq!zE85d%_vgiQ`RjV<=epz}KCJTU754h7dX=x{xE_d?I(*@$`pp*&=WV&$ z^e^>FN1u9yc%xY9jONW5fu{voY|T|EcS7<9@1(-99>r_)uNk!%;pS z?CDl@KEsN~7k--SJo(i9mjC`Y9lR=*Pt-Sx**`ZnXKo+sAwV%zk9*+w;iTImEx~uAp_PZ`v&Ere@?LPeJKFu-pi+407y489- zZqjeD`$Q)(*IUg=UKQ(ycw#7@*sE9T&py}9Pj&nI;cvgsG}`<9r!0QZMxTGyr{8_7 z>UfYIOg%BwC$4amm;JrItXntCI(>D1;@O`c`pWY8ee}(mgZdynn10iBj?kB<1L?pj zuU=uVPaRe|y}W*ipRw!C{q6Jbyh#pBygGiO zI?OsvIN4^C@nLOcbgOlG+^F*x(x;PXeY*RCzW%{c|IpDvoFMgWO@0b4h;Zv*n)=xGiJpHNf^?Uj))<-8X_0%)pi+g?Qu%}nm zNB!2%PjfwAe)&%A``_w&U*0p)^XWi!aS!Rhicc30R{SbY2jZok zn4e;P@Biv^rdIVQe6uOx@s&Qz^BbSv_P=~09jGqu;V2&u_H?T{pJByw-TX9{Uw?PW z@l&h%g@-l!Cha@m)7R`RDs5_q^umQ>!{1JaKOhp1vNA z>d~#xoT}c-kM>(HtaR-+e$sazuG@OR)T;idi<%Oi$4BbR)5o_Sb+I~3y>-Q_>i7Cw z2dviX^#woa`n+ndbIzSw)wk^17w{Ildgb>m4!g_Ls{Xe2>&82O zzEiEU+TZ56Px|FywcgP@T{zl@H+LQQiS(d-#!u_>`>rQG^_m$tiZ|lUx0%OhIDon&YjcayAD{b+d0Zly3UjLyrTVrl>2eXZ!{&m%5TrN&JXjg$9W^B2l3a9 ztLpdqTnDVy%@2OkwLkCv<7ZE;>fc#>(G9QiTR(mC_4N>cvuMt$I-~u3hSfZJ{1n^w zFMs2{Q>*&(f37L<5ud);`HW{BP##~bo_csty+S;=Dlhvprfc0$KKUNk{8YC;_xeb4 zLf!ulNEe?z)XzsesJ}uwP@i~JG*5r(W?b>)Azdh+$QOQ^w=@5{x0_nj-@IQ_z_X7l zU4EoNx@`CRY|eF<^Ubk7F~r|hICoBu?>b<$Zm*y1Z@TWoMXzcbTixg1ch=1p7gqXSfAf>B{e9?9Zkt-&4?1|_-W)uAJsj1e+e7nuysVG*Suac-F+F~Y zod=IS?lBWp{igf2PQ20cBeeGqClMd2i+ecA$AdlHs_wdA#gpf!x%__5$CsZk@$tl?bM*CaRL}e#n%CoH zeYDSdVd{wOv-I1~(|-Pi&z`92`f?o^<0ayaV)m~Zn>V+Aw2#lQn&WwqpY-kT=Ulq{ z^R#EQuM_Tk{au|W`stZ(J?^s@58}fruRe;Y(~C!S^E#?N*PVVhmp3*pIj}jZ{9jO{o*sMc;4^WzwV3gBVYNLM@_Bjdtca;=)%-@zuZrCeCLpP@(>^5iAOQ} zsyOqh->X|U%zkm@x6fn0_PfVURDQ4e_u0&|ZtH^b5P#jcs(!D}b--%f_8~v%I)6WX zi+!h7pC8h}6ZhsM&%URVn0o4TDjenMulQA7U!wIw_0-`jKh?cHIpd<|Os($6=U(2F z(y!EaUL?{>*2bx>>K{sxRl_!AegbKgI6DU4H0kQ>*%+ix)0633L;r67sa5@=eVY;GS*CBbLvTtIlakboHmc{yb0gd0dz$FHXHG zpFA->luuNLRXy_=TR+UYSiSQ52`{|sL{&eu{rTRV-~X=sc24oz^_cgc)o*_AlP`NM zZpaV(-b0rBKL4q$fXBB5eJAAp#^<-W@`*cz7XMJsd=-!K@nEIb%eyXE@%fT|+n>+9 zy8XRg-@l@h>+8+QKK&J@9v+J`%O zp>h-tB4PK&k!K_#7-E7{RxjMbR zypBrW`I~<8>*Q;f&+ji?(Uj>|&zJi1oHkG0b9^lc`|6=TF||71 z-+5|NGT(kpefyWM>eeMsClMd2i|1n1pZSbO=eRCd_1VY#G>=~o{n1BEt?maMJaKOh zp1vNA>d~#xoT}c-kM>(HtaR1+N#A`q;CpSSt6z8jri54RZ~gYM`TEQiC*p4w>CV-e z+vj><)$jb}C!YLQ4r{N!)vx((TY2*GdMdy9uTCd1_0%&z8dv@Da8xgOe1=&U^Oc|G zdOdjD5l@<^>Q|rK7FZi&x=2IbUVq_ z>EKnde4;*Red5II-)u~Ou1>Gdb-+r$dVS*k<@K*_zp2LW?H4sg^LzUn&pPluAFC(k zdet*u#iM*YSn2ihuCJ$4`R#oDiO;rAuK2w*zK_hmTn|5;AJ$=AnOBFYw`#nq-rPRd z1$+Jc#B+WhebPzOrK-RG?(NFyx?kys^O>*e^z~OUKExMSILgzHZyiv56zRaKk1zZ* z&;ESDi+*})Rlg?ox97#w_xMP^tw-Lx#MD#I{AgVD%fnH<B z59HMkUHpFnc%$cudHSpi%0v8he=VNKCG+^^K9iwE5w z&&TqKsi&U#DjwzI!Ah@}=W|a-ou6W_2cQ0Hx0yz%@BPg-8IOOt9`_;lx460=bW^X& zCr>=e$AkLkBEMnPU%fuz$3w4M{(IDPa(%ry*{8q4Q9Zg9(y!{h{Aj=R!b(@&{-y8y zJ@*64?{~Hz(vph~y*}3gd+ScW{k?*>Jo)ugtNQu-Hzo6{ z^ThdF`K{mj#IU#CRqLzj_xfB1?5&TVbouql>+UtRs$a7(5U=vPx4-pQ*jw-1x;eY5 zkI%50!xw&9m*>m>_38EoUVYnRo8few^QkYN``i6-Kh2{jPQ5B$@z%v3)vk_+u ze$sa?AAU-6PW_VYtrJf_J;=}W+j{iTNu;wbq(4`0w9j?HYK}TT>3e^9%7x2+FD&2B zyDzElag%=2hvti6>a9DTJEzBY9k5z2KhkeM-+95Oj-Fc8w_Vbd_?+vY*W2G-hw2yS zdex!pFh`u&tK-v!@~~P@=EYEd>bg#Ts{8+4-~Nf^-*-(N=Yl+}{HEVSI-5mvR@E8p z=QFJ4d0tDu`E|rYe`ab`f7arKqIKBksjolxnZ6#T-ezO-=IV_0@flWg_`y&5&XbMD zw*Q}7{aIU^65eL_xAoD3)p|FZUsb=?=Q?1uPJZ!|uJ=3t?apndsvpySeBAx}C!^1o z<|S6^T{TaCZvSW>pJ6q}dB9Kl^6!7fsZ*=^r$676@Z7J|cTRcysEg^-NyLZh;!*7N zWj*83IedmypE^Iy^L+V^6PCY^Ob1U)M_l2k-=lifoLpbUtMYUEtru4P>gl)lmlxlw zoeb*!zdEmLQdg|b-^ASC_}0S*div!P@v46H?4vukj}Be^P#(%VXZfk_^~LtbJ#Ug$ zKkglE5+2{ohx%QQyt(|PlZX%1#dERhmxpt8@m&{`Pvi?f&2=BXa^@YUmVcI52;kA{ z&B4=$FHTg?KJi?npMCN$>!Y#t!d@Rg&Gmiz!}dI4YW4l_^V_du?)?2r{M2`Qa)0An zSB0s!uGoAys%xDUUmaFF=K(*h!_P0zKk_A0tNl+0Prp3$xZ$^T=<6Y!&7wK0>Wucg zK3L7;3qR?5J$K*}_nlhR7cWfy=5D!*#Ir&sA(H^fV%&rdPGf9~Vw zOfBkHAKetZzDRwsI=w_Zd3<^6REP2vPd!mTR2M_}#H^bypRs)Ecz?)GJfDBRW9#zw zHIKVpQ^3=oy3VO;|LdsUOxTH{n&AEy8^n^^{7MV zmU+$_^~79nH79vhtRLcup**be>JSggCwd(7Q=LC=yyo;N<$kEAF2B?(#PhhIlZZ!0 zU0mTPPk-_=#_OT=!iw*81V7F5`QR@caNg9a{?ngqN_c!ref!sbRu{XUbQ1BQy10j< zd_36Gt?GP+70)@#PjlsOd}RCochvWISX1H~K7Fx%Jay}l#}}*Pt5gK@Y zWt==ZF!^Hplb>|jPl-1+F1`IzC#w2cA8DO<`su;+8{c}IWAce~pt`t+qkKHr)2-^R z3syY)il64{d($J&np)NK=R3}Y)K9bQ!#qH}= z8|vqOyG`inW}UA4>v;n&(RDy|@mw@tpSaSk^7<3$LG?<9pLlyO{_&q&p&s`|Y?*9EI})8VI>U+16qw5e5npZNLwT)*i<>l4Fjy_?Ois^9B# z9k5!b^Ms#t`MuAFUpBSAb+O@(Xun>v^Yd8yl)iqCo2rfntrMo680r%z_Ug&6;z}p; z)(tB?zVMTd@B3W;@RO!i_ru?3ql=&V?pNiv{t8oXU9ov{^?H5ku+p#gxA*fu{*&iS zt^B5wKKJHiA3mK#b$zQM-Cf;py|9{RfAiD&7RU6)wV!A^)$5l_p4=4adYzH_xxew9 z_v+@Ui&IZM^BIrw^x;GKxu_pj{T}c9biLm1eD3^vPp$69j?0@8p8J*h{G_ig)@Ob6 zAU>?}>Z6!CVmPXkJl6rUE_U7gG?!mz|3&+GgP-4Dx)7wxKYIG@eM52l_N(h*lU z%A4Dty4DRVJ&$jG(s6!2{Zs9mR_@2!UeOqMd`*4#EB!W4olauvsb{{5NBMZL((C2< z3@bjr_-U^1vt9I^CrqvCN4Ed&wfp^8e)8LV>ybAv5g)3HN3qwZ4o7v9=el6lGv=qc z{N8fsNCqL=)`>u!l)YPJ`A5YwygQpK)oT#pEvq*og&S;53h>l6ZMT^_Ro#YncIgy+Rsm@Khgf%ir|?{n6HjlmcG=`&A%RmX$$VCq$| ze1-I@dX=yGs=Rf>if10b@%+Bjz5civtbX-pT8I6QPscp>G5r>+>!+umo_dv6PkzS9 zQ>UACF~o=ZGS0mIM1IP9zjLqO*l&_lKW0l~`1vNjx!%t}{^E%}Kgjp|*5`VlxlrDG z`s#^!-?K0Ktt0d1LUZusT_-=)t>eni9W%A6AMuganLPS1&l7ytqb{FF2daxlvDc># zM|G3OPndPFIzP?j$0yF&K2g>1` z{QC2!7Wd<;i|-He$2pw3_B|e5>#6+4OTEp;bj=^t8||YDD}8?OlfKs{uf5{QQ>!{1 zJaKOhp1vNA>d}RC#jE069qWa?etzQlKHJMb-F{MU|NH+&@jU! zR2Pq8uTLG0>L$;1!K{lt?)hmhzdrec2TWA=Lm!^FHwRB2K9q;@`qqW?=j!$PtQS`L zeB~!y=khD>)qe9;{dJc$CA!Yx)c5+P+TZ%;24* z=R9X>Re#Uzni8IKA@wW2_4P3I@H5|wdwp5YxTmkqXDFX&pYhW?e%C~d-Zn?em~av-}BqN#9Zg9G2LC=&u3W8vk&=cee$k%g=KkL2zr;oLy2;ZIvo3c1{B*s(Uv<wrDIh|_`wLbGxugWJc<6d9ZGfurKc0I7-@r$44 z@$2>1KV)iEr-Nr7%R}e0{VcDqhxqG4^XBS}_VF23bNIqf`tqOqzn?p`s$cSmri54d z?LP7w-+D5y4s*R#K6x4U`m&yJ>Q%Apf)$Tn{4~$|`LDlk`TxH>t9>23d!D#od^MlG zb%_)4H;Z&v)fw$~J+PW*AC`Xq!S<oFG}Pd)YU$U%L!Pco&{i+u3JWu|k`R)2DKdOAMkI%5usr>f(e@q9jiscjaS(muN>|ZseKevCh&-K9G9DdUG z{&L67mp_ladhz~}E`I9Uzq!Bh^;ejB>x#{rtJmvOhn2p^ckXZBxBvMUK6`3ar^B}@ zmQU0-irK$vY~I}d(LO%IYL3?-{G{*uRqy@F_J-2uD>uEgDcQG>zJ9M;vTi+IpX1}H zryd?uPgI9l&lnF@b@_^C-4H*KFZ`tE^Vn1GvUO^4Kd$Z49^V7PV-w*rA zZBwf{9XxSw4xT=!PYmVttqbYT)$8?HFRb+G@KbEx?*2YobB-^Sk9>v|&*QuD`y+qS zUKy$%a@VFrS3iC0%JVzx`tiju`C_O~T;V8hZhz`pH|**0laB8rzxf{R`}WTFeeTwj z=z1KazH_R&ANmu`Prc2?bm6ETKCJrkIx3z#Kk3?^?|$l2CyM%Y2Q&u#%1`$}ovzr+ zOL=kn&9wy(qVb$+-89(Xpd-wmRyX+i)(pNFmg)ioJo+t9VKaXur zrEmO$e1`Igbkc8+_uC$}eQHtHuTR{YlRW1Noy62rr!yC;et9@o7vFlJe4@`Y_-U@+ zFMid1Z$GuDZ+xsV@bp)@=_|hTicVtcsb{_y_xjXfPp_);8TNSmG?yRuJMz|3tNL-z zYf5-thorv82fx*=N1je1K2#T1ILgzH59Q~g>w@}I*LCw#onQZBd;876w@566@aX0o z`9ys^Og;S6%XpNhui{sEeTmi!)l6;3eXjYhEIresysV zNBMZLr(4yn7gjvJ^3z<~`BslOVQT%Z#6k#LedGBUKhgR!PiNg|&RpGIpX-2?K41As*UyW;`VTLjTD{-@(t%AW zeN27#0Z*NNB3`xLDo+>E%{cSwbk(7H#`1}DVAkoV^HZK*hko|(snz{B@@q{A&+ClT zul6@z-Df(9si&U#UfkjSLHRsh&I-@DkO+U@ipLKoqxq0%C4y^L(754hnVWrc{>xUIzou96sUwhs6#Hm&N zvd1?i`yZb^AN7mX&2u00JAc&i)#b$%>Vx{kiRv)x8JmlraptpM%uoINy7Ged^Go&o zd#3d1=;vShs-8#(rXIdLtnzc~Jso;5`PL2b67}&@?D=x{_aT~Nd`bVre1`Ig_Fww# z`R?2goHVtlZ$G3dnD1OkUGH1ztLw8aC=c=1jjQVS`dkO>t(%{8eSh%42eexJ@(eBD_1 zX|D72s82q5YH>dfxLsr5B0f|Xk7BP+9ggZIkIyjcV%N=2bK6ge zH#T1I#Yat4b-zE)XMB3lT>j$8XTMmT4y^L(^u^G8aUxxq^-32{9nulwLC=@`l()Y> z_Ss{nl=>C#YYcjP(g)o~JbCLeS3MCQs*8I#%1>U#y*cWxtJjamPkesA;e_S)^QYgc zO~C854G+zu|$GUM;f0So^t^-#6>gl)FgL~cKwUea!9-nAR^eVsIFMcQA`7WR6 zI-t6^hogKv*wd}*e1sLxf8DsMey`7Uz-ry< z{G@AtpY-FmpIX&#{j#QnSM6{2EB!W4ADzT%y>s()=l0=`_PZ{qKhg6RKk0iP^ZUxtn_+$KEjG` zPWo+szyGrjm|E4h|7ug>r+)h4>h+2BWWQKF_2_0^T%o>1{3_q8>%+5dD4*zY&rdqu zU%vaE51U%l?{;oepo^cnqkhv#OucGO@~T)r#G4!IOYE&b`TR8B-yi(^HHS`A^`nny zo%Hcj&w1fIQODO`A^y72yt%r)K6TjB=O;hB9=!0VH%zVSbnvQJKGAyI2RapI|7c8a zlwa{ibzB#$^qiagq;G$|@S5fKmuDT(7>Q($n0o4&ui{ZY9<20wc|OC6 z&#%hw|9-*4r&jg+|8(`IK0o=Z?z*7$i6Q>FaaH|ZpX-3ty6t0r(v?5t>gJsK)(1Bw zyvlE{>(g)Rk*AZG>s8Nu6_4`qV5Qf~yDnJq)%j_z^I(tb+Y_Yv=>G@fe(}|O`p|qa z#9ueAs^9B#9k91&Lf``DwoVz3+aviK@QG*4BwvohOyw`g&-6n?-Zx>dfs+zpH+}@DtDboku_PG1F4j zulPn|(05Mf^|%l8puF>j4#Z2nDxbWJv#*LX-_yx@Z~e*Vr@4H&(+ie=zxaq_TPJ;w zgVa-ZZqSA5Rh)WwnIDbKOMdG1;_MgetLEVGgWvMrU;gHgE&se&oxZp?2TxxQNA>7d zXiin{B?v_QvvG-s8{dqcg;@%uQefUrw%IjMf(x0o>>$6_i(@(#> zKKY$@-G6FTKlJNOi7rh2YJWRlcAi&@MmjL{R*loAjC;EHu;N=ctazRm`AOIJ?Voey z)~VI^!|C9OdvlWKaYZL_RF7_jbgOzVKiY4-u+sIs#!ve8_iK+_K2JtJAGCk%XY;K` z-n>No%_7}Zbw>ML7p&&>UVm?UR(k~4|Gqz{FV|yUKUMSMVPu+ZdqK*&gB;xNXG;dVD$5)4y zuGb0t^2?@m$P)F{GDqFRyOhP(INy z!B6wN-+9g7F8}?LGj=ovAMxpm`7MU-ul+AiFW0LsFE+1-`YN6}ta$R(eEnI^7%%s? z`|$DSK6@JF@8!Lw{kVQZU+P&GKlv!HPhOmezizZ%II3@b6<-}zJg?jMX&t`r^SwLV zeQI?-wj9)y=tBDX({FjGFJt-C!^^z5huM#x`Hb-rlV{y9`C@+ala9yzJx^;Vx%wVk zTPI!nlpajK<>@EVgZQgr>d)1g+i%@af6nDAKk@i=!KusNXWR3(O#x4T>gvz?p|6Lj zx7padxjLhLe1_GWYJWSw?|!$ZPp$69n->Q2IsFvV6`ODUiS%;4c>1Ay^3@@pcrI4` zd0o~4t2un-Cw;F6zi@5)eJQVBzI8=YG7mrX`G==2_PLGq$&2w}HK)q!gLtr)SBF)f zx^+YOM6cWUN!NWi`;iw-E#435#}oJFB#*yz5>ro|PKBd9{mIW5uZPwPD?WXGiuwIF z-#l+>wf~>BwJG7*r>Q@Bep??sn0o7u=g#TzT?eez%NKsq^?kOle`@ckRsFJKni5^- zbn3fb_OUvB{fX-M>q5G4R2LsseR&-f&;H~mU7w#k{-}MXR`qwD+LU_wmEZc@2lM1% zuD8ms%F{;|%Fjjpu-DH|*Y8|^%AR+fTGcPww<+PdU#ahL!&h~E(E7v>e^=q$IX%AX zfYrMB#ZS8S=f|FT;nb>52T$CagQu^DsfSOe!cm_7WBI~?)jEWUH$a+d%jhd*XP{Nyn5>4LG>Q?`08*~J?n;* zjygZh<=2D%_7PL7`#}eqmq`zvvzEK^1L;Z;!*ZidK{rvy;qQ+47`y_a)p5N9*Co$Klp7|;sE!(0ob1zI;iw+n3h7t%UVgOSdSRt&pV`0ky*}Cf z|1nn1E$8_Rm#hC0d3}-pAEWPAz3bc)C$jtTwpX+};=Z_F|DxFb<$2iipgO)fokV=7 zE*`~RpE?}XO&*_N*2SI|`Dw1-mpbiB51gp#7cXuQp8G`)@-zL`2dz&G>Fg?;JEzBY z9k5!r{l-ta{JQh?%Rf&$Y4O4wZ@1sgSw6$cZ~HI(_WI-#|9jiis!oUB;@%uQeLWo2 zqg$alRlS!V?YCZ7=~m~7*C!9Zwte4zx8Ie6Ezf6Iv8&gEUY{KDGrusk`o096yzjj^ z*{8q4)Wf4QioHH{II5dG>xJs6>pbD7y3bE8yvYGmtMAWNI{MTr#Ir6apIGT^HgC?T zPLJ<8V5RGMk)L!u4?h2|o<6lYPp*D-Q%YXyd)&C6_}1fjT0SxL)HC0UdwuG#r&rZo z7p!=E;itKtFE79A^6No5c;en1Jbn039?I)m7t)`r*Xy%hSn1ox>9_OyDJL!eyy4=r zo05H-`qt$>CJ#UNck-;4o)}hnb%+P$N6~tdpL%#O`C`7L-}Jult%E13y1%bUS3f=I zoZ=%rvHK}cPfQ1@i+e~1R(!g!;`Q?SA$~QVpX#?((QWFAq}>52`2j>J@*q zui{xZ%sJxf`O?qRzI)5#CaU|feersbZZ)4SU(M6!aUo8`ht(YQQKXZoUtSEWyn1i_ zc>Lfe-J356CtlW`5!K)JnpVK$D?R93r!Q|^{HBwL57otUvFewHb9M1u7nDzQZt&Aw z`A=`#KDDUt`I4r0j@Qrpq|fi&|37zgjxUvue1;Xz<2e0x zF2C`b)2CK-Iyt{LM<1TY1)W4Zed^*0M|t{_pD|t!tru2&&ujcN&pC7Hf4}e4>hml* zc;en1JbgV(J$yP9j`H;5L;1OAy-+jp7M&R z)z9zgxGz;KpQvvXvwzjtyt)0OeSC)19Cd!ucb>fRZg-hl-H*}#zrpjc=LhrY_Yi+w zXx?1C(LO%HYEE^2+uzSS`MFc8|DT>tt|Mc-M7&YV{*38lepkmim(Q@8Z~yU=j{W_J zKfYjUwZHwoE1vTx^_~Cp)vZUKP9pxQNN?4CeWN5uX&-lz_rVWnpu@{_*(z4u@CwDLF@SonrUVxnaKbWWQJ)pI+*yLp&&-dh)Q+t@8Sy zdFDWQI%2-^GxHBPwtZ5lzUtpw&-Iy8AwI+tk7BP+9ggZIkIyjcV*c~fT<6I@U)dPW zn~U48m+bs`13o?I^8osI(7Gz5vu-qRu5PbS9aj4GH$VB|Jou--ZqBsAe!Zzm7Pj%n_zUOUDom%bh<6hO2^toU3VeW6b`aC~oUQ7p8d3A^f*&kUuz7y&LjFT{iG}Je#p}k(}C*Z9@2popDwI;y}bV3 zJbv*L-}g_xx&5KjDBqXZ`uwJhH+p{Kr{B3wyx#R~wmx@GkMBBQwLW!z()IKCC*Jhr zsnz}1{rA~!*hxOZ8)j|n{5?v3^4t6QM?dU7Q|ntjfi8sb=w>XRh-Y2m3bTLJnEu@U z(LUD$dvo|n-}(K~Gnc>bbJ8C)2HooU(mCV#fo`5B^7O>HUiHjZ@hBe;R(id>epvDO zmHXT4lZ_7_KDDYJ{d-M`Z~UasudG`KG&lL`sh9bziy>Wc)h~|^^<`Y;=|H?hb$-(K z`r+J%96GhA>&Fw*5m%_MhogGEIaPf$Z=F59^}>p8AM%sF*De3}fjdvF>iK-BKlL5= z)&4if`ou8x)*a8C)8o4iSgn_@{G{7{>vCh`FFt?j)av!iDF-wqyvlF7|K9wz?#hoU z-|OQutaO|k{G{vk$^ZMV6Q@@9gASg!HwRB&4@dRrR%lLD@8w7Ptrzxm`AOgJ@BH;e z2TrZ(kGili@T&ct&xgr(pXC#Cz3Q3o#l1dt*wd@(d>+-|C%*IG8=qc&J$TJoZ6co6 zeYp;K=Lx=bxli(msi&U#UfkhdG~_WJw%SGHG%>c>5`Dbh_nbE^Hz z=d9C%bYPWN?_t$14|_UQU4P|=KKqcL`1bcD-+%r@RsT-=^D`Un6FxmL-Q=m~`tama zPoF%j@>#EVneWA^E4FSZpU4+}nkRqCGww3AsO!fQ_vYZ~>tX8Q)2VQjr@!J?d3}l2 z3)NFc%ulh$`xoE4{QcM?{-i0r#wDPJv@0><<()ulZW^{OuiUa{jQte zc+T%zKeL^ciB6)v9**jzzL>svRUFl^URceukNHWT-;clWHdCuQ9X!u}@{q3m zEU&MJ`0GOR=IV|1@flWg)cHx@^W~#{_zqL6pSRJ$6Vnk_s1NEBL-|T))ja*V{k=Zx zg_XX~IrvG}`^&HX+D)fc_k#{z70V~;8^!D&jp>i_E8eIMpJAov@y<{B{NCecCr_>F zTh40?y52{oex4_Eoj>aG^u(#Bp7~zf>r*!u_H?sO2WDODy7_6|mIdKvk9q9$FZIX$ zbYtN0j~=A!e#(nIuIMD9Bqy|CihXZ$qRdGdYVXgl5h-u9KIgy+7bK3)4+UF`XoP9pxQNbldO zpU<$G&li5;@$s@lkD8XOZvA*-bHo+u>tX8QSGw|}yox`!pN{oH`9%ATpXRv_pLzBx zrWWcU1;81 zz0p2?!)lJlJwNF?zklqK=S{8ZXEpnGe*ZiDG{^l?rwgqsV|hBT%Bw?qRV)wbLixF< zAL_RrsLoG$ejoJtU!GdkQ%6ikTp|4)rXGH!D?iFh{@gzO(0ZYKqB=j#bspUH*k?>F z>c{*_Q^2dvlic6tnx{`p58|&ISJm(Jxei#Z+xtL%()IJ=9WT25)T;hVuWw5Bzw?5= zdCnBXbEc|BDhzxZi>`zi6p#tAQ9{{M8({AN?a)1Uh87r)iT z>U0wEp}Ke!dwuF~R5y8}e!Kqk+x|TH!;hGx)lYbKn?&C^kn`OK`mRS`mB&jxeVOmY z^kDMo!mN+R`eD__SAJTT=gS}1Yrm;g-QQ=UYoDe*UB2Q&_rpASaq5l6`r&9_@~kKG zRg9mQeEXB1*2%9IJ?@ODMg6+pY6_k&@#&i9e(@Vm-CTLGIz4qd@=zUSf5!S?-gi0> zFHxO-VpW&tr`Z30`HW|^7l6KB==Y21fu+q@}s;Sf3A-8!b+d7{4~ewlPiAs1yig2|G~lk55~D} ze_IFr9$GKN7th72Kl2%n&T)OP>Qm>Zc|IR}*JGCdKlIaI+Zg)rQ{R15ht5mS5AyV2 z>Q(vVWlSGtovu1eUN26aitqZ1{r39}FL`VGKC<)u$j3A#^Sw?7|p3jhv%dCx!%5UfIGamQAsm15x`cuyw^$PXj zLwP7)>1;M{&Zth0Z@sY6Rp%#N_u+jnK4xmQ|IcY(kJ$NnRds&r&vS!*53BXAnm2E5 z|7af{VKs*@{G{K0D{W)r4}YvN)Zg;en667SGgZdKKltMO3+O|&6(>r zJxFI)VQ&r}%+FXJ(t{P>>ui4FdA$Gd0S}%e)eqjkO~BJ{F4Uj*Lm$lb$xtn_+$KKFF^V*ldv z`rK#pX$bcj_Tskf%1vY1Adz4{C?vx z&zV}(uld!cz(+_|zx$YV>#@)A@zhhV%Ht&_KkH&VDBnZt#?uewlPAwlb$;FEzaBhM z)ct&iEDyQQ#5a8Uke_(+)+4W9td0-W z#T8~h{fbW)R=i%`T$p_M>iqWqp+D-uCr+*Wrjyr^FRS$KO8WSg`p%iG)7PJ|eCmzH^zbv5AH~u2;K9_l4$o`+l=uHP z+~iO1IJK&u@v}_{kFTjuH~qFAc{+)yr=IyL9_8b~O0So9U9jRiXZdNa{Ga{O{ias+ zZO?8>c$ME?-|*FZ>yf9Eh!54pJsjmHFXP@Eb=L*u6ZyqY^X$(nfBo#KMg93d(iHHV zJE^Omuj=}Gh`%m0Z?4{GAD>}0hcEo3FaP8}KWA!Hf65!0QocW=&tLswb?b4T&7-G| zubw*U_;~VAU7Sc)KTKZ6bn(RyUtMf}@>AW<=YRRkS4>p(H{Ydo;_0UcE5G$wmpG9Q zR2TPfl#d5{x>eovz=~&I@zY%I=TAPpec@Z34xYF-2TxxQNA>6?($D$ff_5Ak7@S0BaH5yMfP&6U5?3vNA8)sHx~b*l4~9#rQy zU8r8gbReF1E}EBpsoRUQU#zd1gU1hk%KNU%t_O{Rx$J!1C@Pu=?D@x|)+>Qx@E!t6_)x;Zd;87GepOupFb z3x3k^_X>9ZJdIQTI`SD}^E2c0+xz)LzyIeas`^bo-#R`1Q_nuF_P4t8$olAsbA4hs z%FFd-Os_(Gh-bdM7|P>A*UwKpKX2Ib&DTv-^)EfNF?zcEriX8y=Z&o6Ss$$O>WQO# zJeWG8v3}Rn>*FWB&j&AfUi&~${nPJlO3opC`sUiN{FR65`su0DQ_pZ3d-)w*ITb&MG70V~;8^!EjH8yW<|7ahdVKv7-<0pN7pKzz+ zTdMRo+}aL;0M$)5gU=@H}6>3bYR8jD?iP1u3!DD2TiT&Z@X_(qRZFRr<=ag*I&h{x7pad zxjLhLc(9t|Jm9D6_kQQ(``mkKRp0)W#-LmI?SApweCLS!VIDm(K2#U?aFmY+d%9I! zKdgB2{B*sGUmMwY&Ar;^gZ{qN`R&IK-JkF9GySIDL+gV0;vSCjlb3OCjyj*Ae4_oy zPxE|!^1w51Kef0Y`tiiQIe7Z;#fj?KC$5lARnL6IukzV1wq7WoI@NjNJh=V|=TEKt zzWnH>M929+ALjnXht?%dq_bJ1J6C6JpX-5DzdAqhyr2K`KW<+@R?pwxali6<+%J02 z{c%6!6H`w;^S!v&rw)61Rh^GL9p@`Q@%eSytDZiMc#FhhAfEZ=i7V9C!_>pCbmd2R z$)DS&A6hSzPhBxT#rEH?Kdl+3!nt#LeAfZ1b=$W5q|2`_ zU((JK?}skBS5u-}`RUxiGoQYE#`4L>%e)vSPrkzBXFS?x-MxOVL->i$?_2-MVbe(W zWBajf1$e9O?_3`}nAch5lP4bK<3as%(e=QppI`hm*XLC)eEI!bs~fNo!lRq9d?MZ` zW`8fH+vAPaNA>s&D}Cq5#==kfKL38-w)RTJ{kZC$O$pEaN`1EnFVVVS)>Cg?u{m?~ zdVP5vmA-S9pLF^7%DXN9ykYe3e|sNQo$uyUSgm)}yg76GNBj54A3d0QRX%yWICaFZ*I(6L2dsFmJN@?ah97!Bb57mwb6cPO)c5${uev^HePW2e zt8ng|9^ZAq-n#iom)|E}`;e(s{YzIgMY_)E)X)7**ZHF^PfwhBRX%wc)6Kk?E|iB= zULBePD@x;A3c>3_giR#%Wo{RLePabA{G`3z?^?AJW(_BAq z*!_7`<=k?f&-`1i{!3K(?VNbv|MiTC?0$@%-{w0%Otc<|Cx-aD3g^!0@zq^VwQhBO z;@RKleYpL?rTW``s443+M;~+^sYCZS>+~QUSmo6#?DeU`N~f3C?>Z~KIzPo;U%dK^ zpPE|LPyd;wgqQo*9M6aRmDiW+lZW{0!c}!g`&V6r+L0j zef{h5+q!a26_4`qV5Qf~^AT2jb$*)5uZJDK{QZ-o7e6W1hoAcT({J(I^Q4-Sb2HWl zSFLAX#{7WlIoG+7e*6EtKJXV$np*ul`I=vBN^~K8Xy5Z!9^W}+J@ODA;)yFvUdGv{ z4%IW3uh6<-_KW$#PkHZmc7HzD9Og^U?;g)S%l+-~e#aN?KaEh|y5irXwodEmAwI+x z_i&V-yo`Hu)a`31pXhZqKh1OgKH@g*13`8Dc;en1JbgVJ)uY=(^Lo6jkM>zFOdYX( z%uo8h5Am{RY@4X+`~A<>iRV0`2i>nczs36KB+`NE;<;G$%fq?4_?}>Z6!Cy?9jDb-=36b@S7F`48RV?5WlL zxN2)t!gKzoezm{#LF*I4)LU1)s(!D}b--%9{Ng8F=gb$kwBK(~Kk1K}5}y9lul&ya zO(!w+)HC0UdwuG#r&rba3@e^IKh5>?`H!A?r-`C|(jPPiUC+04pvMP)>AFAehdezo z9jGqu;V2&u_H?VdepvDN#ZT93fB(_nKY40ZAN_ogpZqqTKD0hD#9ueAs^9B#9k5zA z9e#@Wb@2_h!EGXJ<`R z^`~rY4${R>y~=O*M_oTXn0k1bAB}sum421ax%6Sy#q{_oUi=xKjbD86)`{x<{yR@? zop{b^dQd+1xAn-=Nu&eS#iQ8kQ-`Cv$#Y#W>tffEst)UczmS? z?O*!x)*~;b2l3a9tLpdqTnDVy?R^P9>Du3K{V(kocHEEKEd=qL52^1y@Ks%35AoN9 z=FQa`?c+1-&EY3~?=KHJVfptPjy$?C=z4vV`ts?w*nKvSo;dZ?GhfA{{N!a^>1SRa z%(|E_xxektcmGp!Nc~$MY@K|m_BTJ(@vX;P`9$l3>f#=b^6_9#x2n4?Sn>G5PjmUb z_f1|owW{y)$fjhU;?oz8p5OTNa=q$_<|U5u^!51a$rnTGhVqH_AwTK+dBX#*XwIwe z{a>2`UHde3dw%OPkDfU7)H9#)C{G_gl%I?GVXvQ`uGj06Gk*BAsa2g0UKPtH>Kn!E zAC2jc@+;n`4xeGA=iJ~YeXmbG`Lp{?t)5Tc|E8vdr$6<3``bBYee_`JRr%zN#`K`R zjOBaiI$*`8&rflC7r(Lb*)Ke1YE?h(RZR)6^4sSx&Smqhhu?G(@u9l7!cm@nd?-H` zT^FqS`N2U`E`t~fFE))kvGSFhKnUio33$9o_3-3j<}yzi*@7Sr;^J(hv zRUW!ud^B$q>1-Cwo2xUokI%5`=NCWm_?erZ~&`b}TgR*t7XuZPb$U+g}M>A}=n zcRY7akDu33t=Dz)6OWJQ{GVT*QqB`UpQjJ?!Q9{S?gM?-K?mZ`MS5^!{1 zJaKPM^30`^n0o4TMzPnI^^8a7STC&ls@H>Fe?R^W+ox83U$XdOp>?@0sh|5Be-u-1 zZfwrnzT}VU@EN9#*m=rN`kpVZeN*$>`JUfrtNeBz(KnAiv_3J!-&HtwPLJ<8V6|@h zo1b*~^_Sa@nOfZsI(U`exxcede}(v~BHdN{^^NNA8R}2;ex9H7`S^oRY=59vT|b_< zHwRB&4^t1HP7g=<$;-GmN8Ng%d?I~*itW$$KJ)CUMg8FYn*yHaz0}p;J5T5&rru^_ z`g3(g`}hp2IqLkRZ-2l239p%2)!+7)O-Y}}N9xP-o35C?JUwyhsb{{5NBMZL((C2* z!-{Wz^3(PDdrEs;f5g)i6TiTyX-ud^DoeQb&e$iLA9(l3*4Dojr&Yjca zyAD{b+j+uIx_&h!JOYZLUuhi|G-_}JZG4)oB>F(-&*8{70{Nkte-Ez^e z-(B0^qqhGq`EV=X_0AJKXg#@~)nT>X&E{9t@AbK^9?$zoe&R1aCfRu15ywxL=zi?I ztzB_XpKkTIlc$rIzFJr2M`Qg^-`rSVVsHKUz4PQB-qD=r_f=;!W%{{Zy6HEb$CL9$ zo*twFtGs%Jy*_nV>GbmYVa2cfw!go1(!NtGzrXR+rbL%tsn1XQSY4mII1wLKbJXXe zd8wN*-72q656UOnZ~Ua|T;Bcn@Y1mxj`<94n6?eF+EkDXf8>9{ZA-W)uA zJsj1e3+aki#ko4x3#)#w6ZnbeJh;dAPMnskZvA*-bHo+u>*1(gHAmhY@mw73w_aGy z>G}PLpKU+s@%^jwls%(>xS7U<_kaR$vfMI#=a-W)uA z_~JzM>=Tb7-HM;}jH|xPTQAJISe>8d`FwE4xy!$QdG);-1JAiY4?2(RXL+&PM<)?~ zT}Xef-e?~mVKqlR&u{N9-+0V(rdIW9ezhs_4brzherDY~_X!_QJ@u+QUSjgIF2;lM zJ+y8-{ZKx6^88fi_X&4u8TIWqYR=GwbfEoB50B4P9ghyI^6JpMDwcvaS_Gyn1TZ<|`xuiC3I_=ZnkOxM0u7w7XOJ?qA&SLN{_KFqur(uMLcb*j96m^?bJ zo1gN|gR6e~%&Ap9b$lKm5B2j`o_-JU*M;Vd>docz8CLy#;U}J-?|krm?HAJ2^Z#SC zFY`L8^8!EX=EBrlHQub=Xn)UV>!-(0v3>iLlO8cq)h|A~b=tS)q+XsUc#!Yr_4MdN zec}r7V8yHQ)|>Uz!-L5e^M#*u?C)D1aj%K0e$}@dgRXvhke_^{E1&zD9v&U2E*`~R zpE?}XO`d+3b+PN`r|WgD?|W8rRDIiitrO4VfF9&0zvcBo>k~u#U4?V!^!Tm=R_nGu z`AOHg{;6x)`+4;pcWz2_D?i<@s;=KWF|=NFafLlzufADb>xR|5%5Og1=!@-N{PuYj zJ&%uEuYFHP-Fozi=|TK;~>SM6`-@5erS>@>># zxZ-`w>#$FAJ?>Y|wH|ZT6H`w;^Hn^`PhQ5Ae&$_QuOE+}`22pu%bz$&-$>#mZIZq` zFZAd6Ew9gg%DlSkQ5VB1KU&ADaIU^}!>WJhK3w>T$FKbkJZM^y`*Gt-+Y0gcmg{zV z@_ty)uHN6d9_y|0RXzDt?7CpZ<2OIe-TirNbBrIAfAV~W@`>u{xBY#`w?1WRQGfs9 zMp%c}A*oyKZ+*~wF-*O6#jEP~`dkOB)=P(#g!Rm!5pDPaW!msVASPzv9b7ev17)rIQX@{{8u@wzf%lyZ3kVgWoXkhnQdK zw{!hZ-`N=I`uHvG%}Jj9OeZn*)amqal#d5{x>enJVa49;)8m$7{6;bmUj!|cb; ze8zZ*$+K>ld~vnEodquXx?1C(LO%IY7W2bU;571 zZ@u7@sa2g0p13y$PhSsH51&qjqdfiiP<}32FVtV@@>AXZzRPdzJGH8xwtr*L#itMT zJFn!$9#?b{@z;&!%+>Ansl!U&b@P*dyZ3kH*bV3T46*nrhUvF`c=@^s{_$NNt)J&3=naPFKQ-*v!h-PQT+?$x$w2#lQn&Wwm zpY-|lOW%3e)T&MgPu!b>r>}>jdUSheUXPdc(LU>isUxoTx9@A7`&-YQsQf;%{r9>% z-|tlGp_}J7exh|k>suB7t#!Z525 zK3yoE*sIfp*)LY-r@Zrf+r8T)_ha<)!RkD*ZgVr1w+@IWo{OWpIVa<4E`3<>Jns2P zfAO@q@e8+KK2NT@LsP=@_(*-Pdvbs4vo3LB>W#*9M)?(Qu8!-0RloB${r2*$1-`$k(D!+4o<6BQYU#i1g zZ)LSLdho zIVX1i{n+LlUwS_Bv*O98-~Rr|ULR>YRQ?<~fg3-+iR7ZawtH^dSDaaaH|ZpX-3ty7|RVx}GoZ@$IKh zt?K@s5}y9l_i`rv)@NPf#MD#Id=-!K@nEIb%ex+kpSs@H@KfD+^5VxYzy3b!(@hDl z@>@RV^WR)`deC~;jjQVS`tmxeb@Mg-_IcGyw;wgNs*nEuy!~yR(0y>;$S3A{)iYnk zqkKGA>GkrXe)EN&=H6;C;6az&X8M=UYfrkkG5CgWJ?27ns4xAN*GEsEJXG&tkFRbH ztaS0St`BBite>CqUJvfLeEXDgKMp>!G3fG1AN06!e#q-H7s^9?Smo76F?GanR3~|^ z17=;U&QEilujih(e4hCHTc6h%^q|L0&u{bSiRsKmI&=GyKUW7oF?IQpe%qgaeBe(` zE$WAUu_^E|{Smtlc-A3yKk?=9tW!Nvy@#on{7P58;%D9*SoQOjpLl+Lx#RfueS7E4 z_W1p@>O65i^U?a~_YfcAiz^)E>96=zUSA@gp?d1@g`etvzH`(s-EnI5c~+&PPrX7s z>k?O3>CDa3-&K9C2Uc^``DtCgKe+vOA2PMt|CcTr@cym#ceVa1Z+%sLG(W1tM_B3e zi=XtJGkcxiPFnRXi|=dFtuubtW(8@vBL za&;$`^L&QO)qjcjn|}Lw+P+7%lU&{BCv4n*{h7CJ zm^xyQYkt!4e&>ldJ$a(4KWl62#Phn29`rn1-4FfNCx&!(70#X0Zr@<|m%>`=9>08SQ?2ZU44H zy#HwX+qx^Ct9-AI&#=<*K9HYu`S^pET{yL>)4>z>=HThW7bmJ`pSXwgR^^TM)3;tI zpE{n`_^IyvzVr3VKi@fNQNVLw=)>N5qTl+&kj}2cxpR7a*8!__J73dpuTOUW{lUt) zmUkcc4CS-CI#2BT|M^49|9|nL4r~f^^{4LW{hr#xpXNHhcij9{Q>*)N z^+8RE?*Gr;-ACPCR^{S<>`m+khKk}*v<;%*JAx1@3L=6pse}|-Dx`uacoL5y66g-! zw&DvKDy7k`9BHNgkfTQ`GgKscE=^?UnUe}$>HuGqY}dc8h%Sn1n;{N$hW``2ImlBrca zzt7a;BlRo4t;byR5>rn-^S!v&rw)61Ro!(#ywv3jKh@>;J?nz0RlU;5Ie3Zm@x_Vi z*}rN`e{TP1pY_6Oj{V6``u=|B^B*{CYE}Q_k2WPd`!w}EkK!d-7tDIrdtIvmwap6h~H&zPU) zdVTVt5AHKjJ)iz)`+B8qe}C!mk$QYKpDvV#@-X$*6|bt_>&xq?*6VT4Pdw+zho9VD z0J2nUJzI^4k zI-SJSQ_p-a?)9m|o?cbwGpu;7o1f--Kla?$-Fa$NKlQN2po^dSe!k?lx>%h~B0j9< zsE?vK_;jIsVy{jYX200uo}YC2{krpZov7;i@Wj12c=~#ndiZqaV%4Adj7R5KFRc3L z^Hc0R**ku|yd++qczmS3`)EI_)3+{hBK};YGq*4Kb9Gz~tor$te)H>-cl+6CDeBgh zdgiECsIP~kdc8SSy_dJX9#7qRVb$*(rK5Xk5^&#B~Kk@y^5_{T|bmhblv=<NDdCOoZ}as*>k~u#9ffn}^!Tm=R_nI^_(|9M z-^U!XWolL5{E()EXCJ4&`$b<}pLK~7@u9kS6nlMH&v>w#4teSV6)KDo^e51(4q z4{N{9-1dG|<+px*)3qLb@`<@#^~_iCC?5}2dcC~sf)(F6%1?8h>%aT%<{A6lOn;;$Q5)$jGW4p^<5U+K5sUw+}7c7UkU$@v-MCE`Kr6DMZ>x?*!i z^?H2Q0V`emn4fgpr(!oYHvQT1{-%Q`?#;o|*TYdgx{$7TRh+A1y|C){xKF?B@00dj z{=R~X?%$N?d3@w`$>;ekcAx1ark;A{N8_qr9**iIkIyjc;>vG+9Pkf!nW*l^MSHhS z>*Fgu*n54V-}=Om&W^&lb9#K&0jqWMm7jF|JpWHy+KyDe{l7IOyvlFS!+6mCc0c43 zbG_=BAC0Sic{r+E*J^}CpP~N5>iqWm;%{AYzo~`a zM;zJ|tYcNbt&2`#u5)foZ*E`mM|E5eOdWCMx7R0!AGiE|Z2rAewVvMowyp}TZ{29U zu+qiGIA#NSb9?x=o`uMR6+zVMSD z?!(<~^Wv%1{n+{UfH&M-@z6|bt_>vJ8jS}#A+Z=Z)a^q+rxYE{4Xu}#VRxqe%ZxpWe9z3Q1CjjR65XFRI! zx?t7k+~B8q@>gB-$f;F5KW{(kxB1qi&%8u@s4ni|C_i}__vWa(E-0T!pPyo{Pj-I4 zeL8m2F`wa0vo<#Dv-I2k{-Jxmd}>v{_Uv{?=sFkZyI=a%<;Bi-d3rGQ@Z@2YSBDi( z9^&^f`PL1qeszA*vA<6^W%>JNHon=E_=Znk?ENY{bHwfkzC2#)Re8J$>BHpFg;}rS z)YA{EKK1mQ9}j=P^50*s``>Rc-}+&--tEn=s^9B#9k5!beU*OudFP9tc;3|N_4(yL z))dY6b7bl}FX*c~zy70M4_b%yi+kAP^BGop9{2pD>+=u~eCCscFZ{G#=gE=R9WzmX!G>Kl;?e2N!PAEi z<)OU3b)z|>x;?)2!b;aZ<0oDDo&O$V<=k?f&v3bVW8`t1`W{`#%o{=ZCuWm`f)SAF7MzV%0AX=j!6SE+}6iKh1T{{L?e`oLYQ8q0h(CO+WSX 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∓k8%m*VO&j72)OkY4yYI3mLN8i9$b&<8!0m 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 diff --git a/activitysim/abm/test/data/override_hh_ids.csv b/activitysim/abm/test/data/override_hh_ids.csv index 5586cf78b..f40ca6157 100644 --- a/activitysim/abm/test/data/override_hh_ids.csv +++ b/activitysim/abm/test/data/override_hh_ids.csv @@ -1,11 +1,11 @@ household_id -702445 -608031 -93713 -93769 -2525286 -945618 -945700 -1321232 -237967 -1320576 +257127 +566664 +2344918 +823865 +2819629 +2820385 +111572 +110877 +1131400 +26388 diff --git a/activitysim/abm/test/data/skims.omx b/activitysim/abm/test/data/skims.omx index d86a5e8dd5352eead3761ef510ca29b13f6e23c0..18c6dc4408eeb1957945567d0870afdee3dd96f1 100644 GIT binary patch literal 3674745 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ₕ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%0DebvT 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 P(wAhC&urFdbn;A%pQmNE%?b*7BP*OL?W}4$ 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

tP&miY(oP&`_s`gPX0RoyY!} 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 z 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 T4e3E(_vX`)B$W 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%0A^qhI6n{?vX#MtRODo6Ox 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 gmOLrTpN(W7M)X^3XtVEnlw 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⁣!jlw9z*YBI#EsiTPxSw~jDm@eB#P0S= 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

?aI@fz4sL} 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

6uARp_NASG&aT+8d~ovi$aI6U#TU3S&WSh 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 BJSqSH literal 4354008 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