idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idmtools/__init__.py +27 -8
- idmtools/analysis/__init__.py +5 -0
- idmtools/analysis/add_analyzer.py +89 -0
- idmtools/analysis/analyze_manager.py +490 -0
- idmtools/analysis/csv_analyzer.py +103 -0
- idmtools/analysis/download_analyzer.py +96 -0
- idmtools/analysis/map_worker_entry.py +100 -0
- idmtools/analysis/platform_analysis_bootstrap.py +94 -0
- idmtools/analysis/platform_anaylsis.py +291 -0
- idmtools/analysis/tags_analyzer.py +93 -0
- idmtools/assets/__init__.py +9 -0
- idmtools/assets/asset.py +453 -0
- idmtools/assets/asset_collection.py +514 -0
- idmtools/assets/content_handlers.py +19 -0
- idmtools/assets/errors.py +23 -0
- idmtools/assets/file_list.py +191 -0
- idmtools/builders/__init__.py +11 -0
- idmtools/builders/arm_simulation_builder.py +152 -0
- idmtools/builders/csv_simulation_builder.py +76 -0
- idmtools/builders/simulation_builder.py +348 -0
- idmtools/builders/sweep_arm.py +109 -0
- idmtools/builders/yaml_simulation_builder.py +82 -0
- idmtools/config/__init__.py +7 -0
- idmtools/config/idm_config_parser.py +486 -0
- idmtools/core/__init__.py +10 -0
- idmtools/core/cache_enabled.py +114 -0
- idmtools/core/context.py +68 -0
- idmtools/core/docker_task.py +207 -0
- idmtools/core/enums.py +51 -0
- idmtools/core/exceptions.py +91 -0
- idmtools/core/experiment_factory.py +71 -0
- idmtools/core/id_file.py +70 -0
- idmtools/core/interfaces/__init__.py +5 -0
- idmtools/core/interfaces/entity_container.py +64 -0
- idmtools/core/interfaces/iassets_enabled.py +58 -0
- idmtools/core/interfaces/ientity.py +331 -0
- idmtools/core/interfaces/iitem.py +206 -0
- idmtools/core/interfaces/imetadata_operations.py +89 -0
- idmtools/core/interfaces/inamed_entity.py +17 -0
- idmtools/core/interfaces/irunnable_entity.py +159 -0
- idmtools/core/logging.py +387 -0
- idmtools/core/platform_factory.py +316 -0
- idmtools/core/system_information.py +104 -0
- idmtools/core/task_factory.py +145 -0
- idmtools/entities/__init__.py +10 -0
- idmtools/entities/command_line.py +229 -0
- idmtools/entities/command_task.py +155 -0
- idmtools/entities/experiment.py +787 -0
- idmtools/entities/generic_workitem.py +43 -0
- idmtools/entities/ianalyzer.py +163 -0
- idmtools/entities/iplatform.py +1106 -0
- idmtools/entities/iplatform_default.py +39 -0
- idmtools/entities/iplatform_ops/__init__.py +5 -0
- idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
- idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
- idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
- idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
- idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
- idmtools/entities/iplatform_ops/utils.py +185 -0
- idmtools/entities/itask.py +316 -0
- idmtools/entities/iworkflow_item.py +167 -0
- idmtools/entities/platform_requirements.py +20 -0
- idmtools/entities/relation_type.py +14 -0
- idmtools/entities/simulation.py +255 -0
- idmtools/entities/suite.py +188 -0
- idmtools/entities/task_proxy.py +37 -0
- idmtools/entities/templated_simulation.py +325 -0
- idmtools/frozen/frozen_dict.py +71 -0
- idmtools/frozen/frozen_list.py +66 -0
- idmtools/frozen/frozen_set.py +86 -0
- idmtools/frozen/frozen_tuple.py +18 -0
- idmtools/frozen/frozen_utils.py +179 -0
- idmtools/frozen/ifrozen.py +66 -0
- idmtools/plugins/__init__.py +5 -0
- idmtools/plugins/git_commit.py +117 -0
- idmtools/registry/__init__.py +4 -0
- idmtools/registry/experiment_specification.py +105 -0
- idmtools/registry/functions.py +28 -0
- idmtools/registry/hook_specs.py +132 -0
- idmtools/registry/master_plugin_registry.py +51 -0
- idmtools/registry/platform_specification.py +138 -0
- idmtools/registry/plugin_specification.py +129 -0
- idmtools/registry/task_specification.py +104 -0
- idmtools/registry/utils.py +119 -0
- idmtools/services/__init__.py +5 -0
- idmtools/services/ipersistance_service.py +135 -0
- idmtools/services/platforms.py +13 -0
- idmtools/utils/__init__.py +5 -0
- idmtools/utils/caller.py +24 -0
- idmtools/utils/collections.py +246 -0
- idmtools/utils/command_line.py +45 -0
- idmtools/utils/decorators.py +300 -0
- idmtools/utils/display/__init__.py +22 -0
- idmtools/utils/display/displays.py +181 -0
- idmtools/utils/display/settings.py +25 -0
- idmtools/utils/dropbox_location.py +30 -0
- idmtools/utils/entities.py +127 -0
- idmtools/utils/file.py +72 -0
- idmtools/utils/file_parser.py +151 -0
- idmtools/utils/filter_simulations.py +182 -0
- idmtools/utils/filters/__init__.py +5 -0
- idmtools/utils/filters/asset_filters.py +88 -0
- idmtools/utils/general.py +286 -0
- idmtools/utils/gitrepo.py +336 -0
- idmtools/utils/hashing.py +239 -0
- idmtools/utils/info.py +124 -0
- idmtools/utils/json.py +82 -0
- idmtools/utils/language.py +107 -0
- idmtools/utils/local_os.py +40 -0
- idmtools/utils/time.py +22 -0
- idmtools-0.0.3.dist-info/METADATA +120 -0
- idmtools-0.0.3.dist-info/RECORD +116 -0
- idmtools-0.0.3.dist-info/entry_points.txt +9 -0
- idmtools-0.0.3.dist-info/licenses/LICENSE.TXT +3 -0
- idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/WHEEL +0 -0
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filtering utility.
|
|
3
|
+
|
|
4
|
+
Copyright 2025, Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
from idmtools.core import ItemType, EntityStatus
|
|
7
|
+
from idmtools.core.interfaces.ientity import IEntity
|
|
8
|
+
from idmtools.entities.experiment import Experiment
|
|
9
|
+
from idmtools.entities.iplatform import IPlatform
|
|
10
|
+
from idmtools.utils.general import parse_value_tags
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FilterItem:
|
|
14
|
+
"""
|
|
15
|
+
FilterItem provides a utility to filter items on a platform.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def filter_item(platform: IPlatform, item: IEntity, tags=None, status: EntityStatus = None,
|
|
20
|
+
entity_type: bool = False, max_simulations: int = None, skip_sims=None, **kwargs):
|
|
21
|
+
"""
|
|
22
|
+
Filter simulations from an Experiment or Suite using tag and status criteria.
|
|
23
|
+
|
|
24
|
+
By default, this filters simulations that have a status of `EntityStatus.SUCCEEDED`.
|
|
25
|
+
Additional filtering can be applied by specifying tag values or tag-based conditions.
|
|
26
|
+
|
|
27
|
+
This method supports:
|
|
28
|
+
- Skipping specific simulations by ID
|
|
29
|
+
- Filtering based on simulation status (e.g., FAILED, SUCCEEDED)
|
|
30
|
+
- Tag-based filtering (both exact match and conditional/lambda-based)
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
>>> filter_item(platform, experiment, status=EntityStatus.FAILED)
|
|
34
|
+
>>> filter_item(platform, experiment, tags={"Run_Number": "2"})
|
|
35
|
+
>>> filter_item(platform, experiment, tags={"Run_Number": lambda v: 2 <= v <= 10})
|
|
36
|
+
>>> filter_item(platform, experiment, tags={"Coverage": 0.8}, status=EntityStatus.SUCCEEDED)
|
|
37
|
+
>>> filter_item(platform, experiment, tags={"Coverage": 0.8}, max_simulations=10)
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
platform (IPlatform): The platform instance to query simulations from.
|
|
41
|
+
item (IEntity): An Experiment or Suite to filter simulations from.
|
|
42
|
+
tags (dict): A dictionary of tag key-value pairs to filter by for a simulation. Values may be:
|
|
43
|
+
* A fixed value (e.g., {"Run_Number": 2})
|
|
44
|
+
* A lambda or callable function for conditional logic
|
|
45
|
+
(e.g., {"Run_Number": lambda v: 2 <= v <= 10})
|
|
46
|
+
status (EntityStatus, Optional): The experiment's status.
|
|
47
|
+
entity_type (bool, optional): If True, return simulation entities instead of just their IDs.
|
|
48
|
+
skip_sims (list, optional): A list of simulation IDs (as strings) to exclude from the results.
|
|
49
|
+
max_simulations (int, optional): Maximum number of simulations to return. Returns all if not set.
|
|
50
|
+
**kwargs: Extra args.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Union[List[Union[str, Simulation]], Dict[str, List[Union[str, Simulation]]]]:
|
|
54
|
+
If the item is an `Experiment`, returns a list of simulation IDs or simulation entities
|
|
55
|
+
(if `entity_type=True`).
|
|
56
|
+
|
|
57
|
+
If the item is a `Suite`, returns list ofdictionary where each key is an experiment ID and
|
|
58
|
+
each value is a list of simulation IDs or simulation entities (depending on the `entity_type` flag).
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
def match_tags(sim: IEntity, tags=None):
|
|
62
|
+
"""
|
|
63
|
+
Check if simulation match tags.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
sim: simulation
|
|
67
|
+
tags: search tags
|
|
68
|
+
|
|
69
|
+
Returns: bool True/False
|
|
70
|
+
"""
|
|
71
|
+
# If no tags are provided, treat it as an empty filter (match all)
|
|
72
|
+
if tags is None:
|
|
73
|
+
return True
|
|
74
|
+
# Normalize simulation tag values and wrap them with TagValue for safe comparisons
|
|
75
|
+
# (e.g., allows "5" == 5 and supports operators like >, <, == in tag filters)
|
|
76
|
+
sim_tags = parse_value_tags(sim.tags, wrap_with_tagvalue=True)
|
|
77
|
+
|
|
78
|
+
# Iterate over each tag filter condition
|
|
79
|
+
for k, v in tags.items():
|
|
80
|
+
sim_val = sim_tags.get(k)
|
|
81
|
+
# If the simulation does not have the tag, skip it
|
|
82
|
+
if sim_val is None:
|
|
83
|
+
return False
|
|
84
|
+
# If the filter value is a callable (e.g., lambda), evaluate the condition
|
|
85
|
+
if callable(v):
|
|
86
|
+
if not v(sim_val):
|
|
87
|
+
return False
|
|
88
|
+
# Otherwise, do a direct comparison between the simulation tag and expected value
|
|
89
|
+
elif sim_val != v:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
if item.item_type not in [ItemType.EXPERIMENT, ItemType.SUITE]:
|
|
95
|
+
raise ValueError("This method only supports Experiment and Suite types!")
|
|
96
|
+
|
|
97
|
+
if skip_sims is None:
|
|
98
|
+
skip_sims = []
|
|
99
|
+
# ------------------------------------------------------------------ #
|
|
100
|
+
# Base case ─ Experiment
|
|
101
|
+
# ------------------------------------------------------------------ #
|
|
102
|
+
if isinstance(item, Experiment):
|
|
103
|
+
potential_sims = item.get_simulations()
|
|
104
|
+
|
|
105
|
+
# filter by status
|
|
106
|
+
sims_status_filtered = [sim for sim in potential_sims if sim.status == status] if status else potential_sims
|
|
107
|
+
|
|
108
|
+
# filter tags
|
|
109
|
+
sims_tags_filtered = [sim for sim in sims_status_filtered if match_tags(sim, tags)]
|
|
110
|
+
|
|
111
|
+
# filter sims
|
|
112
|
+
sims_id_filtered = [sim for sim in sims_tags_filtered if str(sim.uid) not in skip_sims]
|
|
113
|
+
|
|
114
|
+
# consider max_simulations for return
|
|
115
|
+
sims_final = sims_id_filtered[0:max_simulations if max_simulations else len(sims_id_filtered)]
|
|
116
|
+
|
|
117
|
+
if entity_type:
|
|
118
|
+
return sims_final
|
|
119
|
+
else:
|
|
120
|
+
return [s.id for s in sims_final]
|
|
121
|
+
|
|
122
|
+
# Suite case:
|
|
123
|
+
experiments = item.get_experiments()
|
|
124
|
+
result = {}
|
|
125
|
+
|
|
126
|
+
for exp in experiments:
|
|
127
|
+
filtered = FilterItem.filter_item(
|
|
128
|
+
platform,
|
|
129
|
+
item=exp,
|
|
130
|
+
tags=tags,
|
|
131
|
+
status=status,
|
|
132
|
+
entity_type=entity_type,
|
|
133
|
+
max_simulations=max_simulations,
|
|
134
|
+
skip_sims=skip_sims,
|
|
135
|
+
**kwargs,
|
|
136
|
+
)
|
|
137
|
+
result[exp.id] = filtered
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def filter_item_by_id(cls, platform: IPlatform, item_id: str, item_type: ItemType = ItemType.EXPERIMENT,
|
|
142
|
+
tags=None, status=None, entity_type=False, skip_sims=None, max_simulations: int = None,
|
|
143
|
+
**kwargs):
|
|
144
|
+
"""
|
|
145
|
+
Retrieve and filter simulations from an Experiment or Suite by item ID.
|
|
146
|
+
|
|
147
|
+
This method looks up the specified item (Experiment or Suite) by ID on the given platform,
|
|
148
|
+
then filters its simulations using the class's `filter_item()` method.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
platform (IPlatform): The platform instance used to fetch the item.
|
|
152
|
+
item_id (str): The unique identifier of the Experiment or Suite.
|
|
153
|
+
item_type (ItemType, optional): The type of item (Experiment or Suite). Defaults to Experiment.
|
|
154
|
+
tags (dict, optional): A simulation's tags to filter by.
|
|
155
|
+
status (EntityStatus, Optional): The experiment's status.
|
|
156
|
+
entity_type (bool, optional): If True, return simulation entities instead of just their IDs.
|
|
157
|
+
skip_sims (List[str], optional): List of simulation IDs to skip during filtering. Defaults to an empty list.
|
|
158
|
+
max_simulations (int, optional): Maximum number of simulations to return. Defaults to None (no limit).
|
|
159
|
+
**kwargs: Additional keyword arguments passed to `filter_item()`.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Union[List[Union[str, Simulation]], Dict[str, List[Union[str, Simulation]]]]:
|
|
163
|
+
If the item is an `Experiment`, returns a list of simulation IDs or simulation entities
|
|
164
|
+
(if `entity_type=True`).
|
|
165
|
+
|
|
166
|
+
If the item is a `Suite`, returns a dictionary where each key is an experiment ID and
|
|
167
|
+
each value is a list of simulation IDs or simulation entities (depending on the `entity_type` flag).
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If the provided `item_type` is not Experiment or Suite.
|
|
171
|
+
"""
|
|
172
|
+
if skip_sims is None:
|
|
173
|
+
skip_sims = []
|
|
174
|
+
if item_type not in [ItemType.EXPERIMENT, ItemType.SUITE]:
|
|
175
|
+
raise ValueError("This method only supports Experiment and Suite types!")
|
|
176
|
+
|
|
177
|
+
# retrieve item by id and type
|
|
178
|
+
item = platform.get_item(item_id, item_type, raw=False, force=True)
|
|
179
|
+
|
|
180
|
+
# filter simulations
|
|
181
|
+
return cls.filter_item(platform, item=item, tags=tags, status=status, entity_type=entity_type,
|
|
182
|
+
skip_sims=skip_sims, max_simulations=max_simulations, **kwargs)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains all the default filters for the assets.
|
|
3
|
+
|
|
4
|
+
A filter function needs to take only one argument: an asset. It returns True/False indicating whether to add or filter
|
|
5
|
+
out the asset.
|
|
6
|
+
|
|
7
|
+
You can notice functions taking more than only an asset.
|
|
8
|
+
To use those functions, use must create a partial before adding it to a filters list.
|
|
9
|
+
For example::
|
|
10
|
+
|
|
11
|
+
python
|
|
12
|
+
fname = partial(file_name_is, filenames=["a.txt", "b.txt"])
|
|
13
|
+
AssetCollection.from_directory(... filters=[fname], ...)
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import typing
|
|
17
|
+
|
|
18
|
+
if typing.TYPE_CHECKING:
|
|
19
|
+
from typing import List
|
|
20
|
+
from idmtools.core import TAsset
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def default_asset_file_filter(asset: 'TAsset') -> 'bool':
|
|
24
|
+
"""
|
|
25
|
+
Default filter to leave out Python caching.
|
|
26
|
+
|
|
27
|
+
This filter is used in the creation of
|
|
28
|
+
:class:`~idmtools.assets.asset_collection.AssetCollection`, regardless of user filters.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if no files match default patterns of "__py_cache__" and ".pyc"
|
|
32
|
+
"""
|
|
33
|
+
patterns = [
|
|
34
|
+
"__pycache__",
|
|
35
|
+
".pyc"
|
|
36
|
+
]
|
|
37
|
+
return not any([p in asset.absolute_path for p in patterns])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def file_name_is(asset: 'TAsset', filenames: 'List[str]') -> 'bool':
|
|
41
|
+
"""
|
|
42
|
+
Restrict filtering to assets with the indicated filenames.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
asset: The asset to filter.
|
|
46
|
+
filenames: List of filenames to filter on.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if asset.filename in filenames
|
|
50
|
+
"""
|
|
51
|
+
return asset.filename in filenames
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def file_extension_is(asset: 'TAsset', extensions: 'List[str]') -> 'bool':
|
|
55
|
+
"""
|
|
56
|
+
Restrict filtering to assets with the indicated filetypes.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
asset: The asset to filter.
|
|
60
|
+
extensions: List of extensions to filter on.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if extension in extensions
|
|
64
|
+
"""
|
|
65
|
+
return asset.extension in extensions
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def asset_in_directory(asset: 'TAsset', directories: 'List[str]', base_path: str = None) -> 'bool':
|
|
69
|
+
"""
|
|
70
|
+
Restrict filtering to assets within a given directory.
|
|
71
|
+
|
|
72
|
+
This filter is not strict and simply checks if the directory portion is present in the assets absolute path.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
asset: The asset to filter.
|
|
76
|
+
directories: List of directory portions to include.
|
|
77
|
+
base_path: base_path
|
|
78
|
+
"""
|
|
79
|
+
if base_path is None:
|
|
80
|
+
base_path = os.getcwd()
|
|
81
|
+
norm_base_path = os.path.abspath(base_path)
|
|
82
|
+
norm_dirs = [f"{os.sep}{d}{os.sep}" for d in directories]
|
|
83
|
+
norm_asset_absolute_path = os.path.abspath(asset.absolute_path)
|
|
84
|
+
norm_root = norm_asset_absolute_path.replace(norm_base_path, "")
|
|
85
|
+
|
|
86
|
+
if not norm_asset_absolute_path.startswith(norm_base_path):
|
|
87
|
+
return False
|
|
88
|
+
return any([d in norm_root for d in norm_dirs])
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tag Parsing and Normalization Utilities.
|
|
3
|
+
|
|
4
|
+
This module provides helper functions and classes for converting string-based metadata tags
|
|
5
|
+
into native Python types (e.g., bool, int, float) to enable accurate filtering and comparison.
|
|
6
|
+
|
|
7
|
+
It includes:
|
|
8
|
+
- JSON serialization/deserialization of tags
|
|
9
|
+
- Conversion of sets to lists for JSON compatibility
|
|
10
|
+
- Coercion logic via TagValue for safe and flexible comparisons
|
|
11
|
+
|
|
12
|
+
Typical use cases include tag-based filtering of simulations or experiments during analysis.
|
|
13
|
+
|
|
14
|
+
Typical Use Case:
|
|
15
|
+
-----------------
|
|
16
|
+
Used during filtering of simulations or experiments in analysis pipelines where user-defined
|
|
17
|
+
tags are compared with numeric thresholds or exact matches.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
--------
|
|
21
|
+
sim.tags={"Run_Number": lambda v: 4 <= v <= 10, "Coverage": "0.8"}
|
|
22
|
+
parsed = parse_value_tags(sim.tags, wrap_with_tagvalue=True)
|
|
23
|
+
assert parsed["Run_Number"] >=4 and <=10
|
|
24
|
+
assert parsed["Coverage"] == 0.8
|
|
25
|
+
|
|
26
|
+
Copyright 2025, Gates Foundation. All rights reserved.
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
from typing import Dict
|
|
30
|
+
|
|
31
|
+
from idmtools.core.interfaces.ientity import IEntity
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_item_tags(item: IEntity):
|
|
35
|
+
"""
|
|
36
|
+
Normalize and update an entity's tags in place.
|
|
37
|
+
|
|
38
|
+
This function parses the given item's tags using `parse_value_tags` and updates
|
|
39
|
+
the `item.tags` dictionary with the normalized values.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
item: An entity object that contains a `.tags` dictionary.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The same item, with its `.tags` dictionary updated in-place.
|
|
46
|
+
"""
|
|
47
|
+
if item.tags is None:
|
|
48
|
+
return item
|
|
49
|
+
else:
|
|
50
|
+
item.tags = parse_value_tags(item.tags)
|
|
51
|
+
return item
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_value_tags(tags: Dict, wrap_with_tagvalue: bool = False) -> Dict[str, any]:
|
|
55
|
+
"""
|
|
56
|
+
Parse and normalize a tag dictionary into native Python types.
|
|
57
|
+
|
|
58
|
+
Converts string tag values such as:
|
|
59
|
+
- "true"/"false" → bool
|
|
60
|
+
- "5" → int
|
|
61
|
+
- "0.8" → float
|
|
62
|
+
- sets → lists
|
|
63
|
+
|
|
64
|
+
Optionally wraps values using `TagValue` for comparison safety.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
tags (dict): The dictionary of raw tag values to parse.
|
|
68
|
+
wrap_with_tagvalue (bool): If True, wraps each value in a TagValue object
|
|
69
|
+
for smart comparison support.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
dict: A dictionary with normalized or wrapped tag values.
|
|
73
|
+
"""
|
|
74
|
+
# convert tags value as set to list first
|
|
75
|
+
tags = {k: list(v) if isinstance(v, set) else v for k, v in tags.items()}
|
|
76
|
+
tags_json = json.dumps(tags, cls=SetEncoder)
|
|
77
|
+
# Then: parse back into a dict with properly typed values
|
|
78
|
+
converted_tags = json.loads(tags_json, cls=CustomDecoder)
|
|
79
|
+
# Update result.tags in-place
|
|
80
|
+
for k, v in converted_tags.items():
|
|
81
|
+
converted = v
|
|
82
|
+
if wrap_with_tagvalue:
|
|
83
|
+
converted = TagValue(v)
|
|
84
|
+
tags[k] = converted
|
|
85
|
+
return tags
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SetEncoder(json.JSONEncoder):
|
|
89
|
+
"""
|
|
90
|
+
Custom JSON encoder that converts Python sets into lists to ensure compatibility with JSON serialization.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def default(self, obj):
|
|
94
|
+
"""
|
|
95
|
+
Override default encoding behavior.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
obj: The object being encoded.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
JSON-compatible representation of the object.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(obj, set):
|
|
104
|
+
return list(obj)
|
|
105
|
+
return super().default(obj)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CustomDecoder(json.JSONDecoder):
|
|
109
|
+
"""
|
|
110
|
+
Custom JSON decoder that converts string values into appropriate Python types (bool, int, float, None).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, *args, **kwargs): # noqa D415
|
|
114
|
+
# Optionally override object_hook here
|
|
115
|
+
super().__init__(object_hook=self.custom_object_hook, *args, **kwargs)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def denormalize_tag_value(val):
|
|
119
|
+
"""
|
|
120
|
+
Convert a raw string tag value into a Python-native type.
|
|
121
|
+
|
|
122
|
+
Supported conversions:
|
|
123
|
+
- "true"/"false" → bool
|
|
124
|
+
- "null"/"none" → None
|
|
125
|
+
- "5", "0.8" → int, float
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
val (Any): The value to convert.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The normalized Python value.
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(val, str):
|
|
134
|
+
return val # Already a native type
|
|
135
|
+
|
|
136
|
+
val_lower = val.strip().lower()
|
|
137
|
+
if val_lower == "true":
|
|
138
|
+
return True
|
|
139
|
+
elif val_lower == "false":
|
|
140
|
+
return False
|
|
141
|
+
elif val_lower in ("null", "none"):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Try integer/float
|
|
145
|
+
try:
|
|
146
|
+
if "." in val:
|
|
147
|
+
return float(val)
|
|
148
|
+
return int(val)
|
|
149
|
+
except ValueError:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
# Return as-is if it's not a simple number/bool/null
|
|
153
|
+
return val
|
|
154
|
+
|
|
155
|
+
def custom_object_hook(self, obj: Dict):
|
|
156
|
+
"""
|
|
157
|
+
Apply normalization to each value in a decoded JSON object.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
obj (dict): Dictionary of tag values.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
dict: Normalized tag dictionary.
|
|
164
|
+
"""
|
|
165
|
+
# Modify dicts as they're loaded
|
|
166
|
+
new_obj = {}
|
|
167
|
+
for k, v in obj.items():
|
|
168
|
+
new_obj[k] = self.denormalize_tag_value(v)
|
|
169
|
+
return new_obj
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TagValue:
|
|
173
|
+
"""
|
|
174
|
+
Wrapper for a tag value that supports smart comparisons.
|
|
175
|
+
|
|
176
|
+
Automatically converts strings like "5" or "0.8" to int/float
|
|
177
|
+
and enables comparison against numbers or other tag values.
|
|
178
|
+
|
|
179
|
+
Useful when users perform tag-based filtering with operators
|
|
180
|
+
like >, <, ==, etc., and tag values may be strings.
|
|
181
|
+
|
|
182
|
+
Attributes:
|
|
183
|
+
raw (Any): The original tag value before coercion.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, raw): # noqa D107
|
|
187
|
+
self.raw = raw
|
|
188
|
+
|
|
189
|
+
def _coerce(self, other):
|
|
190
|
+
"""
|
|
191
|
+
Convert both self.raw and other to comparable types using the same logic as CustomDecoder.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
other (Any): Value to compare to.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tuple[Any, Any]: Coerced values ready for comparison.
|
|
198
|
+
"""
|
|
199
|
+
convert = CustomDecoder.denormalize_tag_value
|
|
200
|
+
return convert(self.raw), convert(other)
|
|
201
|
+
|
|
202
|
+
def __eq__(self, other): return self._coerce(other)[0] == self._coerce(other)[1] # noqa E704
|
|
203
|
+
|
|
204
|
+
def __lt__(self, other): return self._coerce(other)[0] < self._coerce(other)[1] # noqa E704
|
|
205
|
+
|
|
206
|
+
def __le__(self, other): return self._coerce(other)[0] <= self._coerce(other)[1] # noqa E704
|
|
207
|
+
|
|
208
|
+
def __gt__(self, other): return self._coerce(other)[0] > self._coerce(other)[1] # noqa E704
|
|
209
|
+
|
|
210
|
+
def __ge__(self, other): return self._coerce(other)[0] >= self._coerce(other)[1] # noqa E704
|
|
211
|
+
|
|
212
|
+
def __repr__(self): return repr(self.raw) # noqa E704
|
|
213
|
+
|
|
214
|
+
def __str__(self): return str(self.raw) # noqa E704
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class FilterSafeItem:
|
|
218
|
+
"""
|
|
219
|
+
A lightweight wrapper around an entity (e.g., Simulation or Experiment) to enable safe tag-based filtering during multiprocessing operations (e.g., within analyzers).
|
|
220
|
+
|
|
221
|
+
This wrapper:
|
|
222
|
+
- Normalizes and wraps tag values via `TagValue` to support flexible comparisons (e.g., >, ==).
|
|
223
|
+
- Supports safe pickling/unpickling by stripping unpickleable fields like `_platform`.
|
|
224
|
+
- Delegates attribute access to the original wrapped item via `__getattr__` (if implemented).
|
|
225
|
+
|
|
226
|
+
Typical usage:
|
|
227
|
+
safe_item = FilterSafeItem(simulation)
|
|
228
|
+
tags = safe_item.tags
|
|
229
|
+
# Now tags["Run_Number"] supports TagValue comparison logic
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
_item (IEntity): The original entity being wrapped.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, item):
|
|
236
|
+
"""
|
|
237
|
+
Initialize the filter-safe wrapper with a given entity.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
item (IEntity): The original simulation or experiment to wrap.
|
|
241
|
+
"""
|
|
242
|
+
self._item = item
|
|
243
|
+
|
|
244
|
+
def __getstate__(self):
|
|
245
|
+
"""
|
|
246
|
+
Prepare the object for pickling by removing unpickleable platform references.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
dict: The serializable state dictionary.
|
|
250
|
+
"""
|
|
251
|
+
state = self.__dict__.copy()
|
|
252
|
+
for key in ['_platform', '_platform_object', '_parent']:
|
|
253
|
+
state.pop(key, None)
|
|
254
|
+
return state
|
|
255
|
+
|
|
256
|
+
def __getattr__(self, attr):
|
|
257
|
+
"""
|
|
258
|
+
Delegate access to attributes not defined on the wrapper to the wrapped item.
|
|
259
|
+
|
|
260
|
+
This makes FilterSafeItem behave like the original simulation object for all
|
|
261
|
+
standard attributes (e.g., `id`, `status`, `experiment_id`, etc.).
|
|
262
|
+
This function makes simulation._item.id = simulation.id the same in filter function.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
The attribute value from the wrapped entity.
|
|
266
|
+
"""
|
|
267
|
+
return getattr(self._item, attr)
|
|
268
|
+
|
|
269
|
+
def __setstate__(self, state):
|
|
270
|
+
"""
|
|
271
|
+
Restore object state after unpickling.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
state (dict): The state dictionary.
|
|
275
|
+
"""
|
|
276
|
+
self.__dict__.update(state)
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def tags(self):
|
|
280
|
+
"""
|
|
281
|
+
Get normalized tags from the wrapped item, with each value wrapped in TagValue for type-safe comparison.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dict[str, any]: Dictionary of tags.
|
|
285
|
+
"""
|
|
286
|
+
return parse_value_tags(self._item.tags)
|