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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/core_tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -128,6 +128,8 @@ jobs:
run: |
uv run pytest --pyargs activitysim.cli

- run: uv run pytest test/test_skim_name_conflicts.py
- run: uv run pytest test/random_seed/test_random_seed.py

builtin_regional_models:
needs: foundation
Expand DownExpand Up@@ -166,7 +168,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -235,7 +237,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -277,7 +279,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand DownExpand Up@@ -310,7 +312,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install activitysim
run: |
uv sync --locked --only-group github-action
Expand Down
65 changes: 63 additions & 2 deletions activitysim/core/skim_dataset.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
import glob
import logging
import os
import re
import time
from functools import partial
from pathlib import Path
Expand DownExpand Up@@ -499,8 +500,6 @@ def _apply_digital_encoding(dataset, digital_encodings):
As modified
"""
if digital_encodings:
import re

# apply once, before saving to zarr, will stick around in cache
for encoding in digital_encodings:
logger.info(f"applying zarr digital-encoding: {encoding}")
Expand DownExpand Up@@ -753,6 +752,68 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset:
d = None # skims are not stored in shared memory, so we need to load them
do_not_save_zarr = False

potential_conflicts = network_los_preload.skims_info.get(skim_tag).skim_conflicts
if potential_conflicts:
# There are some conflicts in the skims, where both time-dependent
# and time-agnostic skim with the same name are present. We need
# to check we have sufficient ignore rules in place to correct this
# condition.

def _should_ignore(ignore, x):
if isinstance(ignore, str):
ignore = [ignore]
if ignore is not None:
for i in ignore:
if re.match(i, x):
return True
return False

ignore = state.settings.omx_ignore_patterns
problems = []
for time_agnostic, time_dependent in potential_conflicts.items():
# option 1, ignore all the time-dependent skims
# if this is fulfilled, we are ok and can proceed
if all((_should_ignore(ignore, i)) for i in time_dependent):
continue
# option 2, ignore the time-agnostic skim
# if this is fulfilled, we are ok and can proceed
if _should_ignore(ignore, time_agnostic):
continue
# otherwise, we have a problem. collect all the problems
# and raise an error at the end listing all of them
problems.append(time_agnostic)
if problems:
solution_1 = "__.+'\n - '^".join(problems)
solution_2 = "$'\n - '^".join(problems)
# we have a problem, raise an error
error_message = (
f"skims {problems} are present in both time-dependent and time-agnostic formats.\n"
"Please add ignore rules to the omx_ignore_patterns setting to resolve this issue.\n"
"To ignore the time dependent skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_1}__.+'\n"
"\n"
"To ignore the time agnostic skims, add the following to your settings file:\n"
"\n"
"omx_ignore_patterns:\n"
f" - '^{solution_2}$'\n"
"\n"
"You can also do some variation or combination of the two, as long as you resolve\n"
"the conflict(s). In addition, note that minor edits to model spec files may be\n"
"needed to accommodate these changes in how skim data is represented (e.g. changing\n"
"`odt_skims` to `od_skims`, or similar modifications wherever the offending variable\n"
"names are used). Alternatively, you can modify the skim data in the source files to\n"
"remove the naming conflicts, which is typically done upstream of ActivitySim in\n"
"whatever tool you are using to create the skims in the first place.\n"
"\n"
"See [https://activitysim.github.io/?q=skims] for more information.\n"
)
# write the error message to the log
logger.error(error_message)
# raise an error to stop the run, put the entire error message there also
raise ValueError(error_message)

if d is None:
time_periods = _dedupe_time_periods(network_los_preload)
if zarr_file:
Expand Down
65 changes: 58 additions & 7 deletions activitysim/core/skim_dict_factory.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
import os
import warnings
from abc import ABC
from collections import defaultdict

import numpy as np
import openmatrix as omx
Expand DownExpand Up@@ -86,6 +87,7 @@ def __init__(self, state, skim_tag, network_los):
self.offset_map_name = None
self.offset_map = None
self.omx_keys = None
self.skim_conflicts = None
self.base_keys = None
self.block_offsets = None

Expand DownExpand Up@@ -117,7 +119,12 @@ def load_skim_info(self, state, skim_tag):
logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}")

with omx.open_file(omx_file_path, mode="r") as omx_file:
# fixme call to omx_file.shape() failing in windows p3.5

# Check the shape of the skims. All skim files loaded within this
# loop need to have the same shape. For the first file, the
# shape is set to the omx_shape attribute, so we know what shape
# to expect. For subsequent files, we check that the shape is the
# same as the first file. If not, we raise an error.
if self.omx_shape is None:
self.omx_shape = tuple(
int(i) for i in omx_file.shape()
Expand All@@ -127,13 +134,24 @@ def load_skim_info(self, state, skim_tag):
int(i) for i in omx_file.shape()
), f"Mismatch shape {self.omx_shape} != {omx_file.shape()}"

# Check that all the matrix names are unique across all the
# omx files. This check is only looking at the name as stored in
# the file, and is not processing any time period transformations.
# If duplicate names are found, a warning is issued. This is not
# a fatal error, but it is inefficient and may be symptom of a
# deeper problem.
for skim_name in omx_file.listMatrices():
if skim_name in self.omx_manifest:
warnings.warn(
f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file.filename}"
)
self.omx_manifest[skim_name] = omx_file_path

# We load the offset map if it exists. This is expected to be
# a 1D array of integers that that gives ID values for each TAZ
# in the skims. ActivitySim expects there to be only one such
# mapping, although it can appear multiple times (e.g. once in
# each file).
for m in omx_file.listMappings():
if self.offset_map is None:
self.offset_map_name = m
Expand All@@ -146,21 +164,54 @@ def load_skim_info(self, state, skim_tag):
f"Multiple mappings in omx file: {self.offset_map_name} != {m}"
)

# - omx_keys dict maps skim key to omx_key
# DISTWALK: DISTWALK
# ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...
# Create the `omx_keys` mapping, which connects skim key to omx_key.
# The skim key is either a single string that names a skim that is not
# time-dependent, or a 2-tuple of strings which names a skim and a time
# period. The omx_key is the original name of the skim in the omx file.
# For non-time-dependent skims, the omx_key is the same as the skim key,
# e.g. DISTWALK: DISTWALK. For time-dependent skims, the omx_key is the
# skim key with the time period appended,
# e.g. ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM.
self.omx_keys = dict()
for skim_name in self.omx_manifest.keys():
key1, sep, key2 = skim_name.partition("__")

# - ignore composite tags not in dim3_tags_to_load
if dim3_tags_to_load and sep and key2 not in dim3_tags_to_load:
# If a skim is found that has a time period that is not one of
# the known named time periods, a warning is issued, and that
# skim is ignored. This is not a fatal error, but it may be a
# symptom of a deeper problem.
warnings.warn(f"skim '{key1}' has unknown time period '{key2}'")
continue

skim_key = (key1, key2) if sep else key1

self.omx_keys[skim_key] = skim_name

# Create a skim_conflicts set, which identifies any skims that have both
# time-dependent and time-agnostic versions. This condition in and of
# itself is not a fatal error, as it is possible to have both types of skims
# in the same data when using the legacy codebase. When using skim_dataset
# instead of skim_dictionary (the former is required when using sharrow) this
# condition is no longer allowed, although we can potentially recover if
# the user has specified instructions that certain skim variables are not
# to be loaded. The recovery option is checked later, in the skim_dataset
# module, as that is where the skim variables are actually loaded.
time_dependent_skims = defaultdict(set)
time_agnostic_skims = set()
for k, v in self.omx_keys.items():
if isinstance(k, tuple):
time_dependent_skims[k[0]].add(v)
else:
time_agnostic_skims.add(k)
self.skim_conflicts = {
k: v for k, v in time_dependent_skims.items() if k in time_agnostic_skims
}
if self.skim_conflicts:
msg = "some skims have both time-dependent and time-agnostic versions:"
for k in self.skim_conflicts:
msg += f"\n- {k}"
warnings.warn(msg)

# Count the number of skims in the omx file
self.num_skims = len(self.omx_keys)

# - key1_subkeys dict maps key1 to dict of subkeys with that key1
Expand Down
Loading
Loading