psr-graf 2.3.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.
- psr/graf/__init__.py +1 -0
- psr/graf/graf.py +716 -0
- psr/graf/py.typed +0 -0
- psr_graf-2.3.0.dist-info/METADATA +336 -0
- psr_graf-2.3.0.dist-info/RECORD +7 -0
- psr_graf-2.3.0.dist-info/WHEEL +4 -0
- psr_graf-2.3.0.dist-info/licenses/LICENSE +21 -0
psr/graf/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .graf import * # noqa: F403
|
psr/graf/graf.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import os
|
|
5
|
+
import struct
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Any, BinaryIO, TextIO
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseBinReader",
|
|
12
|
+
"BinReader",
|
|
13
|
+
"CsvReader",
|
|
14
|
+
"GrafError",
|
|
15
|
+
"GrafIOError",
|
|
16
|
+
"__version__",
|
|
17
|
+
"load_as_dataframe",
|
|
18
|
+
"open_bin",
|
|
19
|
+
"open_csv",
|
|
20
|
+
"version",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
__version__ = "2.3.0"
|
|
24
|
+
|
|
25
|
+
# Number of bytes in a word (int32, float, ...)
|
|
26
|
+
_WORD = 4
|
|
27
|
+
|
|
28
|
+
# Check whether pandas' dataframe is available.
|
|
29
|
+
try:
|
|
30
|
+
import numpy as np
|
|
31
|
+
import pandas as pd
|
|
32
|
+
|
|
33
|
+
_HAS_PANDAS = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
_HAS_PANDAS = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def version() -> str:
|
|
39
|
+
return __version__
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GrafError(Exception):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GrafIOError(GrafError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _GrafReaderBase:
|
|
51
|
+
BLOCK_TYPE_BLOCK = 0
|
|
52
|
+
BLOCK_TYPE_HOUR = 1
|
|
53
|
+
STAGE_TYPE_WEEKLY = 1
|
|
54
|
+
STAGE_TYPE_MONTHLY = 2
|
|
55
|
+
STAGE_TYPE_13MONTHLY = 6
|
|
56
|
+
STAGE_DESCRIPTION = {
|
|
57
|
+
STAGE_TYPE_WEEKLY: "weekly",
|
|
58
|
+
STAGE_TYPE_MONTHLY: "monthly",
|
|
59
|
+
STAGE_TYPE_13MONTHLY: "13 months",
|
|
60
|
+
}
|
|
61
|
+
BLOCK_DESCRIPTION = {BLOCK_TYPE_BLOCK: "block", BLOCK_TYPE_HOUR: "hour"}
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
# string encoding
|
|
65
|
+
self._encoding = "utf-8"
|
|
66
|
+
# file references
|
|
67
|
+
self._name = ""
|
|
68
|
+
# hdr file data
|
|
69
|
+
self._case_initial_stage = None
|
|
70
|
+
self._stages = None
|
|
71
|
+
self._min_stage = None
|
|
72
|
+
self._max_stage = None
|
|
73
|
+
self._scenarios = None
|
|
74
|
+
self._varies_by_scenario = None
|
|
75
|
+
self._varies_by_block = None
|
|
76
|
+
self._hour_or_block = None
|
|
77
|
+
self._stage_type = None
|
|
78
|
+
self._initial_year = None
|
|
79
|
+
self._units = None
|
|
80
|
+
self._agents = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def initial_stage(self) -> int:
|
|
84
|
+
return self._case_initial_stage
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def stages(self) -> int:
|
|
88
|
+
return self._stages
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def min_stage(self) -> int:
|
|
92
|
+
return self._min_stage
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def max_stage(self) -> int:
|
|
96
|
+
return self._max_stage
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def scenarios(self) -> int:
|
|
100
|
+
return self._scenarios
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def varies_by_scenario(self) -> int:
|
|
104
|
+
return self._varies_by_scenario
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def varies_by_block(self) -> int:
|
|
108
|
+
return self._varies_by_block
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def hour_or_block(self) -> int:
|
|
112
|
+
return self._hour_or_block
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def stage_type(self) -> int:
|
|
116
|
+
return self._stage_type
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def initial_year(self) -> int:
|
|
120
|
+
return self._initial_year
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def units(self) -> str:
|
|
124
|
+
return self._units
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def agents(self) -> tuple[str, ...]:
|
|
128
|
+
return self._agents
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def name(self) -> str:
|
|
132
|
+
return self._name
|
|
133
|
+
|
|
134
|
+
def open(self, file_path: str | os.PathLike[str], **kwargs: Any) -> None:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def close(self) -> None:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def _check_indexes(self, stage: int, scenario: int, block: int | None = None) -> None:
|
|
141
|
+
if not self._min_stage <= stage <= self._max_stage:
|
|
142
|
+
raise IndexError(f"Stage {stage} out of range ({self._min_stage}, {self._max_stage}).")
|
|
143
|
+
|
|
144
|
+
if not 1 <= scenario <= self._scenarios:
|
|
145
|
+
raise IndexError(f"Scenario {scenario} out of range ({self._scenarios}).")
|
|
146
|
+
|
|
147
|
+
if block is not None:
|
|
148
|
+
total_blocks = self.blocks(stage)
|
|
149
|
+
if not 1 <= block <= total_blocks:
|
|
150
|
+
raise IndexError(f"Block {block} out of range ({total_blocks} for stage {stage}).")
|
|
151
|
+
|
|
152
|
+
def blocks(self, stage: int) -> int:
|
|
153
|
+
raise NotImplementedError
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BaseBinReader(_GrafReaderBase):
|
|
157
|
+
"""
|
|
158
|
+
SDDP binary data reader class.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self) -> None:
|
|
162
|
+
super().__init__()
|
|
163
|
+
# file references
|
|
164
|
+
self.__hdr_file_path = ""
|
|
165
|
+
self.__bin_file_path = ""
|
|
166
|
+
self._bin_file_handler: BinaryIO | None = None
|
|
167
|
+
self.__single_bin_mode = False
|
|
168
|
+
self.__bin_offset = 0
|
|
169
|
+
# print hdr information
|
|
170
|
+
self._print_metadata = False
|
|
171
|
+
# hdr file data
|
|
172
|
+
self._bin_version = None
|
|
173
|
+
self._name_length = None
|
|
174
|
+
self._bin_offsets = None
|
|
175
|
+
|
|
176
|
+
def __del__(self) -> None:
|
|
177
|
+
if self._bin_file_handler is not None:
|
|
178
|
+
self.close()
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def bin_version(self) -> int:
|
|
182
|
+
return self._bin_version
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def name_length(self) -> int:
|
|
186
|
+
return self._name_length
|
|
187
|
+
|
|
188
|
+
def open(self, file_path: str | os.PathLike[str], **kwargs: Any) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Opens a single binary or HDR and BIN file pairs for reading,
|
|
191
|
+
when one of then is specified. If a file path without extension is
|
|
192
|
+
specified, tries to open a pair of .hdr and .bin file.
|
|
193
|
+
The BIN file stays open during BinReader lifetime.
|
|
194
|
+
|
|
195
|
+
Keyword arguments:
|
|
196
|
+
print_metadata -- Print files metadata to stdout (Default = False).
|
|
197
|
+
encoding -- encoding to decode strings in binary files
|
|
198
|
+
(Default = utf-8).
|
|
199
|
+
|
|
200
|
+
Non thread-safe method.
|
|
201
|
+
"""
|
|
202
|
+
self._encoding = kwargs.get("encoding", "utf-8")
|
|
203
|
+
self._print_metadata = kwargs.get("print_metadata", False)
|
|
204
|
+
|
|
205
|
+
# file paths
|
|
206
|
+
file_path = os.fspath(file_path)
|
|
207
|
+
base_path, ext = os.path.splitext(file_path)
|
|
208
|
+
if ext.lower() in (".hdr", ".bin", ""):
|
|
209
|
+
self.__hdr_file_path = base_path + ".hdr"
|
|
210
|
+
self.__bin_file_path = base_path + ".bin"
|
|
211
|
+
else:
|
|
212
|
+
self.__hdr_file_path = file_path
|
|
213
|
+
self.__single_bin_mode = True
|
|
214
|
+
self._name = os.path.basename(base_path)
|
|
215
|
+
|
|
216
|
+
# Check files existence.
|
|
217
|
+
if not os.path.exists(self.__hdr_file_path):
|
|
218
|
+
if not self.__single_bin_mode:
|
|
219
|
+
error_msg = f"HDR file not found: {self.__hdr_file_path}"
|
|
220
|
+
else:
|
|
221
|
+
error_msg = f"File not found: {self.__hdr_file_path}"
|
|
222
|
+
raise FileNotFoundError(error_msg)
|
|
223
|
+
|
|
224
|
+
if not self.__single_bin_mode:
|
|
225
|
+
if not os.path.exists(self.__bin_file_path):
|
|
226
|
+
error_msg = f"BIN file not found: {self.__bin_file_path}"
|
|
227
|
+
raise FileNotFoundError(error_msg)
|
|
228
|
+
|
|
229
|
+
if not self.__single_bin_mode:
|
|
230
|
+
# read HDR
|
|
231
|
+
with open(self.__hdr_file_path, "rb") as hdr_file:
|
|
232
|
+
self.__read_hdr(hdr_file)
|
|
233
|
+
|
|
234
|
+
# read BIN and keep it open
|
|
235
|
+
self._bin_file_handler = open(self.__bin_file_path, "rb")
|
|
236
|
+
else:
|
|
237
|
+
# Read single binary file and keep it open.
|
|
238
|
+
data_file = open(self.__hdr_file_path, "rb")
|
|
239
|
+
try:
|
|
240
|
+
self.__read_hdr(data_file)
|
|
241
|
+
except BaseException:
|
|
242
|
+
data_file.close()
|
|
243
|
+
raise
|
|
244
|
+
self.__bin_offset = data_file.tell()
|
|
245
|
+
self._bin_file_handler = data_file
|
|
246
|
+
|
|
247
|
+
def close(self) -> None:
|
|
248
|
+
"""Closes the binary file for reading."""
|
|
249
|
+
if self._bin_file_handler is not None and not self._bin_file_handler.closed:
|
|
250
|
+
self._bin_file_handler.close()
|
|
251
|
+
|
|
252
|
+
def __read_hdr(self, input_stream: BinaryIO) -> None:
|
|
253
|
+
def unpack_int() -> int:
|
|
254
|
+
"""Unpack 4 bytes as integer from input_stream and move its
|
|
255
|
+
position by 4 bytes.
|
|
256
|
+
"""
|
|
257
|
+
return struct.unpack("<i", input_stream.read(_WORD))[0]
|
|
258
|
+
|
|
259
|
+
def unpack_str(length: int) -> str:
|
|
260
|
+
# Unpack variable length string.
|
|
261
|
+
bytes_value = struct.unpack(f"{length}s", input_stream.read(length))[0]
|
|
262
|
+
return bytes_value.decode(self._encoding).strip()
|
|
263
|
+
|
|
264
|
+
# Record #1
|
|
265
|
+
seek_curpos = 1 # Seek from current position flag
|
|
266
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
267
|
+
self._bin_version = unpack_int()
|
|
268
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
269
|
+
|
|
270
|
+
# Record #2
|
|
271
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
272
|
+
self._min_stage = unpack_int()
|
|
273
|
+
self._max_stage = unpack_int()
|
|
274
|
+
self._scenarios = unpack_int()
|
|
275
|
+
agents_count = unpack_int()
|
|
276
|
+
self._varies_by_scenario = unpack_int()
|
|
277
|
+
self._varies_by_block = unpack_int()
|
|
278
|
+
self._hour_or_block = unpack_int()
|
|
279
|
+
self._stage_type = unpack_int()
|
|
280
|
+
self._case_initial_stage = unpack_int()
|
|
281
|
+
self._initial_year = unpack_int()
|
|
282
|
+
self._units = unpack_str(7)
|
|
283
|
+
self._name_length = unpack_int()
|
|
284
|
+
self._stages = self._max_stage - self._min_stage + 1
|
|
285
|
+
|
|
286
|
+
if self._varies_by_scenario == 0:
|
|
287
|
+
self._scenarios = 1
|
|
288
|
+
|
|
289
|
+
if self._print_metadata:
|
|
290
|
+
if not self.__single_bin_mode:
|
|
291
|
+
print("HDR/BIN graf metadata:")
|
|
292
|
+
else:
|
|
293
|
+
print("graf metadata:")
|
|
294
|
+
print(" Binary file version:", self._bin_version)
|
|
295
|
+
print(" First stage:", self._min_stage)
|
|
296
|
+
print(" Number of stages:", self._stages)
|
|
297
|
+
print(" Number of scenarios:", self._scenarios)
|
|
298
|
+
print(" Number of agents:", agents_count)
|
|
299
|
+
print(" Varies per scenario:", self._varies_by_scenario)
|
|
300
|
+
print(" Varies per block/hour:", self._varies_by_block)
|
|
301
|
+
print(" Time mapping:", BinReader.BLOCK_DESCRIPTION[self._hour_or_block])
|
|
302
|
+
print(" Type of stage:", BinReader.STAGE_DESCRIPTION[self._stage_type])
|
|
303
|
+
print(" Case Initial month/week:", self._case_initial_stage)
|
|
304
|
+
print(" Case Initial year:", self._initial_year)
|
|
305
|
+
print(" Units:", self._units)
|
|
306
|
+
print(" Stored name's length:", self._name_length)
|
|
307
|
+
|
|
308
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
309
|
+
|
|
310
|
+
# Record #3
|
|
311
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
312
|
+
|
|
313
|
+
self._bin_offsets = [unpack_int() for _ in range(self._stages + 1)]
|
|
314
|
+
|
|
315
|
+
input_stream.seek(_WORD, seek_curpos)
|
|
316
|
+
|
|
317
|
+
# Agent names
|
|
318
|
+
_agents = []
|
|
319
|
+
for _ in range(agents_count):
|
|
320
|
+
string_length = unpack_int()
|
|
321
|
+
_agents.append(unpack_str(string_length))
|
|
322
|
+
# discard unused bytes
|
|
323
|
+
input_stream.read(_WORD)
|
|
324
|
+
self._agents = tuple(_agents)
|
|
325
|
+
|
|
326
|
+
def _seek(self, i_stage: int, i_scenario: int, i_block: int) -> None:
|
|
327
|
+
# i_scenario, i_block are 1-based indexes; i_stage is 0-based.
|
|
328
|
+
index = (
|
|
329
|
+
self._bin_offsets[i_stage] * self._scenarios
|
|
330
|
+
+ self.blocks(i_stage + self._min_stage) * (i_scenario - 1)
|
|
331
|
+
+ (i_block - 1)
|
|
332
|
+
) * len(self._agents)
|
|
333
|
+
|
|
334
|
+
offset_from_start = self.__bin_offset + index * _WORD
|
|
335
|
+
seek_from_start = 0
|
|
336
|
+
self._bin_file_handler.seek(offset_from_start, seek_from_start)
|
|
337
|
+
|
|
338
|
+
def _read_stage(
|
|
339
|
+
self, stage: int, first_scenario: int, last_scenario: int | None = None
|
|
340
|
+
) -> tuple[int, bytes]:
|
|
341
|
+
"""Read the raw values of all blocks of a given stage, from first_scenario
|
|
342
|
+
to last_scenario (Default = first_scenario). Returns the number of blocks
|
|
343
|
+
and the values, scenario by scenario and block by block."""
|
|
344
|
+
if last_scenario is None:
|
|
345
|
+
last_scenario = first_scenario
|
|
346
|
+
self._check_indexes(stage, first_scenario)
|
|
347
|
+
self._check_indexes(stage, last_scenario)
|
|
348
|
+
self._seek(stage - self._min_stage, first_scenario, 1)
|
|
349
|
+
blocks = self.blocks(stage)
|
|
350
|
+
count = (last_scenario - first_scenario + 1) * blocks * len(self._agents)
|
|
351
|
+
return blocks, self._bin_file_handler.read(count * _WORD)
|
|
352
|
+
|
|
353
|
+
def blocks(self, stage: int) -> int:
|
|
354
|
+
"""Number of blocks for a given stage. 1-based stage."""
|
|
355
|
+
istage = stage - self._min_stage + 1
|
|
356
|
+
if self._varies_by_block != 0:
|
|
357
|
+
return self._bin_offsets[istage] - self._bin_offsets[istage - 1]
|
|
358
|
+
return 1
|
|
359
|
+
|
|
360
|
+
def read(self, stage: int, scenario: int, block: int) -> tuple[float, ...]:
|
|
361
|
+
"""
|
|
362
|
+
Read data of a given stage, scenario, and block. Returns a tuple with
|
|
363
|
+
values for each agent.
|
|
364
|
+
|
|
365
|
+
Raises IndexError if stage, scenario, or block is out of bounds.
|
|
366
|
+
|
|
367
|
+
Non thread-safe.
|
|
368
|
+
"""
|
|
369
|
+
self._check_indexes(stage, scenario, block)
|
|
370
|
+
self._seek(stage - self._min_stage, scenario, block)
|
|
371
|
+
agents = len(self._agents)
|
|
372
|
+
return struct.unpack(f"<{agents}f", self._bin_file_handler.read(agents * _WORD))
|
|
373
|
+
|
|
374
|
+
def read_blocks(self, stage: int, scenario: int) -> list[list[float]]:
|
|
375
|
+
"""
|
|
376
|
+
Read data of a given stage and scenario. Returns a list
|
|
377
|
+
containing lists with block data, for each agent.
|
|
378
|
+
|
|
379
|
+
Raises IndexError if stage or scenario is out of bounds.
|
|
380
|
+
|
|
381
|
+
Non thread-safe.
|
|
382
|
+
"""
|
|
383
|
+
blocks, buffer = self._read_stage(stage, scenario)
|
|
384
|
+
agents = len(self._agents)
|
|
385
|
+
all_values = struct.unpack(f"<{blocks * agents}f", buffer)
|
|
386
|
+
return [list(all_values[i_agent::agents]) for i_agent in range(agents)]
|
|
387
|
+
|
|
388
|
+
def read_blocks_as_array(self, stage: int, scenario: int) -> tuple[tuple[float, ...], ...]:
|
|
389
|
+
"""
|
|
390
|
+
Read data of a given stage and scenario. Returns a tuple with
|
|
391
|
+
dimensions (blocks, agents).
|
|
392
|
+
|
|
393
|
+
Raises IndexError if stage or scenario is out of bounds.
|
|
394
|
+
|
|
395
|
+
Non thread-safe.
|
|
396
|
+
"""
|
|
397
|
+
blocks, buffer = self._read_stage(stage, scenario)
|
|
398
|
+
agents = len(self._agents)
|
|
399
|
+
all_values = struct.unpack(f"<{blocks * agents}f", buffer)
|
|
400
|
+
return tuple(all_values[i : i + agents] for i in range(0, len(all_values), agents))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if not _HAS_PANDAS:
|
|
404
|
+
|
|
405
|
+
class BinReader(BaseBinReader):
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
else:
|
|
409
|
+
|
|
410
|
+
class BinReader(BaseBinReader):
|
|
411
|
+
"""
|
|
412
|
+
A modified version of the psr.graf.BinReader class with an alternative
|
|
413
|
+
to the read_blocks method, that avoids some expensive for loops by
|
|
414
|
+
using numpy to reshape the incoming stream into a 2D array.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def read_blocks_as_array(self, stage: int, scenario: int) -> np.ndarray:
|
|
418
|
+
"""
|
|
419
|
+
Read data of a given stage and scenario. Returns a 2D numpy array
|
|
420
|
+
with dimensions (blocks, agents).
|
|
421
|
+
|
|
422
|
+
Raises IndexError if stage or scenario is out of bounds.
|
|
423
|
+
|
|
424
|
+
Author: corypdavis
|
|
425
|
+
"""
|
|
426
|
+
blocks, buffer = self._read_stage(stage, scenario)
|
|
427
|
+
values = np.frombuffer(buffer, dtype="<f4").astype(np.float32)
|
|
428
|
+
return values.reshape((blocks, len(self._agents)))
|
|
429
|
+
|
|
430
|
+
def _read_stage_array(
|
|
431
|
+
self, stage: int, first_scenario: int, last_scenario: int
|
|
432
|
+
) -> np.ndarray:
|
|
433
|
+
"""Read the values of a range of scenarios of a given stage. Returns a
|
|
434
|
+
read-only numpy array with dimensions (scenarios, blocks, agents)."""
|
|
435
|
+
blocks, buffer = self._read_stage(stage, first_scenario, last_scenario)
|
|
436
|
+
values = np.frombuffer(buffer, dtype="<f4")
|
|
437
|
+
return values.reshape((-1, blocks, len(self._agents)))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class CsvReader(_GrafReaderBase):
|
|
441
|
+
def __init__(self) -> None:
|
|
442
|
+
super().__init__()
|
|
443
|
+
self.__csv_file_path = ""
|
|
444
|
+
self.__data = {}
|
|
445
|
+
self.__max_blocks_per_stage = {}
|
|
446
|
+
|
|
447
|
+
def open(self, file_path: str | os.PathLike[str], **kwargs: Any) -> None:
|
|
448
|
+
"""
|
|
449
|
+
Opens a single csv file for reading.
|
|
450
|
+
"""
|
|
451
|
+
self._encoding = kwargs.get("encoding", "utf-8")
|
|
452
|
+
file_path = os.fspath(file_path)
|
|
453
|
+
self.__csv_file_path = file_path
|
|
454
|
+
self._name = os.path.basename(self.__csv_file_path)
|
|
455
|
+
|
|
456
|
+
# Check file existence.
|
|
457
|
+
if not os.path.exists(file_path):
|
|
458
|
+
raise FileNotFoundError(f"CSV file not found: {file_path}")
|
|
459
|
+
|
|
460
|
+
with open(self.__csv_file_path, encoding=self._encoding) as csv_file:
|
|
461
|
+
self._read_header(csv_file)
|
|
462
|
+
self._read_data(csv_file)
|
|
463
|
+
|
|
464
|
+
def _read_header(self, csv_file: TextIO) -> None:
|
|
465
|
+
csv_reader = csv.reader(csv_file, delimiter=",", quotechar='"')
|
|
466
|
+
header_line1 = next(csv_reader)
|
|
467
|
+
header_line2 = next(csv_reader)
|
|
468
|
+
next(csv_reader)
|
|
469
|
+
header_line_4 = next(csv_reader)
|
|
470
|
+
|
|
471
|
+
self._varies_by_block = int(header_line1[1]) >= 1
|
|
472
|
+
self._units = header_line1[3].strip()
|
|
473
|
+
stage_type = int(header_line1[4])
|
|
474
|
+
self._case_initial_stage = int(header_line1[5])
|
|
475
|
+
self._initial_year = int(header_line1[6])
|
|
476
|
+
if stage_type in (
|
|
477
|
+
self.STAGE_TYPE_WEEKLY,
|
|
478
|
+
self.STAGE_TYPE_MONTHLY,
|
|
479
|
+
self.STAGE_TYPE_13MONTHLY,
|
|
480
|
+
):
|
|
481
|
+
self._stage_type = stage_type
|
|
482
|
+
else:
|
|
483
|
+
raise ValueError(f"Invalid stage type: {stage_type}")
|
|
484
|
+
|
|
485
|
+
self._varies_by_scenario = int(header_line2[1]) == 1
|
|
486
|
+
self._agents = tuple(agent.strip() for agent in header_line_4[3:])
|
|
487
|
+
self._stages = 0
|
|
488
|
+
self._scenarios = 0
|
|
489
|
+
|
|
490
|
+
def _read_data(self, csv_file: TextIO) -> None:
|
|
491
|
+
csv_reader = csv.reader(csv_file, delimiter=",", quotechar='"')
|
|
492
|
+
self.__data = {}
|
|
493
|
+
self._max_stage = 0
|
|
494
|
+
self._min_stage = 999999
|
|
495
|
+
for line in csv_reader:
|
|
496
|
+
key = tuple(map(int, line[:3]))
|
|
497
|
+
values = tuple(map(float, line[3:]))
|
|
498
|
+
self.__data[key] = values
|
|
499
|
+
# Update limits
|
|
500
|
+
stage, scenario, block = key
|
|
501
|
+
self._max_stage = max(self._max_stage, stage)
|
|
502
|
+
self._min_stage = min(self._min_stage, stage)
|
|
503
|
+
self._scenarios = max(self._scenarios, scenario)
|
|
504
|
+
self.__max_blocks_per_stage[stage] = max(
|
|
505
|
+
self.__max_blocks_per_stage.get(stage, block), block
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
self._stages = self._max_stage - self._min_stage + 1
|
|
509
|
+
|
|
510
|
+
if self._is_hourly_data():
|
|
511
|
+
self._hour_or_block = self.BLOCK_TYPE_HOUR
|
|
512
|
+
else:
|
|
513
|
+
self._hour_or_block = self.BLOCK_TYPE_BLOCK
|
|
514
|
+
|
|
515
|
+
def _is_hourly_data(self) -> bool:
|
|
516
|
+
if self._stage_type == self.STAGE_TYPE_WEEKLY:
|
|
517
|
+
return all(blocks == 168 for blocks in self.__max_blocks_per_stage.values())
|
|
518
|
+
if self._stage_type == self.STAGE_TYPE_MONTHLY:
|
|
519
|
+
for stage, blocks in self.__max_blocks_per_stage.items():
|
|
520
|
+
if stage in (1, 3, 5, 7, 8, 10, 12):
|
|
521
|
+
if blocks != 744:
|
|
522
|
+
return False
|
|
523
|
+
elif stage in (4, 6, 9, 11):
|
|
524
|
+
if blocks != 720:
|
|
525
|
+
return False
|
|
526
|
+
else:
|
|
527
|
+
if blocks != 672:
|
|
528
|
+
return False
|
|
529
|
+
return True
|
|
530
|
+
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
def blocks(self, stage: int) -> int:
|
|
534
|
+
"""Number of blocks for a given stage. 1-based stage."""
|
|
535
|
+
if self._varies_by_block != 0:
|
|
536
|
+
return self.__max_blocks_per_stage[stage]
|
|
537
|
+
return 1
|
|
538
|
+
|
|
539
|
+
def read(self, stage: int, scenario: int, block: int) -> tuple[float, ...]:
|
|
540
|
+
"""
|
|
541
|
+
Read data of a given stage, scenario, and block. Returns a tuple with
|
|
542
|
+
values for each agent.
|
|
543
|
+
|
|
544
|
+
Raises IndexError if stage, scenario, or block is out of bounds.
|
|
545
|
+
|
|
546
|
+
Non thread-safe.
|
|
547
|
+
"""
|
|
548
|
+
self._check_indexes(stage, scenario, block)
|
|
549
|
+
return self.__data[(stage, scenario, block)]
|
|
550
|
+
|
|
551
|
+
def _read_stage_array(self, stage: int, first_scenario: int, last_scenario: int) -> np.ndarray:
|
|
552
|
+
"""Read the values of a range of scenarios of a given stage. Returns a
|
|
553
|
+
numpy array with dimensions (scenarios, blocks, agents). Requires numpy."""
|
|
554
|
+
blocks = self.blocks(stage)
|
|
555
|
+
rows = [
|
|
556
|
+
self.read(stage, scenario, block)
|
|
557
|
+
for scenario in range(first_scenario, last_scenario + 1)
|
|
558
|
+
for block in range(1, blocks + 1)
|
|
559
|
+
]
|
|
560
|
+
return np.array(rows, dtype=np.float64).reshape((-1, blocks, len(self._agents)))
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@contextmanager
|
|
564
|
+
def open_bin(file_path: str | os.PathLike[str], **kwargs: Any) -> Iterator[BinReader]:
|
|
565
|
+
"""
|
|
566
|
+
Open SDDP binary files (.hdr and .bin) for reading provided a file path
|
|
567
|
+
for one of them or a file path without extension. Yields a
|
|
568
|
+
BinReader object.
|
|
569
|
+
|
|
570
|
+
Keyword arguments:
|
|
571
|
+
print_metadata -- Print files metadata to stdout (Default = False).
|
|
572
|
+
encoding -- encoding to decode strings in binary files
|
|
573
|
+
(Default = utf-8).
|
|
574
|
+
"""
|
|
575
|
+
obj = BinReader()
|
|
576
|
+
obj.open(file_path, **kwargs)
|
|
577
|
+
try:
|
|
578
|
+
yield obj
|
|
579
|
+
finally:
|
|
580
|
+
obj.close()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@contextmanager
|
|
584
|
+
def open_csv(file_path: str | os.PathLike[str], **kwargs: Any) -> Iterator[CsvReader]:
|
|
585
|
+
"""
|
|
586
|
+
Open SDDP csv result files (.csv) for reading provided a file path.
|
|
587
|
+
Yields a CsvReader object.
|
|
588
|
+
"""
|
|
589
|
+
obj = CsvReader()
|
|
590
|
+
obj.open(file_path, **kwargs)
|
|
591
|
+
try:
|
|
592
|
+
yield obj
|
|
593
|
+
finally:
|
|
594
|
+
obj.close()
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _get_agent_index_filter(
|
|
598
|
+
agents: tuple[str, ...], filter_agents: tuple[str, ...]
|
|
599
|
+
) -> tuple[int, ...]:
|
|
600
|
+
"""Indexes of filter_agents in agents. Names are case insensitive."""
|
|
601
|
+
if len(filter_agents) == 0:
|
|
602
|
+
return tuple(range(len(agents)))
|
|
603
|
+
lower_case_agents = tuple(agent.strip().lower() for agent in agents)
|
|
604
|
+
agents_index = []
|
|
605
|
+
for agent in filter_agents:
|
|
606
|
+
try:
|
|
607
|
+
agents_index.append(lower_case_agents.index(agent.strip().lower()))
|
|
608
|
+
except ValueError:
|
|
609
|
+
raise ValueError(f"Agent {agent!r} not found.") from None
|
|
610
|
+
return tuple(agents_index)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def load_as_dataframe(
|
|
614
|
+
file_path: str | os.PathLike[str],
|
|
615
|
+
*,
|
|
616
|
+
multi_index: bool = True,
|
|
617
|
+
index_format: str = "default",
|
|
618
|
+
filter_agents: Iterable[str] = (),
|
|
619
|
+
filter_stages: Iterable[int] = (),
|
|
620
|
+
filter_scenarios: Iterable[int] = (),
|
|
621
|
+
filter_blocks: Iterable[int] = (),
|
|
622
|
+
**kwargs: Any,
|
|
623
|
+
) -> pd.DataFrame:
|
|
624
|
+
"""
|
|
625
|
+
Load a SDDP result file (.hdr/.bin pair, single binary, or .csv) into a
|
|
626
|
+
pandas.DataFrame. Requires pandas.
|
|
627
|
+
|
|
628
|
+
Keyword arguments:
|
|
629
|
+
multi_index -- Use a pandas.MultiIndex for the index columns
|
|
630
|
+
(Default = True).
|
|
631
|
+
index_format -- 'default' (stage, scenario, block or hour) or 'period'
|
|
632
|
+
(year, month or week, scenario, block or hour).
|
|
633
|
+
filter_agents -- Agents to include, case insensitive (Default = all).
|
|
634
|
+
filter_stages -- Stages to include (Default = all).
|
|
635
|
+
filter_scenarios -- Scenarios to include (Default = all).
|
|
636
|
+
filter_blocks -- Blocks to include (Default = all).
|
|
637
|
+
Other keyword arguments, like encoding, are passed to open_bin or open_csv.
|
|
638
|
+
"""
|
|
639
|
+
if not _HAS_PANDAS:
|
|
640
|
+
raise ImportError(
|
|
641
|
+
"load_as_dataframe requires pandas. Install it with: pip install psr-graf[pandas]"
|
|
642
|
+
)
|
|
643
|
+
if index_format not in ("default", "period"):
|
|
644
|
+
raise ValueError(f"Invalid index_format: {index_format!r}. Expected 'default' or 'period'.")
|
|
645
|
+
|
|
646
|
+
filter_agents = tuple(filter_agents)
|
|
647
|
+
filter_stages = set(filter_stages)
|
|
648
|
+
filter_scenarios = set(filter_scenarios)
|
|
649
|
+
filter_blocks = set(filter_blocks)
|
|
650
|
+
|
|
651
|
+
_, ext = os.path.splitext(file_path)
|
|
652
|
+
open_fn = open_csv if ext.lower() == ".csv" else open_bin
|
|
653
|
+
with open_fn(file_path, **kwargs) as graf_file:
|
|
654
|
+
agents_index = list(_get_agent_index_filter(graf_file.agents, filter_agents))
|
|
655
|
+
df_agents = [graf_file.agents[i] for i in agents_index]
|
|
656
|
+
scenarios = [
|
|
657
|
+
scenario
|
|
658
|
+
for scenario in range(1, graf_file.scenarios + 1)
|
|
659
|
+
if not filter_scenarios or scenario in filter_scenarios
|
|
660
|
+
]
|
|
661
|
+
|
|
662
|
+
# Read each stage at once, then select its scenarios, blocks, and agents.
|
|
663
|
+
stage_columns, scenario_columns, block_columns, values = [], [], [], []
|
|
664
|
+
for stage in range(graf_file.min_stage, graf_file.max_stage + 1):
|
|
665
|
+
if filter_stages and stage not in filter_stages:
|
|
666
|
+
continue
|
|
667
|
+
blocks = [
|
|
668
|
+
block
|
|
669
|
+
for block in range(1, graf_file.blocks(stage) + 1)
|
|
670
|
+
if not filter_blocks or block in filter_blocks
|
|
671
|
+
]
|
|
672
|
+
if not scenarios or not blocks:
|
|
673
|
+
continue
|
|
674
|
+
stage_values = graf_file._read_stage_array(stage, scenarios[0], scenarios[-1])
|
|
675
|
+
selection = np.ix_(
|
|
676
|
+
np.subtract(scenarios, scenarios[0]), np.subtract(blocks, 1), agents_index
|
|
677
|
+
)
|
|
678
|
+
values.append(stage_values[selection].reshape((-1, len(agents_index))))
|
|
679
|
+
stage_columns.append(np.full(len(scenarios) * len(blocks), stage, dtype=np.int64))
|
|
680
|
+
scenario_columns.append(np.repeat(np.array(scenarios, dtype=np.int64), len(blocks)))
|
|
681
|
+
block_columns.append(np.tile(np.array(blocks, dtype=np.int64), len(scenarios)))
|
|
682
|
+
|
|
683
|
+
if values:
|
|
684
|
+
values = np.concatenate(values).astype(np.float64, copy=False)
|
|
685
|
+
stage_column = np.concatenate(stage_columns)
|
|
686
|
+
scenario_column = np.concatenate(scenario_columns)
|
|
687
|
+
block_column = np.concatenate(block_columns)
|
|
688
|
+
else:
|
|
689
|
+
values = np.empty((0, len(agents_index)), dtype=np.float64)
|
|
690
|
+
stage_column = scenario_column = block_column = np.empty(0, dtype=np.int64)
|
|
691
|
+
|
|
692
|
+
block_or_hour = BinReader.BLOCK_DESCRIPTION[graf_file.hour_or_block]
|
|
693
|
+
if index_format == "default":
|
|
694
|
+
index_columns = ("stage", "scenario", block_or_hour)
|
|
695
|
+
index_arrays = [stage_column, scenario_column, block_column]
|
|
696
|
+
else:
|
|
697
|
+
if graf_file.stage_type == _GrafReaderBase.STAGE_TYPE_MONTHLY:
|
|
698
|
+
month_or_week = "month"
|
|
699
|
+
max_periods = 12
|
|
700
|
+
else:
|
|
701
|
+
month_or_week = "week"
|
|
702
|
+
max_periods = 52
|
|
703
|
+
period = stage_column + graf_file.initial_stage - 2
|
|
704
|
+
index_columns = ("year", month_or_week, "scenario", block_or_hour)
|
|
705
|
+
index_arrays = [
|
|
706
|
+
period // max_periods + graf_file.initial_year,
|
|
707
|
+
period % max_periods + 1,
|
|
708
|
+
scenario_column,
|
|
709
|
+
block_column,
|
|
710
|
+
]
|
|
711
|
+
|
|
712
|
+
if multi_index:
|
|
713
|
+
index = pd.MultiIndex.from_arrays(index_arrays, names=index_columns)
|
|
714
|
+
return pd.DataFrame(values, index=index, columns=df_agents)
|
|
715
|
+
index_df = pd.DataFrame(dict(zip(index_columns, index_arrays, strict=True)))
|
|
716
|
+
return pd.concat([index_df, pd.DataFrame(values, columns=df_agents)], axis=1)
|
psr/graf/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: psr-graf
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: Utility module to read PSR SDDP's hdr/bin result file pairs.
|
|
5
|
+
Project-URL: Homepage, https://github.com/psrenergy/pygraf
|
|
6
|
+
Project-URL: Issues, https://github.com/psrenergy/pygraf/issues
|
|
7
|
+
Author-email: PSR <d.parrini@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: pandas
|
|
23
|
+
Requires-Dist: numpy; extra == 'pandas'
|
|
24
|
+
Requires-Dist: pandas; extra == 'pandas'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
PyGraf
|
|
28
|
+
======
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Utility module to read SDDP hdr/bin result file pairs. Some [examples](#usage-samples) to convert it to
|
|
32
|
+
popular formats are available.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Installing
|
|
36
|
+
----------
|
|
37
|
+
|
|
38
|
+
Install the latest version from [PyPI](https://pypi.org/project/psr-graf/)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install psr-graf
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To use `load_as_dataframe`, install it with the optional `pandas` dependency:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install "psr-graf[pandas]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or download this repository contents.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
Usage
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
Start by importing `psr.graf` module. It's possible to read data directly using `open_bin` and `open_csv` functions
|
|
57
|
+
or `load_as_dataframe` function if `pandas` package is available.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
The example below shows how to load data directly into a `pandas.DataFrame` and prints the first 5 lines of data.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import psr.graf
|
|
64
|
+
|
|
65
|
+
df = psr.graf.load_as_dataframe("sample_data/gerter.hdr")
|
|
66
|
+
print(df.head())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The output is:
|
|
70
|
+
```
|
|
71
|
+
Thermal 1 Thermal 2 Thermal 3
|
|
72
|
+
stage scenario block
|
|
73
|
+
1 1 1 7.440000 0.744 0.368069
|
|
74
|
+
2 1 6.437624 0.000 0.000000
|
|
75
|
+
3 1 7.440000 0.744 0.576140
|
|
76
|
+
4 1 7.440000 0.744 2.994997
|
|
77
|
+
5 1 7.440000 0.744 0.916644
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Alternatively, `open_bin` and `open_csv` functions can be used for direct data access as shown in the example next.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import psr.graf
|
|
84
|
+
|
|
85
|
+
with psr.graf.open_bin("sample_data/gerter.hdr") as graf_file:
|
|
86
|
+
print("Stages:", graf_file.stages)
|
|
87
|
+
print("Scenarios:", graf_file.scenarios)
|
|
88
|
+
print("Agents:", graf_file.agents)
|
|
89
|
+
print(f"Initial date: {graf_file.initial_year:04d}/{graf_file.initial_stage:02d}")
|
|
90
|
+
print("Units:", graf_file.units)
|
|
91
|
+
stage = 2
|
|
92
|
+
print(f"Number of blocks at stage {stage}: {graf_file.blocks(stage)}")
|
|
93
|
+
scenario = 10
|
|
94
|
+
block = 1
|
|
95
|
+
print(
|
|
96
|
+
f"Data at stage {stage}, scenario {scenario}, block {block}:",
|
|
97
|
+
graf_file.read(stage, scenario, block),
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The output is:
|
|
102
|
+
```
|
|
103
|
+
Stages: 12
|
|
104
|
+
Scenarios: 50
|
|
105
|
+
Agents: ('Thermal 1', 'Thermal 2', 'Thermal 3')
|
|
106
|
+
Initial date: 2013/01
|
|
107
|
+
Units: GWh
|
|
108
|
+
Number of blocks at stage 2: 1
|
|
109
|
+
Data at stage 2, scenario 10, block 1: (7.440000057220459, 0.7440000176429749, 0.3680693209171295)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Requirements
|
|
113
|
+
------------
|
|
114
|
+
|
|
115
|
+
* Python 3.10 or newer.
|
|
116
|
+
* (Optional) `pandas` package to use `load_as_dataframe` function, installed with the `pandas` extra.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
File Formats
|
|
120
|
+
------------
|
|
121
|
+
|
|
122
|
+
| File Extension | Description |
|
|
123
|
+
|:--------------:|:---------------------------------|
|
|
124
|
+
| .hdr or .bin | Binary .hdr and .bin pair |
|
|
125
|
+
| .dat | Single-binary file |
|
|
126
|
+
| .csv | CSV file with specific structure |
|
|
127
|
+
|
|
128
|
+
* `load_as_dataframe` supports all of them and will determine which reader will be used based on the file extension.
|
|
129
|
+
* `open_bin` supports only .hdr/bin pairs or single-binary files.
|
|
130
|
+
* `open_csv` supports only CSV.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
Both `open_bin`, `open_csv`, and `load_as_dataframe` functions accept `encoding` parameter to specify the encoding of the strings in file. The default is `utf-8`.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
DataFrame options
|
|
137
|
+
-----------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### MultiIndex or single index
|
|
141
|
+
|
|
142
|
+
`load_as_dataframe` accepts an optional keyword argument `multi_index` (default `True`) to specify if the
|
|
143
|
+
returned `pandas.DataFrame` should use `pandas.MultiIndex` or not. If `False`, the returned `pandas.DataFrame` will have a
|
|
144
|
+
single automatic index and the columns 'stage', 'scenario', 'block' will appear before the agents' data.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
```python
|
|
148
|
+
import psr.graf
|
|
149
|
+
|
|
150
|
+
df = psr.graf.load_as_dataframe("sample_data/gerter.hdr", multi_index=False)
|
|
151
|
+
print(df.head())
|
|
152
|
+
print("Column names:", df.columns.values)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The output is:
|
|
156
|
+
```
|
|
157
|
+
stage scenario block Thermal 1 Thermal 2 Thermal 3
|
|
158
|
+
0 1 1 1 7.440000 0.744 0.368069
|
|
159
|
+
1 1 2 1 6.437624 0.000 0.000000
|
|
160
|
+
2 1 3 1 7.440000 0.744 0.576140
|
|
161
|
+
3 1 4 1 7.440000 0.744 2.994997
|
|
162
|
+
4 1 5 1 7.440000 0.744 0.916644
|
|
163
|
+
Column names: ['stage' 'scenario' 'block' 'Thermal 1' 'Thermal 2' 'Thermal 3']
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
On the other hand,
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import psr.graf
|
|
170
|
+
|
|
171
|
+
df = psr.graf.load_as_dataframe("sample_data/gerter.hdr", multi_index=True)
|
|
172
|
+
print(df.head())
|
|
173
|
+
print("Column names:", df.columns.values)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Will produce the following output:
|
|
177
|
+
```
|
|
178
|
+
Thermal 1 Thermal 2 Thermal 3
|
|
179
|
+
stage scenario block
|
|
180
|
+
1 1 1 7.440000 0.744 0.368069
|
|
181
|
+
2 1 6.437624 0.000 0.000000
|
|
182
|
+
3 1 7.440000 0.744 0.576140
|
|
183
|
+
4 1 7.440000 0.744 2.994997
|
|
184
|
+
5 1 7.440000 0.744 0.916644
|
|
185
|
+
Column names: ['Thermal 1' 'Thermal 2' 'Thermal 3']
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Index formats
|
|
189
|
+
|
|
190
|
+
The `index_format` specifies the format of index columns of the returned `pandas.Dataframe`. It accepts the following
|
|
191
|
+
values:
|
|
192
|
+
|
|
193
|
+
| Index Format | Columns |
|
|
194
|
+
|:------------:|:---------------------------------------------|
|
|
195
|
+
| `default` | stage, scenario, block or hour |
|
|
196
|
+
| `period` | year, month or week, scenario, block or hour |
|
|
197
|
+
|
|
198
|
+
* `default` creates a `pandas.DataFrame` with the columns as they are stored in the original file.
|
|
199
|
+
* `period` converts `stage` into `year`, `month` or `week` depending on the stage type of the file and the
|
|
200
|
+
initial year and stage.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
### Filter by stage, scenario, block and/or agent
|
|
204
|
+
|
|
205
|
+
The `load_as_dataframe` function accepts the following optional keyword arguments to filter data:
|
|
206
|
+
|
|
207
|
+
| Argument | Description |
|
|
208
|
+
|:------------------:|:------------------------------------------------------------------------------|
|
|
209
|
+
| `filter_stages` | List of stages to be included |
|
|
210
|
+
| `filter_blocks` | List of blocks to be included |
|
|
211
|
+
| `filter_scenarios` | List of scenarios to be included |
|
|
212
|
+
| `filter_agents` | List of agents to be included. The name is case insensitive. |
|
|
213
|
+
|
|
214
|
+
An empty list or unspecified means all stages, blocks, scenarios or agents will be included.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
```python
|
|
218
|
+
import psr.graf
|
|
219
|
+
|
|
220
|
+
df = psr.graf.load_as_dataframe(
|
|
221
|
+
"sample_data/gerter.hdr",
|
|
222
|
+
filter_stages=[
|
|
223
|
+
8,
|
|
224
|
+
],
|
|
225
|
+
filter_scenarios=[
|
|
226
|
+
10,
|
|
227
|
+
],
|
|
228
|
+
filter_agents=["Thermal 3", "Thermal 2"],
|
|
229
|
+
)
|
|
230
|
+
print(df.head())
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Outputs:
|
|
234
|
+
```
|
|
235
|
+
Thermal 3 Thermal 2
|
|
236
|
+
stage scenario block
|
|
237
|
+
8 10 1 8.030441 0.0
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
Usage Samples
|
|
243
|
+
-------------
|
|
244
|
+
|
|
245
|
+
### [`dataframes_sample.py`](https://github.com/psrenergy/pygraf/blob/main/dataframes_sample.py)
|
|
246
|
+
|
|
247
|
+
Shows how to read data into `pandas.DataFrame`s.
|
|
248
|
+
|
|
249
|
+
Requires `pandas` package installed.
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
### [`matplotlib_sample.py`](https://github.com/psrenergy/pygraf/blob/main/matplotlib_sample.py)
|
|
253
|
+
|
|
254
|
+
Shows how to read data and plot data from hdr/bin file pairs.
|
|
255
|
+
|
|
256
|
+

|
|
257
|
+
|
|
258
|
+
Requires `matplotlib` package installed.
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
### [`csv_sample.py`](https://github.com/psrenergy/pygraf/blob/main/csv_sample.py)
|
|
263
|
+
|
|
264
|
+
Shows how to convert from hdr/bin file pairs to csv using `psr.graf` module.
|
|
265
|
+
|
|
266
|
+
This script can also be called from command line:
|
|
267
|
+
|
|
268
|
+
```bat
|
|
269
|
+
python csv_sample.py input_file.hdr output_file.csv
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Where `output_file.csv` is optional.
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
### [`parquet_sample.py`](https://github.com/psrenergy/pygraf/blob/main/parquet_sample.py)
|
|
276
|
+
|
|
277
|
+
Shows how to convert from hdr/bin file pairs to Apache Parquet format.
|
|
278
|
+
|
|
279
|
+
Requires `pyarrow` package installed.
|
|
280
|
+
|
|
281
|
+
This script can also be called from command line:
|
|
282
|
+
|
|
283
|
+
```bat
|
|
284
|
+
python parquet_sample.py input_file.hdr output_file.parquet
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Where `output_file.parquet` is optional.
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
Development
|
|
292
|
+
-----------
|
|
293
|
+
|
|
294
|
+
The project uses [uv](https://docs.astral.sh/uv/) for development. To set up an environment with the
|
|
295
|
+
development dependencies, run the tests, and lint and format the code:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
uv sync
|
|
299
|
+
uv run pytest
|
|
300
|
+
uv run ruff check
|
|
301
|
+
uv run ruff format
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
To build the source distribution and wheel into `dist/`:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
uv build
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
### Releasing
|
|
312
|
+
|
|
313
|
+
Update `__version__` in `psr/graf/graf.py`, then push a tag with the same version:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
git tag 2.3.0
|
|
317
|
+
git push origin 2.3.0
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
PyPI never accepts a version that was uploaded before, even after it is deleted, so versions like 2.2.0
|
|
321
|
+
cannot be used again.
|
|
322
|
+
|
|
323
|
+
The [Publish](.github/workflows/publish.yml) workflow runs the tests, builds the package, and
|
|
324
|
+
uploads it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/).
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
Issues and Support
|
|
328
|
+
------------------
|
|
329
|
+
|
|
330
|
+
Check PyGraf's [GitHub repository](https://github.com/psrenergy/pygraf/) and [issues page](https://github.com/psrenergy/pygraf/issues) for support.
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
License
|
|
334
|
+
-------
|
|
335
|
+
|
|
336
|
+
PyGraf is licensed under the [MIT License](https://github.com/psrenergy/pygraf/blob/main/LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
psr/graf/__init__.py,sha256=888LVNjnDSeRrCRyMmKzoUH5fdqCu7xPHjS-ev10vDE,34
|
|
2
|
+
psr/graf/graf.py,sha256=yii0D2DAG7hWvJx3lexJJZTw4TrQmsYmRcbXUCd2pqY,25788
|
|
3
|
+
psr/graf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
psr_graf-2.3.0.dist-info/METADATA,sha256=KvquCR1F5Ulp6dBG7Rw_3rkVq6x_uYbo9UDrvSYdzts,10012
|
|
5
|
+
psr_graf-2.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
6
|
+
psr_graf-2.3.0.dist-info/licenses/LICENSE,sha256=EEHshT2r876fXUsEDuEgtvH9bPs0PUbexFqsEujBLRM,1060
|
|
7
|
+
psr_graf-2.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 PSR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|