fameio 3.1.1__py3-none-any.whl → 3.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.
- fameio/cli/convert_results.py +6 -4
- fameio/cli/make_config.py +6 -4
- fameio/cli/parser.py +41 -29
- fameio/input/__init__.py +1 -1
- fameio/input/loader/__init__.py +9 -7
- fameio/input/loader/controller.py +59 -8
- fameio/input/loader/loader.py +14 -7
- fameio/input/metadata.py +35 -13
- fameio/input/resolver.py +5 -4
- fameio/input/scenario/agent.py +50 -16
- fameio/input/scenario/attribute.py +14 -15
- fameio/input/scenario/contract.py +152 -43
- fameio/input/scenario/exception.py +44 -18
- fameio/input/scenario/fameiofactory.py +63 -7
- fameio/input/scenario/generalproperties.py +17 -6
- fameio/input/scenario/scenario.py +111 -28
- fameio/input/scenario/stringset.py +27 -8
- fameio/input/schema/agenttype.py +21 -2
- fameio/input/schema/attribute.py +91 -22
- fameio/input/schema/java_packages.py +8 -5
- fameio/input/schema/schema.py +35 -9
- fameio/input/validator.py +22 -15
- fameio/input/writer.py +136 -36
- fameio/logs.py +3 -31
- fameio/output/__init__.py +5 -1
- fameio/output/agent_type.py +86 -23
- fameio/output/conversion.py +47 -29
- fameio/output/csv_writer.py +88 -18
- fameio/output/data_transformer.py +7 -14
- fameio/output/input_dao.py +62 -21
- fameio/output/output_dao.py +26 -4
- fameio/output/reader.py +58 -13
- fameio/output/yaml_writer.py +15 -6
- fameio/scripts/__init__.py +9 -2
- fameio/scripts/convert_results.py +123 -50
- fameio/scripts/convert_results.py.license +1 -1
- fameio/scripts/exception.py +7 -0
- fameio/scripts/make_config.py +34 -12
- fameio/scripts/make_config.py.license +1 -1
- fameio/series.py +117 -33
- fameio/time.py +74 -17
- fameio/tools.py +7 -5
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/METADATA +19 -13
- fameio-3.2.0.dist-info/RECORD +56 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/WHEEL +1 -1
- CHANGELOG.md +0 -288
- fameio-3.1.1.dist-info/RECORD +0 -56
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/entry_points.txt +0 -0
fameio/output/agent_type.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
from
|
4
|
+
from __future__ import annotations
|
5
5
|
|
6
6
|
from fameprotobuf.services_pb2 import Output
|
7
7
|
|
8
|
+
from fameio.logs import log_error
|
9
|
+
from fameio.output import OutputError
|
10
|
+
|
8
11
|
|
9
12
|
class AgentType:
|
10
13
|
"""Provides information derived from an underlying protobuf AgentType"""
|
@@ -34,7 +37,7 @@ class AgentType:
|
|
34
37
|
"""Returns set of IDs for complex columns, ignoring simple columns"""
|
35
38
|
return {field.field_id for field in self._agent_type.fields if len(field.index_names) > 0}
|
36
39
|
|
37
|
-
def get_column_name_for_id(self, column_index: int) ->
|
40
|
+
def get_column_name_for_id(self, column_index: int) -> str | None:
|
38
41
|
"""Returns name of column by given `column_index` or None, if column is not present"""
|
39
42
|
if 0 <= column_index < len(self._agent_type.fields):
|
40
43
|
return self._agent_type.fields[column_index].field_name
|
@@ -49,7 +52,7 @@ class AgentType:
|
|
49
52
|
return self._agent_type.class_name
|
50
53
|
|
51
54
|
|
52
|
-
class AgentTypeError(
|
55
|
+
class AgentTypeError(OutputError):
|
53
56
|
"""Indicates an error with the agent types definitions"""
|
54
57
|
|
55
58
|
|
@@ -59,34 +62,94 @@ class AgentTypeLog:
|
|
59
62
|
_ERR_AGENT_TYPE_MISSING = "Requested AgentType `{}` not found."
|
60
63
|
_ERR_DOUBLE_DEFINITION = "Just one definition allowed per AgentType. Found multiple for {}. File might be corrupt."
|
61
64
|
|
62
|
-
def __init__(self,
|
63
|
-
|
64
|
-
|
65
|
+
def __init__(self, _agent_name_filter_list: list[str]) -> None:
|
66
|
+
"""
|
67
|
+
Initialises new AgentTypeLog
|
68
|
+
|
69
|
+
Args:
|
70
|
+
_agent_name_filter_list: list of agent type names that are requested for output data extraction
|
71
|
+
"""
|
72
|
+
self._agent_name_filter_list: list[str] | None = (
|
73
|
+
[agent.upper() for agent in _agent_name_filter_list] if _agent_name_filter_list else None
|
74
|
+
)
|
75
|
+
self._requested_agent_types: dict[str, AgentType] = {}
|
76
|
+
self._agents_with_output: list[str] = []
|
65
77
|
|
66
78
|
def update_agents(self, new_types: dict[str, Output.AgentType]) -> None:
|
67
|
-
"""
|
68
|
-
if
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
"""
|
80
|
+
If any new `agent_types` are provided, checks if they are requested for extraction, and, if so, saves them
|
81
|
+
|
82
|
+
Args:
|
83
|
+
new_types: to be saved (if requested for extraction)
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
AgentTypeError: if agent type was already registered, logged with level "ERROR"
|
87
|
+
"""
|
88
|
+
if not new_types:
|
89
|
+
return
|
90
|
+
|
91
|
+
self._agents_with_output.extend(list(new_types.keys()))
|
92
|
+
filtered_types = self._filter_agents_by_name(new_types)
|
93
|
+
self._ensure_no_duplication(filtered_types)
|
94
|
+
self._requested_agent_types.update(filtered_types)
|
95
|
+
|
96
|
+
def _filter_agents_by_name(self, new_types: dict[str, Output.AgentType]) -> dict[str, Output.AgentType]:
|
97
|
+
"""
|
98
|
+
Removes and entries from `new_types` not on `agent_name_filter_list`
|
99
|
+
|
100
|
+
Args:
|
101
|
+
new_types: to be filtered
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
filtered list, or original list if no filter is active
|
105
|
+
"""
|
106
|
+
if self._agent_name_filter_list:
|
107
|
+
return {
|
108
|
+
agent_name: agent_type
|
109
|
+
for agent_name, agent_type in new_types.items()
|
110
|
+
if agent_name.upper() in self._agent_name_filter_list
|
111
|
+
}
|
112
|
+
return new_types
|
113
|
+
|
114
|
+
def _ensure_no_duplication(self, filtered_types: dict[str, Output.AgentType]) -> None:
|
115
|
+
"""
|
116
|
+
Ensures no duplicate agent type definitions occur
|
117
|
+
|
118
|
+
Args:
|
119
|
+
filtered_types: to be checked for duplications with already registered types
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
AgentTypeError: if duplicate agent type is found, logged with level "ERROR"
|
123
|
+
"""
|
124
|
+
for agent_name in self._requested_agent_types:
|
125
|
+
if agent_name in filtered_types:
|
126
|
+
raise log_error(AgentTypeError(self._ERR_DOUBLE_DEFINITION.format(agent_name)))
|
79
127
|
|
80
128
|
def has_any_agent_type(self) -> bool:
|
81
129
|
"""Returns True if any agent type was registered so far present"""
|
82
130
|
return len(self._requested_agent_types) > 0
|
83
131
|
|
84
|
-
def get_agent_type(self,
|
85
|
-
"""
|
86
|
-
|
87
|
-
|
88
|
-
|
132
|
+
def get_agent_type(self, agent_type_name: str) -> AgentType:
|
133
|
+
"""
|
134
|
+
Returns the requested type of agent
|
135
|
+
|
136
|
+
Args:
|
137
|
+
agent_type_name: requested name of agent type
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
stored agent type
|
141
|
+
|
142
|
+
Raises:
|
143
|
+
AgentTypeError: if no agent type could be found with that name, logged with level "ERROR"
|
144
|
+
"""
|
145
|
+
if agent_type_name not in self._requested_agent_types:
|
146
|
+
raise log_error(AgentTypeError(self._ERR_AGENT_TYPE_MISSING.format(agent_type_name)))
|
147
|
+
return AgentType(self._requested_agent_types[agent_type_name])
|
89
148
|
|
90
149
|
def is_requested(self, agent_name: str) -> bool:
|
91
150
|
"""Returns True if given agent_name is known and requested"""
|
92
151
|
return agent_name in self._requested_agent_types
|
152
|
+
|
153
|
+
def get_agents_with_output(self) -> list[str]:
|
154
|
+
"""Returns all names of agents that had output"""
|
155
|
+
return self._agents_with_output
|
fameio/output/conversion.py
CHANGED
@@ -1,53 +1,62 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
4
5
|
|
5
6
|
import math
|
6
|
-
from typing import Optional
|
7
7
|
|
8
8
|
import pandas as pd
|
9
9
|
|
10
10
|
from fameio.cli.options import TimeOptions
|
11
|
-
from fameio.logs import
|
12
|
-
from fameio.
|
11
|
+
from fameio.logs import log_error, log
|
12
|
+
from fameio.output import OutputError
|
13
|
+
from fameio.time import FameTime, ConversionError as TimeConversionError
|
13
14
|
|
14
15
|
_ERR_UNIMPLEMENTED = "Time conversion mode '{}' not implemented."
|
16
|
+
_ERR_TIME_CONVERSION = "Conversion of timestamps failed."
|
15
17
|
_ERR_NEGATIVE = "StepsBefore and StepsAfter must be Zero or positive integers"
|
16
18
|
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
) -> None:
|
21
|
-
"""Applies time merging to `data` based on given `offset`, `period`, and `first_positive_focal_point`"""
|
22
|
-
log().debug("Grouping TimeSteps...")
|
23
|
-
for key in dataframes.keys():
|
24
|
-
df = dataframes[key]
|
25
|
-
index_columns = df.index.names
|
26
|
-
df.reset_index(inplace=True)
|
27
|
-
df["TimeStep"] = df["TimeStep"].apply(lambda t: merge_time(t, first_positive_focal_point, offset, period))
|
28
|
-
dataframes[key] = df.groupby(by=index_columns).sum()
|
20
|
+
class ConversionError(OutputError):
|
21
|
+
"""An error that occurred during conversion of output data"""
|
29
22
|
|
30
23
|
|
31
|
-
def apply_time_merging(data: dict[
|
24
|
+
def apply_time_merging(data: dict[str | None, pd.DataFrame], config: list[int] | None) -> None:
|
32
25
|
"""
|
33
26
|
Applies merging of TimeSteps inplace for given `data`
|
34
27
|
|
35
28
|
Args:
|
36
29
|
data: one or multiple DataFrames of time series; depending on the given config, contents might be modified
|
37
30
|
config: three integer values defining how to merge data within a range of time steps
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
ConversionError: if parameters are not valid, logged with level "ERROR"
|
38
34
|
"""
|
39
35
|
if not config or all(v == 0 for v in config):
|
40
36
|
return
|
41
37
|
focal_point, steps_before, steps_after = config
|
42
38
|
if steps_before < 0 or steps_after < 0:
|
43
|
-
raise
|
39
|
+
raise log_error(ConversionError(_ERR_NEGATIVE))
|
44
40
|
|
45
41
|
period = steps_before + steps_after + 1
|
46
42
|
first_positive_focal_point = focal_point % period
|
47
43
|
_apply_time_merging(data, offset=steps_before, period=period, first_positive_focal_point=first_positive_focal_point)
|
48
44
|
|
49
45
|
|
50
|
-
def
|
46
|
+
def _apply_time_merging(
|
47
|
+
dataframes: dict[str | None, pd.DataFrame], offset: int, period: int, first_positive_focal_point: int
|
48
|
+
) -> None:
|
49
|
+
"""Applies time merging to `data` based on given `offset`, `period`, and `first_positive_focal_point`"""
|
50
|
+
log().debug("Grouping TimeSteps...")
|
51
|
+
for key in dataframes.keys():
|
52
|
+
df = dataframes[key]
|
53
|
+
index_columns = df.index.names
|
54
|
+
df.reset_index(inplace=True)
|
55
|
+
df["TimeStep"] = df["TimeStep"].apply(lambda t: _merge_time(t, first_positive_focal_point, offset, period))
|
56
|
+
dataframes[key] = df.groupby(by=index_columns).sum()
|
57
|
+
|
58
|
+
|
59
|
+
def _merge_time(time_step: int, focal_time: int, offset: int, period: int) -> int:
|
51
60
|
"""
|
52
61
|
Returns `time_step` rounded to its corresponding focal point
|
53
62
|
|
@@ -63,25 +72,31 @@ def merge_time(time_step: int, focal_time: int, offset: int, period: int) -> int
|
|
63
72
|
return math.floor((time_step + offset - focal_time) / period) * period + focal_time
|
64
73
|
|
65
74
|
|
66
|
-
def apply_time_option(data: dict[
|
75
|
+
def apply_time_option(data: dict[str | None, pd.DataFrame], mode: TimeOptions) -> None:
|
67
76
|
"""
|
68
77
|
Applies time option based on given `mode` inplace of given `data`
|
69
78
|
|
70
79
|
Args:
|
71
80
|
data: one or multiple DataFrames of time series; column `TimeStep` might be modified (depending on mode)
|
72
81
|
mode: name of time conversion mode (derived from Enum)
|
73
|
-
"""
|
74
|
-
if mode == TimeOptions.INT:
|
75
|
-
log().debug("No time conversion...")
|
76
|
-
elif mode == TimeOptions.UTC:
|
77
|
-
_convert_time_index(data, "%Y-%m-%d %H:%M:%S")
|
78
|
-
elif mode == TimeOptions.FAME:
|
79
|
-
_convert_time_index(data, "%Y-%m-%d_%H:%M:%S")
|
80
|
-
else:
|
81
|
-
log_error_and_raise(ConversionError(_ERR_UNIMPLEMENTED.format(mode)))
|
82
82
|
|
83
|
-
|
84
|
-
|
83
|
+
Raises:
|
84
|
+
ConversionError: if provided mode is not implemented , logged with level "ERROR"
|
85
|
+
"""
|
86
|
+
try:
|
87
|
+
if mode == TimeOptions.INT:
|
88
|
+
log().debug("No time conversion...")
|
89
|
+
elif mode == TimeOptions.UTC:
|
90
|
+
_convert_time_index(data, "%Y-%m-%d %H:%M:%S")
|
91
|
+
elif mode == TimeOptions.FAME:
|
92
|
+
_convert_time_index(data, "%Y-%m-%d_%H:%M:%S")
|
93
|
+
else:
|
94
|
+
raise log_error(ConversionError(_ERR_UNIMPLEMENTED.format(mode)))
|
95
|
+
except TimeConversionError as e:
|
96
|
+
raise log_error(ConversionError(_ERR_TIME_CONVERSION.format())) from e
|
97
|
+
|
98
|
+
|
99
|
+
def _convert_time_index(data: dict[str | None, pd.DataFrame], datetime_format: str) -> None:
|
85
100
|
"""
|
86
101
|
Inplace replacement of `TimeStep` column in MultiIndex of each item of `data` from FAME's time steps` to DateTime
|
87
102
|
in given `date_format`
|
@@ -89,6 +104,9 @@ def _convert_time_index(data: dict[Optional[str], pd.DataFrame], datetime_format
|
|
89
104
|
Args:
|
90
105
|
data: one or multiple DataFrames of time series; column `TimeStep` will be modified
|
91
106
|
datetime_format: used for the conversion
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
TimeConversionError: if time cannot be converted, logged with level "ERROR"
|
92
110
|
"""
|
93
111
|
log().debug(f"Converting TimeStep to format '{datetime_format}'...")
|
94
112
|
for _, df in data.items():
|
fameio/output/csv_writer.py
CHANGED
@@ -1,36 +1,50 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
4
6
|
from pathlib import Path
|
5
|
-
from typing import Union
|
6
7
|
|
7
8
|
import pandas as pd
|
8
9
|
|
9
|
-
from fameio.logs import log
|
10
|
+
from fameio.logs import log, log_error
|
11
|
+
from fameio.output import OutputError
|
10
12
|
from fameio.output.data_transformer import INDEX
|
11
13
|
from fameio.series import TimeSeriesManager
|
12
14
|
from fameio.tools import ensure_path_exists
|
13
15
|
|
14
16
|
|
17
|
+
class CsvWriterError(OutputError):
|
18
|
+
"""An error occurred during writing a CSV file"""
|
19
|
+
|
20
|
+
|
15
21
|
class CsvWriter:
|
16
22
|
"""Writes dataframes to different csv files"""
|
17
23
|
|
24
|
+
_ERR_DIR_CREATE = "Could not create directory for output files: '{}'"
|
25
|
+
_ERR_FILE_OPEN = "Could not open file for writing: '{}'"
|
26
|
+
_ERR_FILE_WRITE = "Could not write to file '{}' due to: {}"
|
27
|
+
|
18
28
|
_INFO_USING_PATH = "Using specified output path: {}"
|
19
29
|
_INFO_USING_DERIVED_PATH = "No output path specified - writing to new local folder: {}"
|
20
30
|
|
21
31
|
CSV_FILE_SUFFIX = ".csv"
|
22
32
|
|
23
33
|
def __init__(self, config_output: Path, input_file_path: Path, single_export: bool) -> None:
|
34
|
+
"""
|
35
|
+
Raises:
|
36
|
+
CsvWriterError: if output folder could not be created, logged with level "ERROR"
|
37
|
+
"""
|
24
38
|
self._single_export = single_export
|
25
39
|
self._output_folder = self._get_output_folder_name(config_output, input_file_path)
|
26
|
-
self._files = {}
|
40
|
+
self._files: dict[str, Path] = {}
|
27
41
|
self._create_output_folder()
|
28
42
|
|
29
43
|
@staticmethod
|
30
44
|
def _get_output_folder_name(config_output: Path, input_file_path: Path) -> Path:
|
31
45
|
"""Returns name of the output folder derived either from the specified `config_output` or `input_file_path`"""
|
32
46
|
if config_output:
|
33
|
-
output_folder_name = config_output
|
47
|
+
output_folder_name: str | Path = config_output
|
34
48
|
log().info(CsvWriter._INFO_USING_PATH.format(config_output))
|
35
49
|
else:
|
36
50
|
output_folder_name = input_file_path.stem
|
@@ -38,13 +52,30 @@ class CsvWriter:
|
|
38
52
|
return Path(output_folder_name)
|
39
53
|
|
40
54
|
def _create_output_folder(self) -> None:
|
41
|
-
"""
|
55
|
+
"""
|
56
|
+
Creates output folder if not yet present
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
CsvWriterError: if output folder could not be created, logged with level "ERROR"
|
60
|
+
"""
|
42
61
|
log().debug("Creating output folder if required...")
|
43
62
|
if not self._output_folder.is_dir():
|
44
|
-
|
63
|
+
try:
|
64
|
+
self._output_folder.mkdir(parents=True)
|
65
|
+
except OSError as e:
|
66
|
+
raise log_error(CsvWriterError(self._ERR_DIR_CREATE.format(self._output_folder))) from e
|
67
|
+
|
68
|
+
def write_to_files(self, agent_name: str, data: dict[None | str, pd.DataFrame]) -> None:
|
69
|
+
"""
|
70
|
+
Writes `data` for given `agent_name` to .csv file(s)
|
71
|
+
|
72
|
+
Args:
|
73
|
+
agent_name: name of agent whose data are to be written to file(s)
|
74
|
+
data: previously extracted data for that agent that are to be written
|
45
75
|
|
46
|
-
|
47
|
-
|
76
|
+
Raises:
|
77
|
+
CsvWriterError: when file could not be written, logged on level "ERROR"
|
78
|
+
"""
|
48
79
|
for column_name, column_data in data.items():
|
49
80
|
column_data.sort_index(inplace=True)
|
50
81
|
if self._single_export:
|
@@ -56,16 +87,45 @@ class CsvWriter:
|
|
56
87
|
self._write_data_frame(column_data, identifier)
|
57
88
|
|
58
89
|
def write_time_series_to_disk(self, timeseries_manager: TimeSeriesManager) -> None:
|
59
|
-
"""
|
90
|
+
"""
|
91
|
+
Writes time_series of given `timeseries_manager` to disk
|
92
|
+
|
93
|
+
Args:
|
94
|
+
timeseries_manager:
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
CsvWriterError: if data could not be written to disk, logged on level "ERROR"
|
98
|
+
"""
|
60
99
|
for _, name, data in timeseries_manager.get_all_series():
|
61
100
|
if data is not None:
|
62
101
|
target_path = Path(self._output_folder, name)
|
63
102
|
ensure_path_exists(target_path.parent)
|
64
|
-
|
65
|
-
data.to_csv(path_or_buf=target_path, sep=";", header=None, index=None)
|
103
|
+
self._dataframe_to_csv(data, target_path, header=False, index=False, mode="w")
|
66
104
|
|
67
105
|
@staticmethod
|
68
|
-
def
|
106
|
+
def _dataframe_to_csv(data: pd.DataFrame, file: Path, header: bool, index: bool, mode: str) -> None:
|
107
|
+
"""
|
108
|
+
Write given data to specified CSV file with specified parameters using semicolon separators
|
109
|
+
|
110
|
+
Args:
|
111
|
+
data: to be written
|
112
|
+
file: target path of csv file
|
113
|
+
header: write column headers
|
114
|
+
index: write index column(s)
|
115
|
+
mode: append to or overwrite file
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
CsvWriterError: if data could not be written to disk, logged on level "ERROR"
|
119
|
+
"""
|
120
|
+
try:
|
121
|
+
data.to_csv(path_or_buf=file, sep=";", header=header, index=index, mode=mode)
|
122
|
+
except OSError as e:
|
123
|
+
raise log_error(CsvWriterError(CsvWriter._ERR_FILE_OPEN.format(file))) from e
|
124
|
+
except UnicodeError as e:
|
125
|
+
raise log_error(CsvWriterError(CsvWriter._ERR_FILE_WRITE.format(file, str(e)))) from e
|
126
|
+
|
127
|
+
@staticmethod
|
128
|
+
def _get_identifier(agent_name: str, column_name: str | None = None, agent_id: str | None = None) -> str:
|
69
129
|
"""Returns unique identifier for given `agent_name` and (optional) `agent_id` and `column_name`"""
|
70
130
|
identifier = str(agent_name)
|
71
131
|
if column_name:
|
@@ -78,14 +138,24 @@ class CsvWriter:
|
|
78
138
|
"""
|
79
139
|
Appends `data` to existing csv file derived from `identifier` without headers,
|
80
140
|
or writes new file with headers instead
|
141
|
+
|
142
|
+
Args:
|
143
|
+
data: to be written to file
|
144
|
+
identifier: to derive the file name from
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
CsvWriterError: when file could not be written, logged on level "ERROR"
|
81
148
|
"""
|
82
149
|
if self._has_file(identifier):
|
83
150
|
outfile_name = self._get_outfile_name(identifier)
|
84
|
-
|
151
|
+
mode = "a"
|
152
|
+
header = False
|
85
153
|
else:
|
86
154
|
outfile_name = self._create_outfile_name(identifier)
|
87
155
|
self._save_outfile_name(outfile_name, identifier)
|
88
|
-
|
156
|
+
mode = "w"
|
157
|
+
header = True
|
158
|
+
self._dataframe_to_csv(data, outfile_name, header=header, index=True, mode=mode)
|
89
159
|
|
90
160
|
def _has_file(self, identifier: str) -> bool:
|
91
161
|
"""Returns True if a file for given `identifier` was already written"""
|
@@ -97,12 +167,12 @@ class CsvWriter:
|
|
97
167
|
self._files = {}
|
98
168
|
return current_files
|
99
169
|
|
100
|
-
def _get_outfile_name(self, identifier: str) ->
|
101
|
-
"""Returns file
|
170
|
+
def _get_outfile_name(self, identifier: str) -> Path:
|
171
|
+
"""Returns file path for given `agent_name` and (optional) `agent_id`"""
|
102
172
|
return self._files[identifier]
|
103
173
|
|
104
174
|
def _create_outfile_name(self, identifier: str) -> Path:
|
105
|
-
"""Returns fully qualified file
|
175
|
+
"""Returns fully qualified file path based on given `agent_name` and (optional) `agent_id`"""
|
106
176
|
return Path(self._output_folder, f"{identifier}{self.CSV_FILE_SUFFIX}")
|
107
177
|
|
108
178
|
def _save_outfile_name(self, outfile_name: Path, identifier: str) -> None:
|
@@ -4,8 +4,6 @@
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
6
|
from abc import ABC
|
7
|
-
from builtins import staticmethod
|
8
|
-
from typing import Union, Optional
|
9
7
|
|
10
8
|
import pandas as pd
|
11
9
|
from fameprotobuf.services_pb2 import Output
|
@@ -30,13 +28,11 @@ class DataTransformer(ABC):
|
|
30
28
|
def build(complex_column_mode: ResolveOptions) -> DataTransformer:
|
31
29
|
return DataTransformer.MODES[complex_column_mode]()
|
32
30
|
|
33
|
-
def extract_agent_data(
|
34
|
-
self, series: list[Output.Series], agent_type: AgentType
|
35
|
-
) -> dict[Optional[str], pd.DataFrame]:
|
31
|
+
def extract_agent_data(self, series: list[Output.Series], agent_type: AgentType) -> dict[str | None, pd.DataFrame]:
|
36
32
|
"""
|
37
33
|
Returns dict of DataFrame(s) containing all data from given `series` of given `agent_type`.
|
38
34
|
When ResolveOption is `SPLIT`, the dict maps each complex column's name to the associated DataFrame.
|
39
|
-
In any case, the dict maps `None` to a DataFrame with the content of all simple
|
35
|
+
In any case, the dict maps `None` to a DataFrame with the content of all simple columns.
|
40
36
|
"""
|
41
37
|
container = self._extract_agent_data(series, agent_type)
|
42
38
|
data_frames = {}
|
@@ -45,7 +41,7 @@ class DataTransformer(ABC):
|
|
45
41
|
column_name = agent_type.get_column_name_for_id(column_id)
|
46
42
|
if column_id == DataTransformer.SIMPLE_COLUMN_INDEX:
|
47
43
|
data_frame.rename(columns=self._get_column_map(agent_type), inplace=True)
|
48
|
-
index = INDEX
|
44
|
+
index: tuple[str, ...] = INDEX
|
49
45
|
data_frame = data_frame.loc[:, agent_type.get_simple_column_mask()]
|
50
46
|
else:
|
51
47
|
data_frame.rename(columns={0: column_name}, inplace=True)
|
@@ -59,7 +55,7 @@ class DataTransformer(ABC):
|
|
59
55
|
|
60
56
|
def _extract_agent_data(
|
61
57
|
self, series: list[Output.Series], agent_type: AgentType
|
62
|
-
) -> dict[int, dict[tuple, list[
|
58
|
+
) -> dict[int, dict[tuple, list[float | None | str]]]:
|
63
59
|
"""Returns mapping of (agentId, timeStep) to fixed-length list of all output columns for given `class_name`"""
|
64
60
|
container = DataTransformer._create_container(agent_type)
|
65
61
|
mask_simple = agent_type.get_simple_column_mask()
|
@@ -78,7 +74,7 @@ class DataTransformer(ABC):
|
|
78
74
|
self,
|
79
75
|
series: Output.Series,
|
80
76
|
mask_simple: list[bool],
|
81
|
-
container: dict[int, dict[tuple, list[
|
77
|
+
container: dict[int, dict[tuple, list[float | None | str]]],
|
82
78
|
) -> None:
|
83
79
|
"""Adds data from given `series` to specified `container` dict as list"""
|
84
80
|
empty_list: list = [None] * len(mask_simple)
|
@@ -89,14 +85,9 @@ class DataTransformer(ABC):
|
|
89
85
|
if mask_simple[column.field_id]:
|
90
86
|
simple_values[column.field_id] = column.value
|
91
87
|
else:
|
92
|
-
self._merge_complex_column(column, simple_values)
|
93
88
|
self._store_complex_values(column, container, index)
|
94
89
|
container[DataTransformer.SIMPLE_COLUMN_INDEX][index] = simple_values
|
95
90
|
|
96
|
-
@staticmethod
|
97
|
-
def _merge_complex_column(column: Output.Series.Line.Column, values: list) -> None:
|
98
|
-
"""Merges complex column data"""
|
99
|
-
|
100
91
|
@staticmethod
|
101
92
|
def _store_complex_values(column: Output.Series.Line.Column, container: dict[int, dict], base_index: tuple) -> None:
|
102
93
|
"""Stores complex column data"""
|
@@ -112,6 +103,8 @@ class DataTransformerIgnore(DataTransformer):
|
|
112
103
|
|
113
104
|
|
114
105
|
class DataTransformerSplit(DataTransformer):
|
106
|
+
"""Stores complex data columns split by column type"""
|
107
|
+
|
115
108
|
@staticmethod
|
116
109
|
def _store_complex_values(column: Output.Series.Line.Column, container: dict[int, dict], base_index: tuple) -> None:
|
117
110
|
"""Adds inner data from `column` to given `container` - split by column type"""
|