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/scripts/make_config.py
CHANGED
@@ -1,34 +1,56 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
|
+
from __future__ import annotations
|
3
|
+
|
2
4
|
import sys
|
3
5
|
from pathlib import Path
|
6
|
+
from typing import Any
|
4
7
|
|
8
|
+
from fameio.cli import update_default_config
|
5
9
|
from fameio.cli.make_config import handle_args, CLI_DEFAULTS as DEFAULT_CONFIG
|
6
10
|
from fameio.cli.options import Options
|
7
|
-
from fameio.
|
11
|
+
from fameio.input import InputError
|
8
12
|
from fameio.input.loader import load_yaml, validate_yaml_file_suffix
|
9
|
-
from fameio.logs import fameio_logger, log
|
10
13
|
from fameio.input.scenario import Scenario
|
11
14
|
from fameio.input.validator import SchemaValidator
|
12
15
|
from fameio.input.writer import ProtoWriter
|
16
|
+
from fameio.logs import fameio_logger, log, log_critical
|
17
|
+
from fameio.scripts.exception import ScriptError
|
18
|
+
|
19
|
+
_ERR_FAIL: str = "Creation of run configuration file failed."
|
20
|
+
|
21
|
+
|
22
|
+
def run(config: dict[Options, Any] | None = None) -> None:
|
23
|
+
"""
|
24
|
+
Executes the main workflow of building a FAME configuration file
|
13
25
|
|
26
|
+
Args:
|
27
|
+
config: configuration options
|
14
28
|
|
15
|
-
|
16
|
-
|
29
|
+
Raises:
|
30
|
+
ScriptError: if any kind of expected error occurred, logged with level "CRITICAL"
|
31
|
+
"""
|
17
32
|
config = update_default_config(config, DEFAULT_CONFIG)
|
18
33
|
fameio_logger(log_level_name=config[Options.LOG_LEVEL], file_name=config[Options.LOG_FILE])
|
19
34
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
35
|
+
try:
|
36
|
+
file = config[Options.FILE]
|
37
|
+
validate_yaml_file_suffix(Path(file))
|
38
|
+
scenario_definition = load_yaml(Path(file), encoding=config[Options.INPUT_ENCODING])
|
39
|
+
scenario = Scenario.from_dict(scenario_definition)
|
40
|
+
SchemaValidator.check_agents_have_contracts(scenario)
|
24
41
|
|
25
|
-
|
26
|
-
|
27
|
-
|
42
|
+
timeseries_manager = SchemaValidator.validate_scenario_and_timeseries(scenario)
|
43
|
+
writer = ProtoWriter(config[Options.OUTPUT], timeseries_manager)
|
44
|
+
writer.write_validated_scenario(scenario)
|
45
|
+
except InputError as ex:
|
46
|
+
raise log_critical(ScriptError(_ERR_FAIL)) from ex
|
28
47
|
|
29
48
|
log().info("Configuration completed.")
|
30
49
|
|
31
50
|
|
32
51
|
if __name__ == "__main__":
|
33
52
|
run_config = handle_args(sys.argv[1:])
|
34
|
-
|
53
|
+
try:
|
54
|
+
run(run_config)
|
55
|
+
except ScriptError as e:
|
56
|
+
raise SystemExit(1) from e
|
fameio/series.py
CHANGED
@@ -1,26 +1,30 @@
|
|
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
|
5
|
+
|
4
6
|
import math
|
5
7
|
import os
|
6
8
|
from enum import Enum, auto
|
7
9
|
from pathlib import Path
|
8
|
-
from typing import
|
10
|
+
from typing import Any
|
9
11
|
|
10
12
|
import pandas as pd
|
11
13
|
from fameprotobuf.input_file_pb2 import InputData
|
12
14
|
from google.protobuf.internal.wire_format import INT64_MIN, INT64_MAX
|
13
15
|
|
16
|
+
from fameio.input import InputError
|
14
17
|
from fameio.input.resolver import PathResolver
|
15
18
|
from fameio.logs import log, log_error
|
19
|
+
from fameio.output import OutputError
|
16
20
|
from fameio.time import ConversionError, FameTime
|
17
21
|
from fameio.tools import clean_up_file_name
|
18
22
|
|
19
|
-
|
20
23
|
CSV_FILE_SUFFIX = ".csv"
|
24
|
+
FILE_LENGTH_WARN_LIMIT = int(50e3)
|
21
25
|
|
22
26
|
|
23
|
-
class TimeSeriesError(
|
27
|
+
class TimeSeriesError(InputError, OutputError):
|
24
28
|
"""Indicates that an error occurred during management of time series"""
|
25
29
|
|
26
30
|
|
@@ -41,19 +45,25 @@ class TimeSeriesManager:
|
|
41
45
|
_ERR_FILE_NOT_FOUND = "Cannot find Timeseries file '{}'."
|
42
46
|
_ERR_NUMERIC_STRING = " Remove quotes to use a constant numeric value instead of a timeseries file."
|
43
47
|
_ERR_CORRUPT_TIME_SERIES_KEY = "TimeSeries file '{}' corrupt: At least one entry in first column isn't a timestamp."
|
44
|
-
_ERR_CORRUPT_TIME_SERIES_VALUE = "TimeSeries file '{}' corrupt: At least one entry in
|
48
|
+
_ERR_CORRUPT_TIME_SERIES_VALUE = "TimeSeries file '{}' corrupt: At least one entry in second column isn't numeric."
|
45
49
|
_ERR_NON_NUMERIC = "Values in TimeSeries must be numeric but was: '{}'"
|
46
50
|
_ERR_NAN_VALUE = "Values in TimeSeries must not be missing or NaN."
|
47
51
|
_ERR_UNREGISTERED_SERIES = "No timeseries registered with identifier '{}' - was the Scenario validated?"
|
52
|
+
_ERR_UNREGISTERED_SERIES_RE = "No timeseries registered with identifier '{}' - were the timeseries reconstructed?"
|
48
53
|
_WARN_NO_DATA = "No timeseries stored in timeseries manager. Double check if you expected timeseries."
|
49
54
|
_WARN_DATA_IGNORED = "Timeseries contains additional columns with data which will be ignored."
|
55
|
+
_WARN_LARGE_CONVERSION = (
|
56
|
+
"Timeseries file '{}' is large and needs conversion of time stamps. If performance "
|
57
|
+
"issues occur and the file is reused, convert the time stamp column once with "
|
58
|
+
"`fameio.time.FameTime.convert_datetime_to_fame_time_step(datetime_string)`."
|
59
|
+
)
|
50
60
|
|
51
61
|
def __init__(self, path_resolver: PathResolver = PathResolver()) -> None:
|
52
62
|
self._path_resolver = path_resolver
|
53
63
|
self._id_count = -1
|
54
|
-
self._series_by_id: dict[
|
64
|
+
self._series_by_id: dict[str | int | float, dict[Entry, Any]] = {}
|
55
65
|
|
56
|
-
def register_and_validate(self, identifier:
|
66
|
+
def register_and_validate(self, identifier: str | int | float) -> None:
|
57
67
|
"""
|
58
68
|
Registers given timeseries `identifier` and validates associated timeseries
|
59
69
|
|
@@ -61,29 +71,54 @@ class TimeSeriesManager:
|
|
61
71
|
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
62
72
|
|
63
73
|
Raises:
|
64
|
-
|
74
|
+
TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
|
75
|
+
logged with level "ERROR"
|
65
76
|
"""
|
66
77
|
if not self._time_series_is_registered(identifier):
|
67
78
|
self._register_time_series(identifier)
|
68
79
|
|
69
|
-
def _time_series_is_registered(self, identifier:
|
80
|
+
def _time_series_is_registered(self, identifier: str | int | float) -> bool:
|
70
81
|
"""Returns True if the value was already registered"""
|
71
82
|
return identifier in self._series_by_id
|
72
83
|
|
73
|
-
def _register_time_series(self, identifier:
|
74
|
-
"""
|
84
|
+
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
|
87
|
+
|
88
|
+
Args:
|
89
|
+
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
90
|
+
|
91
|
+
Raises:
|
92
|
+
TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
|
93
|
+
logged with level "ERROR"
|
94
|
+
"""
|
75
95
|
self._id_count += 1
|
76
96
|
name, series = self._get_name_and_dataframe(identifier)
|
77
97
|
self._series_by_id[identifier] = {Entry.ID: self._id_count, Entry.NAME: name, Entry.DATA: series}
|
78
98
|
|
79
|
-
def _get_name_and_dataframe(self, identifier:
|
80
|
-
"""
|
99
|
+
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`
|
102
|
+
|
103
|
+
Args:
|
104
|
+
identifier: to be registered - either a single numeric value or a string pointing to a timeseries file
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
tuple of name & dataframe
|
108
|
+
|
109
|
+
Raises:
|
110
|
+
TimeSeriesError: if the file could not be found or contains improper data, or if identifier is NaN,
|
111
|
+
logged with level "ERROR"
|
112
|
+
"""
|
81
113
|
if isinstance(identifier, str):
|
82
114
|
series_path = self._path_resolver.resolve_series_file_path(Path(identifier).as_posix())
|
83
115
|
if series_path and os.path.exists(series_path):
|
84
|
-
data = pd.read_csv(series_path, sep=";", header=None, comment="#")
|
85
116
|
try:
|
86
|
-
|
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)
|
87
122
|
except TypeError as e:
|
88
123
|
raise log_error(TimeSeriesError(self._ERR_CORRUPT_TIME_SERIES_VALUE.format(identifier), e)) from e
|
89
124
|
except ConversionError as e:
|
@@ -96,26 +131,52 @@ class TimeSeriesManager:
|
|
96
131
|
else:
|
97
132
|
return self._create_timeseries_from_value(identifier)
|
98
133
|
|
99
|
-
def _check_and_convert_series(self, data: pd.DataFrame) -> pd.DataFrame:
|
100
|
-
"""
|
101
|
-
|
102
|
-
|
103
|
-
|
134
|
+
def _check_and_convert_series(self, data: pd.DataFrame, identifier: str) -> pd.DataFrame:
|
135
|
+
"""
|
136
|
+
Ensures validity of time series and convert to required format for writing to disk
|
137
|
+
|
138
|
+
Args:
|
139
|
+
data: dataframe to be converted to expected format
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
2-column dataframe, first column: integers, second column: floats (no NaN)
|
143
|
+
|
144
|
+
Raises:
|
145
|
+
ConversionError: if first data column could not be converted to integer, logged with level "ERROR"
|
146
|
+
TypeError: if second data column in given data could not be converted to float or contained NaN,
|
147
|
+
logged with level "ERROR"
|
148
|
+
"""
|
149
|
+
data, additional_columns = data.loc[:, :2], data.loc[:, 2:]
|
150
|
+
if not additional_columns.dropna(how="all").empty:
|
104
151
|
log().warning(self._WARN_DATA_IGNORED)
|
105
152
|
if data.dtypes[0] != "int64":
|
153
|
+
if len(data[0]) > FILE_LENGTH_WARN_LIMIT:
|
154
|
+
log().warning(self._WARN_LARGE_CONVERSION.format(identifier))
|
106
155
|
data[0] = [FameTime.convert_string_if_is_datetime(time) for time in data[0]]
|
107
|
-
data[1]
|
156
|
+
if data.dtypes[1] != "float64":
|
157
|
+
data[1] = [TimeSeriesManager._assert_float(value) for value in data[1]]
|
158
|
+
if data[1].isna().any():
|
159
|
+
raise log_error(TypeError(TimeSeriesManager._ERR_NAN_VALUE))
|
108
160
|
return data
|
109
161
|
|
110
162
|
@staticmethod
|
111
|
-
def
|
112
|
-
"""
|
163
|
+
def _assert_float(value: Any) -> float:
|
164
|
+
"""
|
165
|
+
Converts any given value to a float or raise an Exception
|
166
|
+
|
167
|
+
Args:
|
168
|
+
value: to be converted to float
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
float representation of value
|
172
|
+
|
173
|
+
Raises:
|
174
|
+
TypeError: if given value cannot be converted to float, logged with level "ERROR"
|
175
|
+
"""
|
113
176
|
try:
|
114
177
|
value = float(value)
|
115
178
|
except ValueError as e:
|
116
179
|
raise log_error(TypeError(TimeSeriesManager._ERR_NON_NUMERIC.format(value))) from e
|
117
|
-
if math.isnan(value):
|
118
|
-
raise log_error(TypeError(TimeSeriesManager._ERR_NAN_VALUE))
|
119
180
|
return value
|
120
181
|
|
121
182
|
@staticmethod
|
@@ -128,14 +189,25 @@ class TimeSeriesManager:
|
|
128
189
|
return False
|
129
190
|
|
130
191
|
@staticmethod
|
131
|
-
def _create_timeseries_from_value(value:
|
132
|
-
"""
|
192
|
+
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`
|
195
|
+
|
196
|
+
Args:
|
197
|
+
value: the static value of the timeseries to be created
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
tuple of name & dataframe
|
201
|
+
|
202
|
+
Raises:
|
203
|
+
TimeSeriesError: if given value is NaN, logged with level "ERROR"
|
204
|
+
"""
|
133
205
|
if math.isnan(value):
|
134
206
|
raise log_error(TimeSeriesError(TimeSeriesManager._ERR_NAN_VALUE))
|
135
207
|
data = pd.DataFrame({0: [INT64_MIN, INT64_MAX], 1: [value, value]})
|
136
208
|
return TimeSeriesManager._CONSTANT_IDENTIFIER.format(value), data
|
137
209
|
|
138
|
-
def get_series_id_by_identifier(self, identifier:
|
210
|
+
def get_series_id_by_identifier(self, identifier: str | int | float) -> int:
|
139
211
|
"""
|
140
212
|
Returns id for a previously stored time series by given `identifier`
|
141
213
|
|
@@ -146,11 +218,11 @@ class TimeSeriesManager:
|
|
146
218
|
unique ID for the given identifier
|
147
219
|
|
148
220
|
Raises:
|
149
|
-
|
221
|
+
TimeSeriesError: if identifier was not yet registered, logged with level "ERROR"
|
150
222
|
"""
|
151
223
|
if not self._time_series_is_registered(identifier):
|
152
224
|
raise log_error(TimeSeriesError(self._ERR_UNREGISTERED_SERIES.format(identifier)))
|
153
|
-
return self._series_by_id.get(identifier)[Entry.ID]
|
225
|
+
return self._series_by_id.get(identifier)[Entry.ID] # type: ignore[index]
|
154
226
|
|
155
227
|
def get_all_series(self) -> list[tuple[int, str, pd.DataFrame]]:
|
156
228
|
"""Returns iterator over id, name and dataframe of all stored series"""
|
@@ -175,7 +247,8 @@ class TimeSeriesManager:
|
|
175
247
|
)
|
176
248
|
self._series_by_id[one_series.series_id] = reconstructed
|
177
249
|
|
178
|
-
def _get_cleaned_file_name(self, timeseries_name: str):
|
250
|
+
def _get_cleaned_file_name(self, timeseries_name: str) -> str:
|
251
|
+
"""Ensure given file name has CSV file ending"""
|
179
252
|
if Path(timeseries_name).suffix.lower() == CSV_FILE_SUFFIX:
|
180
253
|
filename = Path(timeseries_name).name
|
181
254
|
else:
|
@@ -183,8 +256,19 @@ class TimeSeriesManager:
|
|
183
256
|
return str(Path(self._TIMESERIES_RECONSTRUCTION_PATH, filename))
|
184
257
|
|
185
258
|
def get_reconstructed_series_by_id(self, series_id: int) -> str:
|
186
|
-
"""
|
187
|
-
|
259
|
+
"""
|
260
|
+
Return name or path for given `series_id` if series these are identified by their number.
|
261
|
+
Use this only if series were added via `reconstruct_time_series`
|
262
|
+
|
263
|
+
Args:
|
264
|
+
series_id: number of series
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
name or path of time series
|
268
|
+
|
269
|
+
Raises:
|
270
|
+
TimeSeriesError: if series was not registered during `reconstruct_time_series`, logged with level "ERROR"
|
271
|
+
"""
|
188
272
|
if series_id < 0 or series_id > self._id_count:
|
189
|
-
raise log_error(TimeSeriesError(self.
|
273
|
+
raise log_error(TimeSeriesError(self._ERR_UNREGISTERED_SERIES_RE.format(series_id)))
|
190
274
|
return self._series_by_id[series_id][Entry.NAME]
|
fameio/time.py
CHANGED
@@ -1,13 +1,17 @@
|
|
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
|
5
|
+
|
4
6
|
import datetime as dt
|
5
7
|
import math
|
6
8
|
import re
|
7
9
|
from enum import Enum, auto
|
8
|
-
from typing import
|
10
|
+
from typing import Any
|
9
11
|
|
12
|
+
from fameio.input import InputError
|
10
13
|
from fameio.logs import log_error
|
14
|
+
from fameio.output import OutputError
|
11
15
|
|
12
16
|
START_IN_REAL_TIME = "2000-01-01_00:00:00"
|
13
17
|
DATE_FORMAT = "%Y-%m-%d_%H:%M:%S"
|
@@ -15,8 +19,8 @@ DATE_REGEX = re.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}:[0-9]{2}:[0-9]{2}")
|
|
15
19
|
FAME_FIRST_DATETIME = dt.datetime.strptime(START_IN_REAL_TIME, DATE_FORMAT)
|
16
20
|
|
17
21
|
|
18
|
-
class ConversionError(
|
19
|
-
"""Indicates that
|
22
|
+
class ConversionError(InputError, OutputError):
|
23
|
+
"""Indicates that time stamp conversion failed"""
|
20
24
|
|
21
25
|
|
22
26
|
class TimeUnit(Enum):
|
@@ -45,9 +49,9 @@ class Constants:
|
|
45
49
|
STEPS_PER_DAY = STEPS_PER_HOUR * HOURS_PER_DAY
|
46
50
|
STEPS_PER_YEAR = STEPS_PER_DAY * DAYS_PER_YEAR
|
47
51
|
STEPS_PER_WEEK = STEPS_PER_DAY * 7
|
48
|
-
STEPS_PER_MONTH = STEPS_PER_YEAR / 12
|
52
|
+
STEPS_PER_MONTH = int(STEPS_PER_YEAR / 12)
|
49
53
|
|
50
|
-
steps_per_unit = {
|
54
|
+
steps_per_unit: dict[TimeUnit, int] = {
|
51
55
|
TimeUnit.SECONDS: STEPS_PER_SECOND,
|
52
56
|
TimeUnit.MINUTES: STEPS_PER_MINUTE,
|
53
57
|
TimeUnit.HOURS: STEPS_PER_HOUR,
|
@@ -70,7 +74,19 @@ class FameTime:
|
|
70
74
|
|
71
75
|
@staticmethod
|
72
76
|
def convert_datetime_to_fame_time_step(datetime_string: str) -> int:
|
73
|
-
"""
|
77
|
+
"""
|
78
|
+
Converts Datetime string to FAME time step
|
79
|
+
|
80
|
+
Args:
|
81
|
+
datetime_string: a datetime in FAME formatting
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
corresponding FAME time step
|
85
|
+
|
86
|
+
Raises:
|
87
|
+
ConversionError: if string could not be interpreted or does not represent a valid FAME time step,
|
88
|
+
logged with level "ERROR"
|
89
|
+
"""
|
74
90
|
if not FameTime.is_datetime(datetime_string):
|
75
91
|
raise log_error(ConversionError(FameTime._FORMAT_INVALID.format(datetime_string)))
|
76
92
|
datetime = FameTime._convert_to_datetime(datetime_string)
|
@@ -85,7 +101,18 @@ class FameTime:
|
|
85
101
|
|
86
102
|
@staticmethod
|
87
103
|
def _convert_to_datetime(datetime_string: str) -> dt.datetime:
|
88
|
-
"""
|
104
|
+
"""
|
105
|
+
Converts given `datetime_string` in FAME formatting to real-world datetime
|
106
|
+
|
107
|
+
Args:
|
108
|
+
datetime_string: to be converted to datetime
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
datetime representation of provided string
|
112
|
+
|
113
|
+
Raises:
|
114
|
+
ConversionError: if provided string could not be converted, logged with level "ERROR"
|
115
|
+
"""
|
89
116
|
try:
|
90
117
|
return dt.datetime.strptime(datetime_string, DATE_FORMAT)
|
91
118
|
except ValueError as e:
|
@@ -94,8 +121,17 @@ class FameTime:
|
|
94
121
|
@staticmethod
|
95
122
|
def convert_fame_time_step_to_datetime(fame_time_steps: int, date_format: str = DATE_FORMAT) -> str:
|
96
123
|
"""
|
97
|
-
Converts given `fame_time_steps` to corresponding real-world datetime string in `date_format
|
98
|
-
|
124
|
+
Converts given `fame_time_steps` to corresponding real-world datetime string in `date_format`
|
125
|
+
|
126
|
+
Args:
|
127
|
+
fame_time_steps: an integer representing time in FAME's internal format
|
128
|
+
date_format: to be used for datetime representation
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
string representing the real-world datetime of the provided time steps
|
132
|
+
|
133
|
+
Raises:
|
134
|
+
ConversionError: if `date_format` is invalid, logged with level "ERROR"
|
99
135
|
"""
|
100
136
|
years_since_start_time = math.floor(fame_time_steps / Constants.STEPS_PER_YEAR)
|
101
137
|
current_year = years_since_start_time + Constants.FIRST_YEAR
|
@@ -110,21 +146,33 @@ class FameTime:
|
|
110
146
|
|
111
147
|
@staticmethod
|
112
148
|
def convert_time_span_to_fame_time_steps(value: int, unit: TimeUnit) -> int:
|
113
|
-
"""
|
149
|
+
"""
|
150
|
+
Converts value of `TimeUnit.UNIT` to FAME time steps
|
151
|
+
|
152
|
+
Args:
|
153
|
+
value: amount of the units to be converted
|
154
|
+
unit: base time unit
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
FAME time steps equivalent of `value x unit`
|
158
|
+
|
159
|
+
Raises:
|
160
|
+
ConversionError: if an unknown time unit is used, logged with level "ERROR"
|
161
|
+
"""
|
114
162
|
steps = Constants.steps_per_unit.get(unit)
|
115
163
|
if steps:
|
116
164
|
return steps * value
|
117
165
|
raise log_error(ConversionError(FameTime._TIME_UNIT_UNKNOWN.format(unit)))
|
118
166
|
|
119
167
|
@staticmethod
|
120
|
-
def is_datetime(string:
|
168
|
+
def is_datetime(string: Any) -> bool:
|
121
169
|
"""Returns `True` if given `string` matches Datetime string format and can be converted to FAME time step"""
|
122
170
|
if isinstance(string, str):
|
123
171
|
return DATE_REGEX.fullmatch(string.strip()) is not None
|
124
172
|
return False
|
125
173
|
|
126
174
|
@staticmethod
|
127
|
-
def is_fame_time_compatible(value:
|
175
|
+
def is_fame_time_compatible(value: int | str) -> bool:
|
128
176
|
"""Returns `True` if given int or string `value` can be converted to a FAME time step"""
|
129
177
|
if isinstance(value, int):
|
130
178
|
return True
|
@@ -142,14 +190,23 @@ class FameTime:
|
|
142
190
|
return True
|
143
191
|
|
144
192
|
@staticmethod
|
145
|
-
def convert_string_if_is_datetime(value:
|
193
|
+
def convert_string_if_is_datetime(value: int | str) -> int:
|
146
194
|
"""
|
147
|
-
Returns FAME time steps
|
148
|
-
|
149
|
-
|
195
|
+
Returns FAME time steps of given `value`. If it is a valid FAME datetime string it is converted to
|
196
|
+
FAME time steps, or, if given `value` is an integer, it is returned without modification.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
value: to be converted
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
FAME time steps equivalent of provided value
|
203
|
+
|
204
|
+
Raises:
|
205
|
+
ConversionError: if given `value` is neither a FAME datetime string nor an integer value,
|
206
|
+
logged with level "ERROR"
|
150
207
|
"""
|
151
208
|
if FameTime.is_datetime(value):
|
152
|
-
return int(FameTime.convert_datetime_to_fame_time_step(value))
|
209
|
+
return int(FameTime.convert_datetime_to_fame_time_step(value)) # type: ignore[arg-type]
|
153
210
|
try:
|
154
211
|
return int(value)
|
155
212
|
except ValueError as e:
|
fameio/tools.py
CHANGED
@@ -1,8 +1,10 @@
|
|
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
|
5
|
+
|
4
6
|
from pathlib import Path
|
5
|
-
from typing import Any
|
7
|
+
from typing import Any
|
6
8
|
|
7
9
|
|
8
10
|
def keys_to_lower(dictionary: dict[str, Any]) -> dict[str, Any]:
|
@@ -17,12 +19,12 @@ def ensure_is_list(value: Any) -> list:
|
|
17
19
|
return [value]
|
18
20
|
|
19
21
|
|
20
|
-
def ensure_path_exists(path:
|
22
|
+
def ensure_path_exists(path: Path | str):
|
21
23
|
"""Creates a specified path if not already existent"""
|
22
24
|
Path(path).mkdir(parents=True, exist_ok=True)
|
23
25
|
|
24
26
|
|
25
27
|
def clean_up_file_name(name: str) -> str:
|
26
|
-
"""Returns given `name` with
|
27
|
-
|
28
|
-
return name.translate(
|
28
|
+
"""Returns given `name` replacing spaces and colons with underscore, and slashed with a dash"""
|
29
|
+
translation_table = str.maketrans({" ": "_", ":": "_", "/": "-"})
|
30
|
+
return name.translate(translation_table)
|
@@ -1,8 +1,7 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: fameio
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2.0
|
4
4
|
Summary: Tools for input preparation and output digestion of FAME models
|
5
|
-
Home-page: https://gitlab.com/fame-framework/wiki/-/wikis/home
|
6
5
|
License: Apache-2.0
|
7
6
|
Keywords: FAME,fameio,agent-based modelling,energy systems
|
8
7
|
Author: Felix Nitsch
|
@@ -20,24 +19,28 @@ Classifier: Programming Language :: Python :: 3.9
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
23
23
|
Classifier: Topic :: Scientific/Engineering
|
24
|
-
Requires-Dist: fameprotobuf (>=2.0.2,<3.0
|
24
|
+
Requires-Dist: fameprotobuf (>=2.0.2,<3.0)
|
25
25
|
Requires-Dist: pandas (>=1.0,<3.0)
|
26
26
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
27
|
-
Project-URL:
|
27
|
+
Project-URL: Changelog, https://gitlab.com/fame-framework/fame-io/-/blob/main/CHANGELOG.md
|
28
|
+
Project-URL: Homepage, https://helmholtz.software/software/fame
|
29
|
+
Project-URL: Issue Tracking, https://gitlab.com/fame-framework/fame-io/-/issues
|
30
|
+
Project-URL: Repository, https://gitlab.com/fame-framework/fame-io
|
28
31
|
Description-Content-Type: text/markdown
|
29
32
|
|
30
33
|
<!-- SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
31
34
|
|
32
35
|
SPDX-License-Identifier: Apache-2.0 -->
|
33
36
|
|
34
|
-
| |
|
35
|
-
|
36
|
-
| **Package** |
|
37
|
-
| **
|
38
|
-
| **Activity** |  
|
39
|
-
| **Style** | [](https://github.com/psf/black) [](https://doi.org/10.21105/joss.04958) [](https://doi.org/10.5281/zenodo.4314337)
|
37
|
+
| | |
|
38
|
+
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
39
|
+
| **Package** |   [](https://badge.fury.io/py/fameio) [](https://api.reuse.software/info/gitlab.com/fame-framework/fame-io) |
|
40
|
+
| **Test** | [](https://gitlab.com/fame-framework/fame-io/commits/main) [](https://gitlab.com/fame-framework/fame-io/-/commits/main) |
|
41
|
+
| **Activity** |   |
|
42
|
+
| **Style** | [](https://github.com/psf/black) [](https://common-changelog.org/)  [](https://github.com/pylint-dev/pylint) |
|
43
|
+
| **Reference** | [](https://doi.org/10.21105/joss.04958) [](https://doi.org/10.5281/zenodo.4314337) |
|
41
44
|
|
42
45
|
# FAME-Io
|
43
46
|
|
@@ -362,7 +365,9 @@ Agent Parameters:
|
|
362
365
|
* `Attributes` Optional; if the agent has any attributes, specify them here in the format "AttributeName: value"; please
|
363
366
|
see attribute table above
|
364
367
|
* `Metadata` Optional; can be assigned to each instance of an Agent, as well as to each of its Attributes
|
368
|
+
* `Ext` Optional; Reserved key for parameters not used by fameio but its extensions, e.g., FAME-Gui
|
365
369
|
|
370
|
+
A warning is logged for any other key at this level.
|
366
371
|
The specified `Attributes` for each agent must match the specified `Attributes` options in the linked Schema (see
|
367
372
|
above).
|
368
373
|
For better structure and readability of the `scenario.yaml`, `Attributes` may also be specified in a nested way as
|
@@ -642,7 +647,7 @@ These CSV files follow a specific structure:
|
|
642
647
|
* They should contain exactly two columns - any other columns are ignored.
|
643
648
|
A warning is raised if more than two non-empty columns are detected.
|
644
649
|
* The first column must be a time stamp in form `YYYY-MM-DD_hh:mm:ss` or
|
645
|
-
a [FAME-Timestamp](https://gitlab.com/fame-framework/wiki/-/wikis/architecture/decisions/TimeStamp) integer value
|
650
|
+
a [FAME-Timestamp](https://gitlab.com/fame-framework/wiki/-/wikis/architecture/decisions/TimeStamp) integer value.
|
646
651
|
* The second column must be a numerical value (either integer or floating-point)
|
647
652
|
* The separator of the two columns is a semicolon
|
648
653
|
* The data must **not** have headers, except for comments marked with `#`
|
@@ -662,6 +667,7 @@ Please refer also to the detailed article about `TimeStamps` in
|
|
662
667
|
the [FAME-Wiki](https://gitlab.com/fame-framework/wiki/-/wikis/TimeStamp).
|
663
668
|
For large CSV files (with more than 20,000 rows) we recommend using the integer representation of FAME-Timestamps in the
|
664
669
|
first column (instead of text representation) to improve conversion speed.
|
670
|
+
A warning will be raised for very large files (exceeding 50,000 rows) that require time stamp conversion.
|
665
671
|
|
666
672
|
### Split and join multiple YAML files
|
667
673
|
|