fameio 3.2.0__py3-none-any.whl → 3.4.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 +4 -6
- fameio/cli/make_config.py +3 -5
- fameio/cli/options.py +6 -4
- fameio/cli/parser.py +53 -29
- fameio/cli/reformat.py +58 -0
- fameio/input/__init__.py +4 -4
- fameio/input/loader/__init__.py +4 -6
- fameio/input/loader/controller.py +11 -16
- fameio/input/loader/loader.py +11 -9
- fameio/input/metadata.py +26 -29
- fameio/input/resolver.py +4 -6
- fameio/input/scenario/agent.py +18 -16
- fameio/input/scenario/attribute.py +85 -31
- fameio/input/scenario/contract.py +78 -38
- fameio/input/scenario/exception.py +3 -6
- fameio/input/scenario/fameiofactory.py +7 -12
- fameio/input/scenario/generalproperties.py +7 -8
- fameio/input/scenario/scenario.py +14 -18
- fameio/input/scenario/stringset.py +5 -6
- fameio/input/schema/agenttype.py +8 -10
- fameio/input/schema/attribute.py +30 -36
- fameio/input/schema/java_packages.py +6 -7
- fameio/input/schema/schema.py +9 -11
- fameio/input/validator.py +178 -41
- fameio/input/writer.py +20 -29
- fameio/logs.py +28 -19
- fameio/output/agent_type.py +14 -16
- fameio/output/conversion.py +9 -12
- fameio/output/csv_writer.py +33 -23
- fameio/output/data_transformer.py +11 -11
- fameio/output/execution_dao.py +170 -0
- fameio/output/input_dao.py +16 -19
- fameio/output/output_dao.py +7 -7
- fameio/output/reader.py +8 -10
- fameio/output/yaml_writer.py +2 -3
- fameio/scripts/__init__.py +15 -4
- fameio/scripts/convert_results.py +18 -17
- fameio/scripts/exception.py +1 -1
- fameio/scripts/make_config.py +3 -4
- fameio/scripts/reformat.py +71 -0
- fameio/scripts/reformat.py.license +3 -0
- fameio/series.py +78 -47
- fameio/time.py +56 -18
- fameio/tools.py +42 -4
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/METADATA +64 -40
- fameio-3.4.0.dist-info/RECORD +60 -0
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/entry_points.txt +1 -0
- fameio-3.2.0.dist-info/RECORD +0 -56
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/WHEEL +0 -0
fameio/output/output_dao.py
CHANGED
@@ -14,7 +14,7 @@ from fameio.output.data_transformer import DataTransformer
|
|
14
14
|
|
15
15
|
|
16
16
|
class OutputDAO:
|
17
|
-
"""Grants convenient access to content of Output protobuf messages for given DataStorages"""
|
17
|
+
"""Grants convenient access to content of Output protobuf messages for given DataStorages."""
|
18
18
|
|
19
19
|
def __init__(self, data_storages: list[DataStorage], agent_type_log: AgentTypeLog) -> None:
|
20
20
|
"""
|
@@ -34,21 +34,21 @@ class OutputDAO:
|
|
34
34
|
|
35
35
|
@staticmethod
|
36
36
|
def _extract_output_from_data_storages(data_storages: list[DataStorage]) -> list[Output]:
|
37
|
-
"""Returns list of Outputs extracted from given `data_storages
|
37
|
+
"""Returns list of Outputs extracted from given `data_storages`."""
|
38
38
|
if data_storages is None:
|
39
39
|
data_storages = []
|
40
40
|
return [data_storage.output for data_storage in data_storages if data_storage.HasField("output")]
|
41
41
|
|
42
42
|
@staticmethod
|
43
43
|
def _extract_new_agent_types(outputs: list[Output]) -> dict[str, Output.AgentType]:
|
44
|
-
"""Returns dict of agent names mapped to its type defined in given `outputs
|
44
|
+
"""Returns dict of agent names mapped to its type defined in given `outputs`."""
|
45
45
|
list_of_agent_type_lists = [output.agent_types for output in outputs if len(output.agent_types) > 0]
|
46
46
|
list_of_agent_types = [item for sublist in list_of_agent_type_lists for item in sublist]
|
47
47
|
return {item.class_name: item for item in list_of_agent_types}
|
48
48
|
|
49
49
|
@staticmethod
|
50
50
|
def _extract_series(outputs: list[Output]) -> dict[str, list[Output.Series]]:
|
51
|
-
"""Returns series data from associated `outputs` mapped to the className of its agent"""
|
51
|
+
"""Returns series data from associated `outputs` mapped to the className of its agent."""
|
52
52
|
list_of_series_lists = [output.series for output in outputs if len(output.series) > 0]
|
53
53
|
list_of_series = [series for sublist in list_of_series_lists for series in sublist]
|
54
54
|
|
@@ -60,7 +60,7 @@ class OutputDAO:
|
|
60
60
|
return series_per_class_name
|
61
61
|
|
62
62
|
def get_sorted_agents_to_extract(self) -> Iterable[str]:
|
63
|
-
"""Returns iterator of requested and available agent names in ascending order by count of series"""
|
63
|
+
"""Returns iterator of requested and available agent names in ascending order by count of series."""
|
64
64
|
all_series = self._get_agent_names_by_series_count_ascending()
|
65
65
|
filtered_series = [agent_name for agent_name in all_series if self._agent_type_log.is_requested(agent_name)]
|
66
66
|
return iter(filtered_series)
|
@@ -72,8 +72,8 @@ class OutputDAO:
|
|
72
72
|
return [agent_name for agent_name, _ in sorted_dict]
|
73
73
|
|
74
74
|
def get_agent_data(self, agent_name: str, data_transformer: DataTransformer) -> dict[str | None, pd.DataFrame]:
|
75
|
-
"""
|
76
|
-
|
75
|
+
"""Returns DataFrame(s) containing all data of given `agent` - data is removed after the first call.
|
76
|
+
|
77
77
|
Depending on the chosen ResolveOption the dict contains one DataFrame for the simple (and merged columns),
|
78
78
|
or, in `SPLIT` mode, additional DataFrames mapped to each complex column's name.
|
79
79
|
|
fameio/output/reader.py
CHANGED
@@ -16,11 +16,11 @@ from fameio.output import OutputError
|
|
16
16
|
|
17
17
|
|
18
18
|
class ProtobufReaderError(OutputError):
|
19
|
-
"""Indicates an error while reading a protobuf file"""
|
19
|
+
"""Indicates an error while reading a protobuf file."""
|
20
20
|
|
21
21
|
|
22
22
|
class Reader(ABC):
|
23
|
-
"""Abstract base class for protobuf file readers"""
|
23
|
+
"""Abstract base class for protobuf file readers."""
|
24
24
|
|
25
25
|
_ERR_FILE_READ = "Could not read file content."
|
26
26
|
_ERR_HEADER_UNRECOGNISED = ""
|
@@ -55,8 +55,7 @@ class Reader(ABC):
|
|
55
55
|
|
56
56
|
@abstractmethod
|
57
57
|
def read(self) -> list[DataStorage]:
|
58
|
-
"""
|
59
|
-
Reads associated filestream and returns one or multiple DataStorage(s) or empty list
|
58
|
+
"""Reads associated filestream and returns one or multiple DataStorage(s) or empty list.
|
60
59
|
|
61
60
|
Returns:
|
62
61
|
one or multiple DataStorage protobuf object(s) read from file
|
@@ -67,8 +66,7 @@ class Reader(ABC):
|
|
67
66
|
|
68
67
|
@staticmethod
|
69
68
|
def get_reader(file: IO, read_single: bool = False) -> Reader:
|
70
|
-
"""
|
71
|
-
Returns reader matching the given file header
|
69
|
+
"""Returns reader matching the given file header.
|
72
70
|
|
73
71
|
Args:
|
74
72
|
file: to be read by the returned Reader
|
@@ -99,7 +97,7 @@ class Reader(ABC):
|
|
99
97
|
|
100
98
|
@final
|
101
99
|
def _read_message_length(self) -> int:
|
102
|
-
"""Returns length of next DataStorage message in file"""
|
100
|
+
"""Returns length of next DataStorage message in file."""
|
103
101
|
message_length_byte = self._file.read(self.BYTES_DEFINING_MESSAGE_LENGTH)
|
104
102
|
if not message_length_byte:
|
105
103
|
log().debug(self._DEBUG_FILE_END_REACHED)
|
@@ -110,8 +108,8 @@ class Reader(ABC):
|
|
110
108
|
|
111
109
|
@final
|
112
110
|
def _read_data_storage_message(self, message_length: int | None = None) -> DataStorage:
|
113
|
-
"""
|
114
|
-
|
111
|
+
"""Returns data storage read from current file position and following `message_length` bytes.
|
112
|
+
|
115
113
|
If `message_length` is omitted, the rest of the file is read. If no message is found, None is returned.
|
116
114
|
|
117
115
|
Args:
|
@@ -157,7 +155,7 @@ class Reader(ABC):
|
|
157
155
|
|
158
156
|
|
159
157
|
class ReaderV2(Reader):
|
160
|
-
"""Reader class for `fame-core>=2.0` output with header of version v002"""
|
158
|
+
"""Reader class for `fame-core>=2.0` output with header of version v002."""
|
161
159
|
|
162
160
|
def read(self) -> list[DataStorage]:
|
163
161
|
messages = []
|
fameio/output/yaml_writer.py
CHANGED
@@ -14,12 +14,11 @@ _INFO_DESTINATION = "Saving scenario to file at {}"
|
|
14
14
|
|
15
15
|
|
16
16
|
class YamlWriterError(OutputError):
|
17
|
-
"""An error occurred during writing a YAML file"""
|
17
|
+
"""An error occurred during writing a YAML file."""
|
18
18
|
|
19
19
|
|
20
20
|
def data_to_yaml_file(data: dict, file_path: Path) -> None:
|
21
|
-
"""
|
22
|
-
Save the given data to a YAML file at given path
|
21
|
+
"""Save the given data to a YAML file at given path.
|
23
22
|
|
24
23
|
Args:
|
25
24
|
data: to be saved to yaml file
|
fameio/scripts/__init__.py
CHANGED
@@ -5,23 +5,34 @@ from fameio.scripts.convert_results import DEFAULT_CONFIG as DEFAULT_CONVERT_CON
|
|
5
5
|
from fameio.scripts.convert_results import run as convert_results
|
6
6
|
from fameio.scripts.exception import ScriptError
|
7
7
|
from fameio.scripts.make_config import run as make_config
|
8
|
+
from fameio.scripts.reformat import run as reformat
|
8
9
|
from fameio.cli.convert_results import handle_args as handle_convert_results_args
|
9
10
|
from fameio.cli.make_config import handle_args as handle_make_config_args
|
11
|
+
from fameio.cli.reformat import handle_args as handle_reformat_args
|
10
12
|
|
11
13
|
|
12
14
|
# noinspection PyPep8Naming
|
13
15
|
def makeFameRunConfig():
|
14
|
-
|
16
|
+
cli_config = handle_make_config_args(sys.argv[1:])
|
15
17
|
try:
|
16
|
-
make_config(
|
18
|
+
make_config(cli_config)
|
17
19
|
except ScriptError as e:
|
18
20
|
raise SystemExit(1) from e
|
19
21
|
|
20
22
|
|
21
23
|
# noinspection PyPep8Naming
|
22
24
|
def convertFameResults():
|
23
|
-
|
25
|
+
cli_config = handle_convert_results_args(sys.argv[1:], DEFAULT_CONVERT_CONFIG)
|
24
26
|
try:
|
25
|
-
convert_results(
|
27
|
+
convert_results(cli_config)
|
28
|
+
except ScriptError as e:
|
29
|
+
raise SystemExit(1) from e
|
30
|
+
|
31
|
+
|
32
|
+
# noinspection PyPep8Naming
|
33
|
+
def reformatTimeSeries():
|
34
|
+
cli_config = handle_reformat_args(sys.argv[1:])
|
35
|
+
try:
|
36
|
+
reformat(cli_config)
|
26
37
|
except ScriptError as e:
|
27
38
|
raise SystemExit(1) from e
|
@@ -17,6 +17,7 @@ from fameio.output.agent_type import AgentTypeLog
|
|
17
17
|
from fameio.output.conversion import apply_time_option, apply_time_merging
|
18
18
|
from fameio.output.csv_writer import CsvWriter
|
19
19
|
from fameio.output.data_transformer import DataTransformer, INDEX
|
20
|
+
from fameio.output.execution_dao import ExecutionDao
|
20
21
|
from fameio.output.input_dao import InputDao
|
21
22
|
from fameio.output.output_dao import OutputDAO
|
22
23
|
from fameio.output.reader import Reader
|
@@ -26,7 +27,7 @@ from fameio.scripts.exception import ScriptError
|
|
26
27
|
_ERR_OUT_OF_MEMORY = "Out of memory. Retry result conversion using `-m` or `--memory-saving` option."
|
27
28
|
_ERR_MEMORY_SEVERE = "Out of memory despite memory-saving mode. Reduce output interval in `FAME-Core` and rerun model."
|
28
29
|
_ERR_FILE_OPEN_FAIL = "Could not open file: '{}'"
|
29
|
-
_ERR_RECOVER_INPUT = "
|
30
|
+
_ERR_RECOVER_INPUT = "Input recovery failed: File was created with `fameio=={}`. Use that version to recover inputs."
|
30
31
|
_ERR_FAIL = "Results conversion script failed."
|
31
32
|
|
32
33
|
_WARN_OUTPUT_SUPPRESSED = "All output data suppressed by agent filter, but there is data available for agent types: {}"
|
@@ -35,8 +36,7 @@ _INFO_MEMORY_SAVING = "Memory saving mode enabled: Disable on conversion of smal
|
|
35
36
|
|
36
37
|
|
37
38
|
def _read_and_extract_data(config: dict[Options, Any]) -> None:
|
38
|
-
"""
|
39
|
-
Read protobuf file, extracts, converts, and saves the converted data; Returns false if no result data was found
|
39
|
+
"""Read protobuf file, extracts, converts, and saves the converted data.
|
40
40
|
|
41
41
|
Args:
|
42
42
|
config: script configuration options
|
@@ -54,8 +54,7 @@ def _read_and_extract_data(config: dict[Options, Any]) -> None:
|
|
54
54
|
|
55
55
|
|
56
56
|
def _extract_and_convert_data(config: dict[Options, Any], file_stream: BinaryIO, file_path: Path) -> None:
|
57
|
-
"""
|
58
|
-
Extracts data from provided input file stream, converts it, and writes the result to output files
|
57
|
+
"""Extracts data from provided input file stream, converts it, and writes the result to output files.
|
59
58
|
|
60
59
|
Args:
|
61
60
|
config: script configuration options
|
@@ -71,7 +70,9 @@ def _extract_and_convert_data(config: dict[Options, Any], file_stream: BinaryIO,
|
|
71
70
|
data_transformer = DataTransformer.build(config[Options.RESOLVE_COMPLEX_FIELD])
|
72
71
|
reader = Reader.get_reader(file=file_stream, read_single=config[Options.MEMORY_SAVING])
|
73
72
|
input_dao = InputDao()
|
73
|
+
execution_dao = ExecutionDao()
|
74
74
|
while data_storages := reader.read():
|
75
|
+
execution_dao.store_execution_metadata(data_storages)
|
75
76
|
if config[Options.INPUT_RECOVERY]:
|
76
77
|
input_dao.store_inputs(data_storages)
|
77
78
|
output = OutputDAO(data_storages, agent_type_log)
|
@@ -85,7 +86,7 @@ def _extract_and_convert_data(config: dict[Options, Any], file_stream: BinaryIO,
|
|
85
86
|
output_writer.write_to_files(agent_name, data_frames)
|
86
87
|
|
87
88
|
if config[Options.INPUT_RECOVERY]:
|
88
|
-
_recover_inputs(config, input_dao)
|
89
|
+
_recover_inputs(config, input_dao, execution_dao.get_fameio_version())
|
89
90
|
if config[Options.MEMORY_SAVING]:
|
90
91
|
_memory_saving_apply_conversions(config, output_writer)
|
91
92
|
|
@@ -97,13 +98,13 @@ def _extract_and_convert_data(config: dict[Options, Any], file_stream: BinaryIO,
|
|
97
98
|
log().info("Data conversion completed.")
|
98
99
|
|
99
100
|
|
100
|
-
def _recover_inputs(config: dict[Options, Any], input_dao: InputDao) -> None:
|
101
|
-
"""
|
102
|
-
Reads scenario configuration from provided input_dao
|
101
|
+
def _recover_inputs(config: dict[Options, Any], input_dao: InputDao, fameio_version: str) -> None:
|
102
|
+
"""Reads scenario configuration from provided `input_dao`.
|
103
103
|
|
104
104
|
Args:
|
105
105
|
config: script configuration options
|
106
106
|
input_dao: to recover the input data from
|
107
|
+
fameio_version: version of fameio that was used to create the input data
|
107
108
|
|
108
109
|
Raises:
|
109
110
|
OutputError: if inputs could not be recovered or saved to files, logged with level "ERROR"
|
@@ -112,18 +113,19 @@ def _recover_inputs(config: dict[Options, Any], input_dao: InputDao) -> None:
|
|
112
113
|
try:
|
113
114
|
timeseries, scenario = input_dao.recover_inputs()
|
114
115
|
except InputError as ex:
|
115
|
-
raise log_error(OutputError(_ERR_RECOVER_INPUT)) from ex
|
116
|
+
raise log_error(OutputError(_ERR_RECOVER_INPUT.format(fameio_version))) from ex
|
116
117
|
base_path = config[Options.OUTPUT] if config[Options.OUTPUT] is not None else "./"
|
117
118
|
series_writer = CsvWriter(
|
118
119
|
config_output=Path(base_path, "./recovered"), input_file_path=Path("./"), single_export=False
|
119
120
|
)
|
120
|
-
series_writer.
|
121
|
+
series_writer.write_all_time_series_to_disk(timeseries)
|
121
122
|
data_to_yaml_file(scenario.to_dict(), Path(base_path, "./recovered/scenario.yaml"))
|
122
123
|
|
123
124
|
|
124
125
|
def _memory_saving_apply_conversions(config: dict[Options, Any], output_writer: CsvWriter) -> None:
|
125
|
-
"""
|
126
|
-
|
126
|
+
"""Rewrite result files: applies time-merging and time conversion options on a per-file basis.
|
127
|
+
|
128
|
+
This is only required in memory saving mode.
|
127
129
|
|
128
130
|
Args:
|
129
131
|
config: script configuration options
|
@@ -142,8 +144,7 @@ def _memory_saving_apply_conversions(config: dict[Options, Any], output_writer:
|
|
142
144
|
|
143
145
|
|
144
146
|
def run(config: dict[Options, Any] | None = None) -> None:
|
145
|
-
"""
|
146
|
-
Reads configured file in protobuf format and extracts its content to .CSV and .YAML file(s)
|
147
|
+
"""Reads configured file in protobuf format and extracts its content to .CSV and .YAML file(s).
|
147
148
|
|
148
149
|
Args:
|
149
150
|
config: script configuration options
|
@@ -167,8 +168,8 @@ def run(config: dict[Options, Any] | None = None) -> None:
|
|
167
168
|
|
168
169
|
|
169
170
|
if __name__ == "__main__":
|
170
|
-
|
171
|
+
cli_config = handle_args(sys.argv[1:])
|
171
172
|
try:
|
172
|
-
run(
|
173
|
+
run(cli_config)
|
173
174
|
except ScriptError as e:
|
174
175
|
raise SystemExit(1) from e
|
fameio/scripts/exception.py
CHANGED
fameio/scripts/make_config.py
CHANGED
@@ -20,8 +20,7 @@ _ERR_FAIL: str = "Creation of run configuration file failed."
|
|
20
20
|
|
21
21
|
|
22
22
|
def run(config: dict[Options, Any] | None = None) -> None:
|
23
|
-
"""
|
24
|
-
Executes the main workflow of building a FAME configuration file
|
23
|
+
"""Executes the main workflow of building a FAME configuration file.
|
25
24
|
|
26
25
|
Args:
|
27
26
|
config: configuration options
|
@@ -49,8 +48,8 @@ def run(config: dict[Options, Any] | None = None) -> None:
|
|
49
48
|
|
50
49
|
|
51
50
|
if __name__ == "__main__":
|
52
|
-
|
51
|
+
cli_config = handle_args(sys.argv[1:])
|
53
52
|
try:
|
54
|
-
run(
|
53
|
+
run(cli_config)
|
55
54
|
except ScriptError as e:
|
56
55
|
raise SystemExit(1) from e
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import sys
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from fameio.cli import update_default_config
|
9
|
+
from fameio.cli.options import Options
|
10
|
+
from fameio.cli.reformat import handle_args, CLI_DEFAULTS as DEFAULT_CONFIG
|
11
|
+
from fameio.logs import fameio_logger, log_error, log_and_print
|
12
|
+
from fameio.output.csv_writer import CsvWriter, CsvWriterError
|
13
|
+
from fameio.scripts.exception import ScriptError
|
14
|
+
from fameio.series import TimeSeriesManager, TimeSeriesError
|
15
|
+
from fameio.tools import get_csv_files_with_pattern, extend_file_name
|
16
|
+
|
17
|
+
FILE_NAME_APPENDIX = "_reformatted"
|
18
|
+
|
19
|
+
_ERR_FAIL = "Timeseries reformatting script failed."
|
20
|
+
_ERR_NO_FILES = "No file found matching this pattern: '{}'"
|
21
|
+
_ERR_FILE_CONVERSION = "Could not reformat file: '{}'"
|
22
|
+
|
23
|
+
|
24
|
+
def reformat_file(file: Path, replace: bool) -> None:
|
25
|
+
"""Transforms content of specified CSV file to FAME format.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
file: whose content is to be reformatted
|
29
|
+
replace: if true, original file will be replaced; otherwise, a new file will be created instead
|
30
|
+
|
31
|
+
Raises:
|
32
|
+
ScriptError: if file could not be read, file reformatting failed, or result file could not be written;
|
33
|
+
logged with level "ERROR"
|
34
|
+
"""
|
35
|
+
try:
|
36
|
+
data = TimeSeriesManager.read_timeseries_file(file)
|
37
|
+
data = TimeSeriesManager.check_and_convert_series(data, str(file), warn=False)
|
38
|
+
target_path = file if replace else extend_file_name(file, FILE_NAME_APPENDIX)
|
39
|
+
CsvWriter.write_single_time_series_to_disk(data, target_path)
|
40
|
+
except (TimeSeriesError, CsvWriterError) as ex:
|
41
|
+
raise log_error(ScriptError(_ERR_FILE_CONVERSION.format(file))) from ex
|
42
|
+
|
43
|
+
|
44
|
+
def run(config: dict[Options, Any] | None = None) -> None:
|
45
|
+
"""Executes the workflow of transforming time series file(s).
|
46
|
+
|
47
|
+
Args:
|
48
|
+
config: configuration options
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
ScriptError: if no file could be found, or if any file could not be transformed, logged with level "ERROR"
|
52
|
+
"""
|
53
|
+
config = update_default_config(config, DEFAULT_CONFIG)
|
54
|
+
fameio_logger(log_level_name=config[Options.LOG_LEVEL], file_name=config[Options.LOG_FILE])
|
55
|
+
try:
|
56
|
+
files = get_csv_files_with_pattern(Path("."), config[Options.FILE_PATTERN])
|
57
|
+
except ValueError as ex:
|
58
|
+
raise log_error(ScriptError(_ERR_NO_FILES.format(config[Options.FILE_PATTERN]))) from ex
|
59
|
+
if not files:
|
60
|
+
raise log_error(ScriptError(_ERR_NO_FILES.format(config[Options.FILE_PATTERN])))
|
61
|
+
for file in files:
|
62
|
+
log_and_print(f"Reformatting file: {file}")
|
63
|
+
reformat_file(file, config[Options.REPLACE])
|
64
|
+
|
65
|
+
|
66
|
+
if __name__ == "__main__":
|
67
|
+
cli_config = handle_args(sys.argv[1:])
|
68
|
+
try:
|
69
|
+
run(cli_config)
|
70
|
+
except ScriptError as e:
|
71
|
+
raise SystemExit(1) from e
|
fameio/series.py
CHANGED
@@ -18,9 +18,9 @@ from fameio.input.resolver import PathResolver
|
|
18
18
|
from fameio.logs import log, log_error
|
19
19
|
from fameio.output import OutputError
|
20
20
|
from fameio.time import ConversionError, FameTime
|
21
|
-
from fameio.tools import clean_up_file_name
|
21
|
+
from fameio.tools import clean_up_file_name, CSV_FILE_SUFFIX
|
22
|
+
|
22
23
|
|
23
|
-
CSV_FILE_SUFFIX = ".csv"
|
24
24
|
FILE_LENGTH_WARN_LIMIT = int(50e3)
|
25
25
|
|
26
26
|
|
@@ -35,7 +35,7 @@ class Entry(Enum):
|
|
35
35
|
|
36
36
|
|
37
37
|
class TimeSeriesManager:
|
38
|
-
"""Manages matching of files to time series ids and their protobuf representation"""
|
38
|
+
"""Manages matching of files to time series ids and their protobuf representation."""
|
39
39
|
|
40
40
|
_TIMESERIES_RECONSTRUCTION_PATH = "./timeseries/"
|
41
41
|
_CONSTANT_IDENTIFIER = "Constant value: {}"
|
@@ -44,14 +44,14 @@ class TimeSeriesManager:
|
|
44
44
|
|
45
45
|
_ERR_FILE_NOT_FOUND = "Cannot find Timeseries file '{}'."
|
46
46
|
_ERR_NUMERIC_STRING = " Remove quotes to use a constant numeric value instead of a timeseries file."
|
47
|
-
|
48
|
-
|
47
|
+
_ERR_CORRUPT_KEYS = "TimeSeries file '{}' corrupt: At least one entry in first column isn't a timestamp."
|
48
|
+
_ERR_CORRUPT_VALUES = "TimeSeries file '{}' corrupt: At least one entry in second column isn't numeric."
|
49
49
|
_ERR_NON_NUMERIC = "Values in TimeSeries must be numeric but was: '{}'"
|
50
50
|
_ERR_NAN_VALUE = "Values in TimeSeries must not be missing or NaN."
|
51
51
|
_ERR_UNREGISTERED_SERIES = "No timeseries registered with identifier '{}' - was the Scenario validated?"
|
52
52
|
_ERR_UNREGISTERED_SERIES_RE = "No timeseries registered with identifier '{}' - were the timeseries reconstructed?"
|
53
53
|
_WARN_NO_DATA = "No timeseries stored in timeseries manager. Double check if you expected timeseries."
|
54
|
-
_WARN_DATA_IGNORED = "Timeseries contains additional columns with data which will be ignored."
|
54
|
+
_WARN_DATA_IGNORED = "Timeseries '{}' contains additional columns with data which will be ignored."
|
55
55
|
_WARN_LARGE_CONVERSION = (
|
56
56
|
"Timeseries file '{}' is large and needs conversion of time stamps. If performance "
|
57
57
|
"issues occur and the file is reused, convert the time stamp column once with "
|
@@ -64,8 +64,7 @@ class TimeSeriesManager:
|
|
64
64
|
self._series_by_id: dict[str | int | float, dict[Entry, Any]] = {}
|
65
65
|
|
66
66
|
def register_and_validate(self, identifier: str | int | float) -> None:
|
67
|
-
"""
|
68
|
-
Registers given timeseries `identifier` and validates associated timeseries
|
67
|
+
"""Registers given timeseries `identifier` and validates associated timeseries.
|
69
68
|
|
70
69
|
Args:
|
71
70
|
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
@@ -78,12 +77,11 @@ class TimeSeriesManager:
|
|
78
77
|
self._register_time_series(identifier)
|
79
78
|
|
80
79
|
def _time_series_is_registered(self, identifier: str | int | float) -> bool:
|
81
|
-
"""Returns True if the value was already registered"""
|
80
|
+
"""Returns True if the value was already registered."""
|
82
81
|
return identifier in self._series_by_id
|
83
82
|
|
84
83
|
def _register_time_series(self, identifier: str | int | float) -> None:
|
85
|
-
"""
|
86
|
-
Assigns an id to the given `identifier` and loads the time series into a dataframe
|
84
|
+
"""Assigns an id to the given `identifier` and loads the time series into a dataframe.
|
87
85
|
|
88
86
|
Args:
|
89
87
|
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
@@ -97,8 +95,7 @@ class TimeSeriesManager:
|
|
97
95
|
self._series_by_id[identifier] = {Entry.ID: self._id_count, Entry.NAME: name, Entry.DATA: series}
|
98
96
|
|
99
97
|
def _get_name_and_dataframe(self, identifier: str | int | float) -> tuple[str, pd.DataFrame]:
|
100
|
-
"""
|
101
|
-
Returns name and DataFrame containing the series obtained from the given `identifier`
|
98
|
+
"""Returns name and DataFrame containing the series obtained from the given `identifier`.
|
102
99
|
|
103
100
|
Args:
|
104
101
|
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
@@ -113,56 +110,92 @@ class TimeSeriesManager:
|
|
113
110
|
if isinstance(identifier, str):
|
114
111
|
series_path = self._path_resolver.resolve_series_file_path(Path(identifier).as_posix())
|
115
112
|
if series_path and os.path.exists(series_path):
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
message = self._ERR_FILE_NOT_FOUND.format(identifier)
|
128
|
-
if self._is_number_string(identifier):
|
129
|
-
message += self._ERR_NUMERIC_STRING
|
130
|
-
raise log_error(TimeSeriesError(message))
|
131
|
-
else:
|
132
|
-
return self._create_timeseries_from_value(identifier)
|
113
|
+
data = self.read_timeseries_file(series_path)
|
114
|
+
return identifier, self.check_and_convert_series(data, identifier)
|
115
|
+
message = self._ERR_FILE_NOT_FOUND.format(identifier)
|
116
|
+
if self._is_number_string(identifier):
|
117
|
+
message += self._ERR_NUMERIC_STRING
|
118
|
+
raise log_error(TimeSeriesError(message))
|
119
|
+
return self._create_timeseries_from_value(identifier)
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def read_timeseries_file(file: Path | str) -> pd.DataFrame:
|
123
|
+
"""Loads a timeseries from file.
|
133
124
|
|
134
|
-
|
125
|
+
Args:
|
126
|
+
file: to be read
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
data frame obtained from file
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
TimeSeriesError: if file could not be read, logged with level "ERROR"
|
135
133
|
"""
|
136
|
-
|
134
|
+
try:
|
135
|
+
return pd.read_csv(file, sep=";", header=None, comment="#")
|
136
|
+
except OSError as e:
|
137
|
+
raise log_error(TimeSeriesError(e)) from e
|
138
|
+
|
139
|
+
@staticmethod
|
140
|
+
def check_and_convert_series(data: pd.DataFrame, file_name: str, warn: bool = True) -> pd.DataFrame:
|
141
|
+
"""Ensures validity of time series and convert to required format for writing to disk.
|
137
142
|
|
138
143
|
Args:
|
139
144
|
data: dataframe to be converted to expected format
|
145
|
+
file_name: used in warnings and errors
|
146
|
+
warn: if True, a warning is raised if large files require conversion (default: True)
|
140
147
|
|
141
148
|
Returns:
|
142
149
|
2-column dataframe, first column: integers, second column: floats (no NaN)
|
143
150
|
|
151
|
+
Raises:
|
152
|
+
TimeSeriesError: if the data do not correspond to a valid time series, logged with level "ERROR"
|
153
|
+
"""
|
154
|
+
try:
|
155
|
+
converted, large_conversion = TimeSeriesManager._check_and_convert_series(data, file_name)
|
156
|
+
if large_conversion and warn:
|
157
|
+
log().warning(TimeSeriesManager._WARN_LARGE_CONVERSION.format(file_name))
|
158
|
+
return converted
|
159
|
+
except TypeError as e:
|
160
|
+
raise log_error(TimeSeriesError(TimeSeriesManager._ERR_CORRUPT_VALUES.format(file_name), e)) from e
|
161
|
+
except ConversionError as e:
|
162
|
+
raise log_error(TimeSeriesError(TimeSeriesManager._ERR_CORRUPT_KEYS.format(file_name), e)) from e
|
163
|
+
|
164
|
+
@staticmethod
|
165
|
+
def _check_and_convert_series(data: pd.DataFrame, file_name: str) -> tuple[pd.DataFrame, bool]:
|
166
|
+
"""Ensures validity of time series and convert to required format for writing to disk.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
data: dataframe to be converted to expected format
|
170
|
+
file_name: used in warnings
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
tuple of 1) dataframe and 2) large conversion indicator:
|
174
|
+
2-column dataframe first column: integers, second column: floats (no NaN)
|
175
|
+
large conversion indicator: if true, the timeseries was large and required conversion
|
176
|
+
|
144
177
|
Raises:
|
145
178
|
ConversionError: if first data column could not be converted to integer, logged with level "ERROR"
|
146
179
|
TypeError: if second data column in given data could not be converted to float or contained NaN,
|
147
180
|
logged with level "ERROR"
|
148
181
|
"""
|
182
|
+
large_file_indicator = False
|
149
183
|
data, additional_columns = data.loc[:, :2], data.loc[:, 2:]
|
150
184
|
if not additional_columns.dropna(how="all").empty:
|
151
|
-
log().warning(
|
185
|
+
log().warning(TimeSeriesManager._WARN_DATA_IGNORED.format(file_name))
|
152
186
|
if data.dtypes[0] != "int64":
|
153
187
|
if len(data[0]) > FILE_LENGTH_WARN_LIMIT:
|
154
|
-
|
188
|
+
large_file_indicator = True
|
155
189
|
data[0] = [FameTime.convert_string_if_is_datetime(time) for time in data[0]]
|
156
190
|
if data.dtypes[1] != "float64":
|
157
191
|
data[1] = [TimeSeriesManager._assert_float(value) for value in data[1]]
|
158
192
|
if data[1].isna().any():
|
159
193
|
raise log_error(TypeError(TimeSeriesManager._ERR_NAN_VALUE))
|
160
|
-
return data
|
194
|
+
return data, large_file_indicator
|
161
195
|
|
162
196
|
@staticmethod
|
163
197
|
def _assert_float(value: Any) -> float:
|
164
|
-
"""
|
165
|
-
Converts any given value to a float or raise an Exception
|
198
|
+
"""Converts any given value to a float or raise an Exception.
|
166
199
|
|
167
200
|
Args:
|
168
201
|
value: to be converted to float
|
@@ -181,7 +214,7 @@ class TimeSeriesManager:
|
|
181
214
|
|
182
215
|
@staticmethod
|
183
216
|
def _is_number_string(identifier: str) -> bool:
|
184
|
-
"""Returns True if given identifier can be cast to float"""
|
217
|
+
"""Returns True if given identifier can be cast to float."""
|
185
218
|
try:
|
186
219
|
float(identifier)
|
187
220
|
return True
|
@@ -190,8 +223,7 @@ class TimeSeriesManager:
|
|
190
223
|
|
191
224
|
@staticmethod
|
192
225
|
def _create_timeseries_from_value(value: int | float) -> tuple[str, pd.DataFrame]:
|
193
|
-
"""
|
194
|
-
Returns name and dataframe for a new static timeseries created from the given `value`
|
226
|
+
"""Returns name and dataframe for a new static timeseries created from the given `value`.
|
195
227
|
|
196
228
|
Args:
|
197
229
|
value: the static value of the timeseries to be created
|
@@ -208,8 +240,7 @@ class TimeSeriesManager:
|
|
208
240
|
return TimeSeriesManager._CONSTANT_IDENTIFIER.format(value), data
|
209
241
|
|
210
242
|
def get_series_id_by_identifier(self, identifier: str | int | float) -> int:
|
211
|
-
"""
|
212
|
-
Returns id for a previously stored time series by given `identifier`
|
243
|
+
"""Returns id for a previously stored time series by given `identifier`.
|
213
244
|
|
214
245
|
Args:
|
215
246
|
identifier: to get the unique ID for
|
@@ -225,13 +256,13 @@ class TimeSeriesManager:
|
|
225
256
|
return self._series_by_id.get(identifier)[Entry.ID] # type: ignore[index]
|
226
257
|
|
227
258
|
def get_all_series(self) -> list[tuple[int, str, pd.DataFrame]]:
|
228
|
-
"""Returns iterator over id, name and dataframe of all stored series"""
|
259
|
+
"""Returns iterator over id, name and dataframe of all stored series."""
|
229
260
|
if len(self._series_by_id) == 0:
|
230
261
|
log().warning(self._WARN_NO_DATA)
|
231
262
|
return [(v[Entry.ID], v[Entry.NAME], v[Entry.DATA]) for v in self._series_by_id.values()]
|
232
263
|
|
233
264
|
def reconstruct_time_series(self, timeseries: list[InputData.TimeSeriesDao]) -> None:
|
234
|
-
"""Reconstructs and stores time series from given list of `timeseries_dao
|
265
|
+
"""Reconstructs and stores time series from given list of `timeseries_dao`."""
|
235
266
|
for one_series in timeseries:
|
236
267
|
self._id_count += 1
|
237
268
|
reconstructed = {Entry.ID: one_series.series_id}
|
@@ -248,7 +279,7 @@ class TimeSeriesManager:
|
|
248
279
|
self._series_by_id[one_series.series_id] = reconstructed
|
249
280
|
|
250
281
|
def _get_cleaned_file_name(self, timeseries_name: str) -> str:
|
251
|
-
"""Ensure given file name has CSV file ending"""
|
282
|
+
"""Ensure given file name has CSV file ending."""
|
252
283
|
if Path(timeseries_name).suffix.lower() == CSV_FILE_SUFFIX:
|
253
284
|
filename = Path(timeseries_name).name
|
254
285
|
else:
|
@@ -256,8 +287,8 @@ class TimeSeriesManager:
|
|
256
287
|
return str(Path(self._TIMESERIES_RECONSTRUCTION_PATH, filename))
|
257
288
|
|
258
289
|
def get_reconstructed_series_by_id(self, series_id: int) -> str:
|
259
|
-
"""
|
260
|
-
|
290
|
+
"""Return name or path for given `series_id` if series these are identified by their number.
|
291
|
+
|
261
292
|
Use this only if series were added via `reconstruct_time_series`
|
262
293
|
|
263
294
|
Args:
|