simstack 0.2.0__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.
- simstack/__init__.py +0 -0
- simstack/_version.py +34 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +447 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +167 -0
- simstack/core/data.py +139 -0
- simstack/core/definitions.py +21 -0
- simstack/core/engine.py +116 -0
- simstack/core/find_simstack_modules.py +43 -0
- simstack/core/hash.py +177 -0
- simstack/core/model_table.py +199 -0
- simstack/core/node.py +911 -0
- simstack/core/node_runner.py +254 -0
- simstack/core/node_table.py +380 -0
- simstack/core/recompute_artifacts.py +120 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resources.py +101 -0
- simstack/core/run_node.py +69 -0
- simstack/core/runner.py +435 -0
- simstack/core/simstack_result.py +41 -0
- simstack/core/submit_node.py +143 -0
- simstack/core/task_id.py +22 -0
- simstack/data/__init__.py +0 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/get_file.py +10 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/models/__init__.py +37 -0
- simstack/models/array_list.py +48 -0
- simstack/models/array_storage.py +54 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_types.py +208 -0
- simstack/models/charts_artifact.py +411 -0
- simstack/models/dataset.py +432 -0
- simstack/models/dataset_metadata.py +303 -0
- simstack/models/file_instance.py +77 -0
- simstack/models/file_list.py +321 -0
- simstack/models/files.py +229 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +40 -0
- simstack/models/node_registry.py +105 -0
- simstack/models/pandas_model.py +210 -0
- simstack/models/parameters.py +388 -0
- simstack/models/pickle_models.py +311 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +32 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +273 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/db.py +457 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/file_hashing.py +68 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +26 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +93 -0
- simstack/util/import_module.py +65 -0
- simstack/util/importer.py +163 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +827 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/path_manager.py +234 -0
- simstack/util/project_root_finder.py +36 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +162 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/ui_tools.py +213 -0
- simstack-0.2.0.dist-info/METADATA +361 -0
- simstack-0.2.0.dist-info/RECORD +91 -0
- simstack-0.2.0.dist-info/WHEEL +5 -0
- simstack-0.2.0.dist-info/entry_points.txt +10 -0
- simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- simstack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Callable, TypeVar, Generic, Dict, Any, AsyncGenerator, Union
|
|
4
|
+
|
|
5
|
+
from odmantic import Model, Field, ObjectId, Reference
|
|
6
|
+
|
|
7
|
+
from simstack.core.asnyc_helper import async_helper
|
|
8
|
+
from simstack.core.context import context
|
|
9
|
+
from simstack.core.node import node
|
|
10
|
+
from simstack.models import (
|
|
11
|
+
simstack_model,
|
|
12
|
+
ModelMapping,
|
|
13
|
+
BinaryOperationInput,
|
|
14
|
+
FloatData,
|
|
15
|
+
)
|
|
16
|
+
from simstack.util.async_zip_utils import async_zip
|
|
17
|
+
from simstack.util.importer import import_function, import_class
|
|
18
|
+
from simstack.util.make_table import make_table_entries
|
|
19
|
+
|
|
20
|
+
# Define your prefix
|
|
21
|
+
PREFIX = "[ParallelExecutor]"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@node
|
|
25
|
+
def multiple_executor_adder(
|
|
26
|
+
arg1: BinaryOperationInput, arg2: BinaryOperationInput
|
|
27
|
+
) -> BinaryOperationInput:
|
|
28
|
+
return BinaryOperationInput(arg1=arg1.arg1 + arg2.arg1, arg2=arg1.arg2 + arg2.arg2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PrefixFilter(logging.Filter):
|
|
32
|
+
def filter(self, record):
|
|
33
|
+
record.msg = f"{PREFIX} {record.msg}"
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Set up the logger
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
logger.addFilter(PrefixFilter())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@async_helper
|
|
43
|
+
async def get_mappings(func: Callable[[Model], Model]):
|
|
44
|
+
"""
|
|
45
|
+
Decorator to map a function to a model.
|
|
46
|
+
"""
|
|
47
|
+
node_model = await context.db.load_node_model_by_name(func.__name__)
|
|
48
|
+
if node_model is None:
|
|
49
|
+
raise ValueError(f"Node model {func.__name__} not found in database.")
|
|
50
|
+
|
|
51
|
+
# Get return type annotation from function
|
|
52
|
+
return_type = func.__annotations__.get("return")
|
|
53
|
+
if return_type is None:
|
|
54
|
+
raise ValueError(f"Function {func.__name__} must have return type annotation")
|
|
55
|
+
|
|
56
|
+
result_model = await context.db.find_one(
|
|
57
|
+
ModelMapping, ModelMapping.name == return_type.__name__
|
|
58
|
+
)
|
|
59
|
+
if result_model is None:
|
|
60
|
+
raise ValueError(f"Result model {return_type.__name__} not found in database.")
|
|
61
|
+
|
|
62
|
+
return node_model.function_mapping, node_model.input_mappings, result_model.mapping
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
T = TypeVar("T", bound=Model)
|
|
66
|
+
R = TypeVar("R", bound=Model)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _prefix_dict_keys(original_dict, prefix):
|
|
70
|
+
"""Helper method to prefix dictionary keys."""
|
|
71
|
+
return {f"{prefix}{key}": value for key, value in original_dict.items()}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _get_formatted_column_defs(mapping, prefix):
|
|
75
|
+
"""Get column definitions for a model with proper field prefixes."""
|
|
76
|
+
model_class = await import_class(mapping)
|
|
77
|
+
column_defs = model_class.make_column_defs()
|
|
78
|
+
|
|
79
|
+
# Add prefix to field names
|
|
80
|
+
for col in column_defs:
|
|
81
|
+
if "field" in col:
|
|
82
|
+
col["field"] = f"{prefix}{col['field']}"
|
|
83
|
+
|
|
84
|
+
# Filter out id fields
|
|
85
|
+
return [col for col in column_defs if col.get("field") != f"{prefix}id"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@simstack_model
|
|
89
|
+
class MultipleExecutorInput(Model, Generic[T]):
|
|
90
|
+
"""
|
|
91
|
+
Represents a MultipleExecutor which handles multiple calculations in parallel and aggregates the results.
|
|
92
|
+
# TODO store the inputs as ids not in memory
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
function_mapping: str
|
|
96
|
+
input_mappings: List[str]
|
|
97
|
+
result_mapping: str
|
|
98
|
+
timeout: int = 600
|
|
99
|
+
inputs_data: List[List[ObjectId]] = Field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_function(cls, func: Callable[[T], R]):
|
|
103
|
+
function_mapping, input_mappings, result_mapping = get_mappings(func)
|
|
104
|
+
print(
|
|
105
|
+
f"Function mapping: {function_mapping}, Input mappings: {input_mappings}, Result mapping: {result_mapping}"
|
|
106
|
+
)
|
|
107
|
+
return cls(
|
|
108
|
+
function_mapping=function_mapping,
|
|
109
|
+
input_mappings=input_mappings,
|
|
110
|
+
result_mapping=result_mapping,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def input_generator(self) -> AsyncGenerator[List[Model], None]:
|
|
114
|
+
"""Deserialize the input models"""
|
|
115
|
+
input_model_classes = [
|
|
116
|
+
await import_class(input_mapping) for input_mapping in self.input_mappings
|
|
117
|
+
]
|
|
118
|
+
for input_item in self.inputs_data:
|
|
119
|
+
input_models = [
|
|
120
|
+
await context.db.find_one(
|
|
121
|
+
input_model_class, input_model_class.id == model_id
|
|
122
|
+
)
|
|
123
|
+
for input_model_class, model_id in zip(input_model_classes, input_item)
|
|
124
|
+
]
|
|
125
|
+
# if one models is None: fail
|
|
126
|
+
if any(model is None for model in input_models):
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"One or more input models not found in database for input IDs: {input_item}"
|
|
129
|
+
)
|
|
130
|
+
yield input_models
|
|
131
|
+
|
|
132
|
+
async def append(self, input_models: Union[T, List[T]], with_save=True):
|
|
133
|
+
"""Add the inputs for a single function call"""
|
|
134
|
+
if isinstance(input_models, Model):
|
|
135
|
+
input_models = [input_models]
|
|
136
|
+
if with_save:
|
|
137
|
+
for input_model in input_models:
|
|
138
|
+
await context.db.save(input_model)
|
|
139
|
+
input_ids = [input_model.id for input_model in input_models]
|
|
140
|
+
self.inputs_data.append(input_ids)
|
|
141
|
+
|
|
142
|
+
async def execute(self, **kwargs) -> "MultipleExecutorOutput":
|
|
143
|
+
task_id = kwargs.get("task_id", "NA")
|
|
144
|
+
global PREFIX
|
|
145
|
+
if task_id != "NA":
|
|
146
|
+
PREFIX = f"[ParallelExecutor task_id: {task_id}]"
|
|
147
|
+
|
|
148
|
+
# Prepare all jobs to run concurrently
|
|
149
|
+
tasks = []
|
|
150
|
+
result_ids = [None] * len(self.inputs_data)
|
|
151
|
+
kwargs["parent_id"] = task_id
|
|
152
|
+
try:
|
|
153
|
+
func = await import_function(self.function_mapping, task_id)
|
|
154
|
+
i = 0
|
|
155
|
+
async for task_input in self.input_generator():
|
|
156
|
+
# Create an async task that calls our async wrapper
|
|
157
|
+
task = asyncio.create_task(func(*task_input, **kwargs))
|
|
158
|
+
# Store index for later reference
|
|
159
|
+
task.index = i
|
|
160
|
+
tasks.append(task)
|
|
161
|
+
i += 1
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Error preparing tasks: {str(e)}")
|
|
164
|
+
|
|
165
|
+
# Wait for all tasks with timeout
|
|
166
|
+
done, pending = await asyncio.wait(tasks, timeout=self.timeout)
|
|
167
|
+
|
|
168
|
+
# Process completed tasks
|
|
169
|
+
for task in done:
|
|
170
|
+
try:
|
|
171
|
+
task_result = task.result()
|
|
172
|
+
result_ids[task.index] = task_result.id
|
|
173
|
+
logger.info(f"Successfully processed task {task.index}")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error processing task {task.index}: {str(e)}")
|
|
176
|
+
|
|
177
|
+
# Log information about pending tasks
|
|
178
|
+
if pending:
|
|
179
|
+
logger.error(f"{len(pending)} tasks didn't complete within the timeout")
|
|
180
|
+
for task in pending:
|
|
181
|
+
logger.error(
|
|
182
|
+
f"Task at index {task.index} didn't complete within timeout"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
output = MultipleExecutorOutput(
|
|
186
|
+
executor_input=self,
|
|
187
|
+
result_mapping=self.result_mapping,
|
|
188
|
+
result_ids=result_ids,
|
|
189
|
+
)
|
|
190
|
+
logger.info("finished")
|
|
191
|
+
return output
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def ui_base_schema(cls):
|
|
195
|
+
return {"inputs_data": {"ui:widget": "hidden"}}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@simstack_model
|
|
199
|
+
class MultipleExecutorOutput(Model, Generic[T, R]):
|
|
200
|
+
"""
|
|
201
|
+
Represents the output of a MultipleExecutor.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
executor_input: MultipleExecutorInput = Reference()
|
|
205
|
+
result_mapping: str # there is one result: either a Model or a SimstackResult
|
|
206
|
+
result_ids: List[Union[ObjectId, None]] = Field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def json_schema(cls):
|
|
210
|
+
return {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {"results": {"type": "array", "items": {"type": "object"}}},
|
|
213
|
+
"required": ["result_mapping", "results"],
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@async_helper
|
|
217
|
+
async def custom_model_dump(self) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Retrieve all results and return them as a list of dictionaries.
|
|
220
|
+
"""
|
|
221
|
+
results = []
|
|
222
|
+
async for result in self.result_generator():
|
|
223
|
+
if result is not None:
|
|
224
|
+
results.append(result.model_dump())
|
|
225
|
+
else:
|
|
226
|
+
results.append(None)
|
|
227
|
+
print(f"{PREFIX} custom_model_dump: {results}")
|
|
228
|
+
return {"result": results}
|
|
229
|
+
|
|
230
|
+
async def result_generator(self) -> AsyncGenerator[Union[R, None], None]:
|
|
231
|
+
"""Deserialize the result models"""
|
|
232
|
+
result_model_class = await import_class(self.result_mapping)
|
|
233
|
+
for result_id in self.result_ids:
|
|
234
|
+
if result_id is not None:
|
|
235
|
+
result_model = await context.db.find_one(
|
|
236
|
+
result_model_class, result_model_class.id == result_id
|
|
237
|
+
)
|
|
238
|
+
if result_model is not None:
|
|
239
|
+
yield result_model
|
|
240
|
+
else:
|
|
241
|
+
yield None
|
|
242
|
+
else:
|
|
243
|
+
yield None
|
|
244
|
+
|
|
245
|
+
@async_helper
|
|
246
|
+
async def make_table(self):
|
|
247
|
+
"""
|
|
248
|
+
Create a table representation of the results with formatted columns.
|
|
249
|
+
Returns a dictionary with table data and column definitions.
|
|
250
|
+
"""
|
|
251
|
+
# Get inputs and results data
|
|
252
|
+
inputs = await self.get_inputs()
|
|
253
|
+
results = await self.get_results()
|
|
254
|
+
|
|
255
|
+
# Create tables for inputs and results
|
|
256
|
+
inputs_table = make_table_entries(inputs)
|
|
257
|
+
results_table = make_table_entries(results)
|
|
258
|
+
|
|
259
|
+
# Merge data based on finished indices
|
|
260
|
+
merged_entries = []
|
|
261
|
+
for i, input_entry in enumerate(inputs_table["rowData"]):
|
|
262
|
+
entry = {**input_entry}
|
|
263
|
+
if i in self.finished_indices:
|
|
264
|
+
result_idx = self.finished_indices.index(i)
|
|
265
|
+
entry.update(results_table["rowData"][result_idx])
|
|
266
|
+
# TODO find all artifacts to this result and add the data to the entry
|
|
267
|
+
merged_entries.append(entry)
|
|
268
|
+
|
|
269
|
+
# Combine column definitions
|
|
270
|
+
merged_columns = inputs_table["columnDefs"] + results_table["columnDefs"]
|
|
271
|
+
|
|
272
|
+
return {"rowData": merged_entries, "columnDefs": merged_columns}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@node
|
|
276
|
+
async def multiple_executor(
|
|
277
|
+
executor: MultipleExecutorInput, **kwargs
|
|
278
|
+
) -> MultipleExecutorOutput:
|
|
279
|
+
return await executor.execute(**kwargs)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@node
|
|
283
|
+
async def async_adder(arg: BinaryOperationInput, **kwargs) -> FloatData:
|
|
284
|
+
return FloatData(value=arg.arg1 + arg.arg2)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def main():
|
|
288
|
+
context.initialize()
|
|
289
|
+
executor_input = MultipleExecutorInput.from_function(async_adder)
|
|
290
|
+
# Create a list of input models
|
|
291
|
+
inputs = [BinaryOperationInput(arg1=2, arg2=value) for value in range(1, 3)]
|
|
292
|
+
|
|
293
|
+
# Append the input models to the executor
|
|
294
|
+
for input_model in inputs:
|
|
295
|
+
await executor_input.append(input_model)
|
|
296
|
+
|
|
297
|
+
executor_output = await multiple_executor(executor_input)
|
|
298
|
+
|
|
299
|
+
print("Results: ", executor_output)
|
|
300
|
+
async for item1, item2 in async_zip(
|
|
301
|
+
executor_input.input_generator(), executor_output.result_generator()
|
|
302
|
+
):
|
|
303
|
+
print(f"Input: {item1}, Result: {item2}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .simstack_model import simstack_model
|
|
2
|
+
from .array_list import ArrayList
|
|
3
|
+
from .artifact_models import ArtifactMapping, ArtifactModel
|
|
4
|
+
from .base_types import (
|
|
5
|
+
IntData,
|
|
6
|
+
FloatData,
|
|
7
|
+
StringData,
|
|
8
|
+
BooleanData,
|
|
9
|
+
BinaryOperationInput,
|
|
10
|
+
IteratorInput,
|
|
11
|
+
)
|
|
12
|
+
from .file_list import FileListModel
|
|
13
|
+
from .models import (
|
|
14
|
+
ModelMapping,
|
|
15
|
+
NodeModel,
|
|
16
|
+
)
|
|
17
|
+
from .parameters import Parameters
|
|
18
|
+
from .node_registry import NodeRegistry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ModelMapping",
|
|
23
|
+
"NodeModel",
|
|
24
|
+
"IntData",
|
|
25
|
+
"FloatData",
|
|
26
|
+
"StringData",
|
|
27
|
+
"BooleanData",
|
|
28
|
+
"ArrayList",
|
|
29
|
+
"BinaryOperationInput",
|
|
30
|
+
"IteratorInput",
|
|
31
|
+
"ArtifactMapping",
|
|
32
|
+
"ArtifactModel",
|
|
33
|
+
"FileListModel",
|
|
34
|
+
"Parameters",
|
|
35
|
+
"NodeRegistry",
|
|
36
|
+
"simstack_model",
|
|
37
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from odmantic import Model, Field
|
|
4
|
+
|
|
5
|
+
from simstack.models import simstack_model
|
|
6
|
+
from simstack.models.array_storage import ArrayStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@simstack_model
|
|
10
|
+
class ArrayList(Model):
|
|
11
|
+
array_list: List[ArrayStorage] = Field(default_factory=list)
|
|
12
|
+
|
|
13
|
+
def append(self, array_storage: ArrayStorage):
|
|
14
|
+
"""Add a new ArrayStorage to the list"""
|
|
15
|
+
self.array_list.append(array_storage)
|
|
16
|
+
|
|
17
|
+
def get(self, index: int) -> ArrayStorage:
|
|
18
|
+
"""Get ArrayStorage at specified index"""
|
|
19
|
+
return self.array_list[index]
|
|
20
|
+
|
|
21
|
+
def remove(self, index: int):
|
|
22
|
+
"""Remove ArrayStorage at specified index"""
|
|
23
|
+
del self.array_list[index]
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def length(self) -> int:
|
|
27
|
+
"""Get number of items in the list"""
|
|
28
|
+
return len(self.array_list)
|
|
29
|
+
|
|
30
|
+
def clear(self):
|
|
31
|
+
"""Remove all items from the list"""
|
|
32
|
+
self.array_list.clear()
|
|
33
|
+
|
|
34
|
+
def __iter__(self):
|
|
35
|
+
"""Enable iteration over array_list"""
|
|
36
|
+
return iter(self.array_list)
|
|
37
|
+
|
|
38
|
+
def __getitem__(self, index: int) -> ArrayStorage:
|
|
39
|
+
"""Enable access using square brackets"""
|
|
40
|
+
return self.array_list[index]
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, index: int, value: ArrayStorage):
|
|
43
|
+
"""Enable item assignment using square brackets"""
|
|
44
|
+
self.array_list[index] = value
|
|
45
|
+
|
|
46
|
+
def __len__(self) -> int:
|
|
47
|
+
"""Enable len() function"""
|
|
48
|
+
return len(self.array_list)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from odmantic import Model
|
|
5
|
+
|
|
6
|
+
from simstack.models.simstack_model import simstack_model
|
|
7
|
+
from simstack.util.ui_tools import ui_hide_fields
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@simstack_model
|
|
11
|
+
class ArrayStorage(Model):
|
|
12
|
+
name: str
|
|
13
|
+
shape: Optional[str] = None # Store array shape as string like "3,3"
|
|
14
|
+
data_json: Optional[str] = None # Store flattened array data as JSON
|
|
15
|
+
|
|
16
|
+
def set_array(self, array):
|
|
17
|
+
"""Store a numpy array"""
|
|
18
|
+
self.shape = ",".join(str(dim) for dim in array.shape)
|
|
19
|
+
self.data_json = json.dumps(array.flatten().tolist())
|
|
20
|
+
|
|
21
|
+
def get_array(self):
|
|
22
|
+
"""Retrieve the numpy array"""
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
shape = tuple(int(dim) for dim in self.shape.split(",")) if self.shape else ()
|
|
26
|
+
flat_array = (
|
|
27
|
+
np.array(json.loads(self.data_json)) if self.data_json else np.array([])
|
|
28
|
+
)
|
|
29
|
+
return flat_array.reshape(shape)
|
|
30
|
+
|
|
31
|
+
def make_table_entries(
|
|
32
|
+
self,
|
|
33
|
+
max_recursion_level=1,
|
|
34
|
+
drop_id=True,
|
|
35
|
+
current_level=0,
|
|
36
|
+
visited=None,
|
|
37
|
+
field_prefix="",
|
|
38
|
+
):
|
|
39
|
+
return {"name": self.name}
|
|
40
|
+
|
|
41
|
+
def make_column_defs_instance(
|
|
42
|
+
self,
|
|
43
|
+
table_name=None,
|
|
44
|
+
max_recursion_level=1,
|
|
45
|
+
drop_id=True,
|
|
46
|
+
current_level=0,
|
|
47
|
+
visited=None,
|
|
48
|
+
field_prefix="",
|
|
49
|
+
):
|
|
50
|
+
return [{"field": self.name, "headerName": "Array"}]
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def ui_schema(cls, **kwargs) -> dict:
|
|
54
|
+
return ui_hide_fields({}, ["shape", "data_json"])
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
|
|
3
|
+
from odmantic import Model, Field
|
|
4
|
+
|
|
5
|
+
from simstack.models.pickle_models import FunctionPickle
|
|
6
|
+
from simstack.models.simstack_model import simstack_model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# class ArtifactList(List):
|
|
10
|
+
# """A list that knows how to update its parent Artifact when modified."""
|
|
11
|
+
#
|
|
12
|
+
# def __init__(self, parent_artifact, key, initial_values=None):
|
|
13
|
+
# super().__init__(initial_values or [])
|
|
14
|
+
# self.parent_artifact = parent_artifact
|
|
15
|
+
# self.key = key
|
|
16
|
+
#
|
|
17
|
+
# def append(self, value):
|
|
18
|
+
# super().append(value)
|
|
19
|
+
# self.parent_artifact[self.key] = self # Update the parent
|
|
20
|
+
#
|
|
21
|
+
# def extend(self, values):
|
|
22
|
+
# super().extend(values)
|
|
23
|
+
# self.parent_artifact[self.key] = self # Update the parent
|
|
24
|
+
#
|
|
25
|
+
# # Add other list methods that modify the list
|
|
26
|
+
# def __setitem__(self, index, value):
|
|
27
|
+
# super().__setitem__(index, value)
|
|
28
|
+
# self.parent_artifact[self.key] = self
|
|
29
|
+
#
|
|
30
|
+
# def __delitem__(self, index):
|
|
31
|
+
# super().__delitem__(index)
|
|
32
|
+
# self.parent_artifact[self.key] = self
|
|
33
|
+
#
|
|
34
|
+
# def insert(self, index, value):
|
|
35
|
+
# super().insert(index, value)
|
|
36
|
+
# self.parent_artifact[self.key] = self
|
|
37
|
+
#
|
|
38
|
+
# def pop(self, index=-1):
|
|
39
|
+
# value = super().pop(index)
|
|
40
|
+
# self.parent_artifact[self.key] = self
|
|
41
|
+
# return value
|
|
42
|
+
#
|
|
43
|
+
# def remove(self, value):
|
|
44
|
+
# super().remove(value)
|
|
45
|
+
# self.parent_artifact[self.key] = self
|
|
46
|
+
#
|
|
47
|
+
# def clear(self):
|
|
48
|
+
# super().clear()
|
|
49
|
+
# self.parent_artifact[self.key] = self
|
|
50
|
+
#
|
|
51
|
+
# def sort(self, *args, **kwargs):
|
|
52
|
+
# super().sort(*args, **kwargs)
|
|
53
|
+
# self.parent_artifact[self.key] = self
|
|
54
|
+
#
|
|
55
|
+
# def reverse(self):
|
|
56
|
+
# super().reverse()
|
|
57
|
+
# self.parent_artifact[self.key] = self
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@simstack_model
|
|
61
|
+
class ArtifactModel(Model):
|
|
62
|
+
name: str
|
|
63
|
+
description: Optional[str] = None
|
|
64
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
|
65
|
+
path: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
model_config = {
|
|
68
|
+
"collection": "artifacts" # All subclasses will use the same collection
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# class IntArtifactModel(ArtifactModel):
|
|
73
|
+
# type: Literal["int"] = "int" # Fixed value for this subclass
|
|
74
|
+
# value: int
|
|
75
|
+
#
|
|
76
|
+
# class FloatArtifactModel(ArtifactModel):
|
|
77
|
+
# type: Literal["float"] = "float" # Fixed value for this subclass
|
|
78
|
+
# value: float
|
|
79
|
+
#
|
|
80
|
+
# class StringArtifactModel(ArtifactModel):
|
|
81
|
+
# type: Literal["string"] = "string" # Fixed value for this subclass
|
|
82
|
+
# value: str
|
|
83
|
+
#
|
|
84
|
+
# class ImageArtifactModel(ArtifactModel):
|
|
85
|
+
# type: Literal["image"] = "image" # Fixed value for this subclass
|
|
86
|
+
# value: str # the image is stored as a base64 string or a file path
|
|
87
|
+
#
|
|
88
|
+
# # Forward reference for type hints
|
|
89
|
+
# ArtifactType = Union[ArtifactModel, IntArtifactModel, FloatArtifactModel, StringArtifactModel, 'ArtifactModelList', ImageArtifactModel]
|
|
90
|
+
#
|
|
91
|
+
# # List subclass - can contain other items
|
|
92
|
+
# class ArtifactModelList(ArtifactModel):
|
|
93
|
+
# type: Literal["list"] = "list"
|
|
94
|
+
# items: List[ArtifactType] = Field(default_factory=list) # List of Item objects
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ArtifactMapping(Model):
|
|
98
|
+
"""
|
|
99
|
+
ArtifactsMapper is a mapping between the artifact and a node registry-path.
|
|
100
|
+
The workflow executor passes a path of the type
|
|
101
|
+
|
|
102
|
+
node1.node2.node4. ... .nodeN
|
|
103
|
+
|
|
104
|
+
where node is the function name of the node
|
|
105
|
+
|
|
106
|
+
Regex can maps this to the target path of the ArtifactsMapping, e.g. a path
|
|
107
|
+
|
|
108
|
+
*.parent1.node
|
|
109
|
+
|
|
110
|
+
it would map on all nodes with name node that have been directly called by a node with the name parent1.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
name: str = Field(default="artifact", unique=True)
|
|
116
|
+
regex_pattern: str = Field(default="")
|
|
117
|
+
function_mapping: str = Field(default="")
|
|
118
|
+
function_code: str = Field(default="")
|
|
119
|
+
pickle_function: Optional[FunctionPickle] = None
|
|
120
|
+
|
|
121
|
+
def set_values(self, other: "ArtifactMapping") -> "ArtifactMapping":
|
|
122
|
+
self.name = other.name
|
|
123
|
+
self.regex_pattern = other.regex_pattern
|
|
124
|
+
self.function_mapping = other.function_mapping
|
|
125
|
+
self.function_code = other.function_code
|
|
126
|
+
self.pickle_function = other.pickle_function
|
|
127
|
+
return self
|