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.
Files changed (53) hide show
  1. fameio/cli/convert_results.py +4 -6
  2. fameio/cli/make_config.py +3 -5
  3. fameio/cli/options.py +6 -4
  4. fameio/cli/parser.py +53 -29
  5. fameio/cli/reformat.py +58 -0
  6. fameio/input/__init__.py +4 -4
  7. fameio/input/loader/__init__.py +4 -6
  8. fameio/input/loader/controller.py +11 -16
  9. fameio/input/loader/loader.py +11 -9
  10. fameio/input/metadata.py +26 -29
  11. fameio/input/resolver.py +4 -6
  12. fameio/input/scenario/agent.py +18 -16
  13. fameio/input/scenario/attribute.py +85 -31
  14. fameio/input/scenario/contract.py +78 -38
  15. fameio/input/scenario/exception.py +3 -6
  16. fameio/input/scenario/fameiofactory.py +7 -12
  17. fameio/input/scenario/generalproperties.py +7 -8
  18. fameio/input/scenario/scenario.py +14 -18
  19. fameio/input/scenario/stringset.py +5 -6
  20. fameio/input/schema/agenttype.py +8 -10
  21. fameio/input/schema/attribute.py +30 -36
  22. fameio/input/schema/java_packages.py +6 -7
  23. fameio/input/schema/schema.py +9 -11
  24. fameio/input/validator.py +178 -41
  25. fameio/input/writer.py +20 -29
  26. fameio/logs.py +28 -19
  27. fameio/output/agent_type.py +14 -16
  28. fameio/output/conversion.py +9 -12
  29. fameio/output/csv_writer.py +33 -23
  30. fameio/output/data_transformer.py +11 -11
  31. fameio/output/execution_dao.py +170 -0
  32. fameio/output/input_dao.py +16 -19
  33. fameio/output/output_dao.py +7 -7
  34. fameio/output/reader.py +8 -10
  35. fameio/output/yaml_writer.py +2 -3
  36. fameio/scripts/__init__.py +15 -4
  37. fameio/scripts/convert_results.py +18 -17
  38. fameio/scripts/exception.py +1 -1
  39. fameio/scripts/make_config.py +3 -4
  40. fameio/scripts/reformat.py +71 -0
  41. fameio/scripts/reformat.py.license +3 -0
  42. fameio/series.py +78 -47
  43. fameio/time.py +56 -18
  44. fameio/tools.py +42 -4
  45. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/METADATA +64 -40
  46. fameio-3.4.0.dist-info/RECORD +60 -0
  47. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/entry_points.txt +1 -0
  48. fameio-3.2.0.dist-info/RECORD +0 -56
  49. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSE.txt +0 -0
  50. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  51. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  52. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  53. {fameio-3.2.0.dist-info → fameio-3.4.0.dist-info}/WHEEL +0 -0
@@ -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
- Returns DataFrame(s) containing all data of given `agent` - data is removed after the first call
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
- Returns given `data_storage` read from current file position and following `message_length` bytes.
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 = []
@@ -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
@@ -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
- run_config = handle_make_config_args(sys.argv[1:])
16
+ cli_config = handle_make_config_args(sys.argv[1:])
15
17
  try:
16
- make_config(run_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
- run_config = handle_convert_results_args(sys.argv[1:], DEFAULT_CONVERT_CONFIG)
25
+ cli_config = handle_convert_results_args(sys.argv[1:], DEFAULT_CONVERT_CONFIG)
24
26
  try:
25
- convert_results(run_config)
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 = "Could not recover inputs due to an incompatibility with this version of fameio."
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.write_time_series_to_disk(timeseries)
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
- Rewrite result files in memory saving mode: apply time-merging and time conversion options on a per-file basis
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
- run_config = handle_args(sys.argv[1:])
171
+ cli_config = handle_args(sys.argv[1:])
171
172
  try:
172
- run(run_config)
173
+ run(cli_config)
173
174
  except ScriptError as e:
174
175
  raise SystemExit(1) from e
@@ -4,4 +4,4 @@
4
4
 
5
5
 
6
6
  class ScriptError(Exception):
7
- """Any kind of expected error that occurred during execution of FAME-Io scripts"""
7
+ """Any kind of expected error that occurred during execution of FAME-Io scripts."""
@@ -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
- run_config = handle_args(sys.argv[1:])
51
+ cli_config = handle_args(sys.argv[1:])
53
52
  try:
54
- run(run_config)
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
@@ -0,0 +1,3 @@
1
+ SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
2
+
3
+ SPDX-License-Identifier: Apache-2.0
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
- _ERR_CORRUPT_TIME_SERIES_KEY = "TimeSeries file '{}' corrupt: At least one entry in first column isn't a timestamp."
48
- _ERR_CORRUPT_TIME_SERIES_VALUE = "TimeSeries file '{}' corrupt: At least one entry in second column isn't numeric."
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
- try:
117
- data = pd.read_csv(series_path, sep=";", header=None, comment="#")
118
- except OSError as e:
119
- raise log_error(TimeSeriesError(e)) from e
120
- try:
121
- return identifier, self._check_and_convert_series(data, identifier)
122
- except TypeError as e:
123
- raise log_error(TimeSeriesError(self._ERR_CORRUPT_TIME_SERIES_VALUE.format(identifier), e)) from e
124
- except ConversionError as e:
125
- raise log_error(TimeSeriesError(self._ERR_CORRUPT_TIME_SERIES_KEY.format(identifier), e)) from e
126
- else:
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
- def _check_and_convert_series(self, data: pd.DataFrame, identifier: str) -> pd.DataFrame:
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
- Ensures validity of time series and convert to required format for writing to disk
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(self._WARN_DATA_IGNORED)
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
- log().warning(self._WARN_LARGE_CONVERSION.format(identifier))
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
- Return name or path for given `series_id` if series these are identified by their number.
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: