energyplus-toolkit 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- energyplus_toolkit-0.2.2/LICENSE +21 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/PKG-INFO +23 -12
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/README.md +18 -9
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/__init__.py +1 -1
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_field.py +39 -22
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_field.py +75 -59
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_object.py +39 -54
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/PKG-INFO +23 -12
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/SOURCES.txt +1 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/pyproject.toml +5 -4
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/cli.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/format/__init__.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/format/cpp/__init__.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/format/cpp/unit_test_file.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/format/runner.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/__init__.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/__init__.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_constants.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_file.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_object.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_regex.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/__init__.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_constants.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_file.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_regex.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/dependency_links.txt +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/entry_points.txt +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/requires.txt +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/top_level.txt +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/setup.cfg +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/tests/test_cli.py +0 -0
- {energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/tests/test_layering.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Julien Marrec
|
|
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.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: energyplus-toolkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Tools for EnergyPlus input files: parse, write back and reformat IDD and IDF files, and the IDF in EnergyPlus C++ unit tests.
|
|
5
5
|
Author-email: Julien Marrec <contact@effibem.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://effibem.com
|
|
8
8
|
Project-URL: Repository, https://github.com/jmarrec/energyplus-toolkit
|
|
9
9
|
Project-URL: Issues, https://github.com/jmarrec/energyplus-toolkit/issues
|
|
10
10
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier:
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
20
21
|
Provides-Extra: test
|
|
21
22
|
Requires-Dist: pytest>=9.0.2; extra == "test"
|
|
22
23
|
Requires-Dist: pytest-cov>=7.0.0; extra == "test"
|
|
@@ -29,6 +30,7 @@ Requires-Dist: flake8>=7.3.0; extra == "dev"
|
|
|
29
30
|
Requires-Dist: flake8-docstrings>=1.7.0; extra == "dev"
|
|
30
31
|
Requires-Dist: black>=25.12.0; extra == "dev"
|
|
31
32
|
Requires-Dist: isort>=7.0.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
# energyplus-toolkit
|
|
34
36
|
|
|
@@ -42,8 +44,8 @@ Tools for [EnergyPlus](https://energyplus.net) input files:
|
|
|
42
44
|
- reformat IDF and IDD files, and the IDF embedded in EnergyPlus C++ unit tests, from the command line or as
|
|
43
45
|
pre-commit hooks.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
- GitHub: <https://github.com/jmarrec/energyplus-toolkit>
|
|
48
|
+
- PyPI: <https://pypi.org/project/energyplus-toolkit/>
|
|
47
49
|
|
|
48
50
|
## Installation
|
|
49
51
|
|
|
@@ -86,18 +88,22 @@ parametric file, C++ code building the IDF...) is left unchanged, with a warning
|
|
|
86
88
|
|
|
87
89
|
```yaml
|
|
88
90
|
- repo: https://github.com/jmarrec/energyplus-toolkit
|
|
89
|
-
rev: v0.2.
|
|
91
|
+
rev: v0.2.2
|
|
90
92
|
hooks:
|
|
91
93
|
- id: energyplus-format-idd
|
|
92
|
-
args: [
|
|
94
|
+
args: [-q]
|
|
95
|
+
# Example to limit to a specific file
|
|
96
|
+
files: ^idd/Energy\+\.idd\.in$
|
|
93
97
|
- id: energyplus-format-idf
|
|
94
98
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
95
99
|
- id: energyplus-format-unit-test
|
|
96
100
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
The IDF and unit test hooks need the IDD to use (`--idd`); the IDD hook doesn't take it.
|
|
104
|
+
`files` is optional: it limits a hook to the paths matching this regex, here the one IDD to format.
|
|
105
|
+
|
|
106
|
+
**Note:** For unit tests, put the hook before clang-format: reformatting then clang-format is stable.
|
|
101
107
|
|
|
102
108
|
## Library
|
|
103
109
|
|
|
@@ -113,7 +119,7 @@ from energyplus_toolkit.input.idf.idf_object import IdfObject
|
|
|
113
119
|
idd = IddFile.load(Path("Energy+.idd"))
|
|
114
120
|
|
|
115
121
|
# A whole IDF file: each object is linked to its IDD object
|
|
116
|
-
idf = IdfFile.load("in.idf", idd) # or IdfFile.loads(text, idd)
|
|
122
|
+
idf = IdfFile.load(Path("in.idf"), idd_file=idd) # or IdfFile.loads(text, idd)
|
|
117
123
|
for zone in idf.get_objects_by_type("Zone"):
|
|
118
124
|
print(zone.name, zone.getDouble(1), zone.getDouble(1, return_default=True)) # "Z2 None 0.0" if left blank
|
|
119
125
|
idf.save("out.idf", overwrite=True) # or str(idf)
|
|
@@ -125,20 +131,25 @@ print(zone)
|
|
|
125
131
|
# Zone,
|
|
126
132
|
# Z3, !- Name
|
|
127
133
|
# 45.0; !- Direction of Relative North {deg}
|
|
134
|
+
idf.add_object(obj=zone)
|
|
128
135
|
|
|
129
136
|
# Or built from scratch, with extensible groups
|
|
130
137
|
branch = IdfObject(idd_object=idd.getObject("Branch"))
|
|
131
138
|
branch.setString(0, "Supply Branch")
|
|
132
139
|
branch.push_extensible_group(["Pump:VariableSpeed", "Pump", "Pump-Inlet", "Pump-Outlet"])
|
|
133
140
|
|
|
141
|
+
idf.remove_object(zone)
|
|
142
|
+
idf.add_objects(objs=[zone, branch])
|
|
143
|
+
assert idf.objects[-2:] == [zone, branch]
|
|
144
|
+
|
|
134
145
|
# The IDD side
|
|
135
146
|
field = idd.getObject("Zone").fields[1]
|
|
136
147
|
print(field.name, field.properties.type, field.properties.units, field.properties.stringDefault)
|
|
137
148
|
# Direction of Relative North real deg 0
|
|
138
149
|
```
|
|
139
150
|
|
|
140
|
-
Values are validated against the IDD when set
|
|
141
|
-
(`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
151
|
+
Values are validated against the IDD when set: types, choices (the casing is normalized), bounds.
|
|
152
|
+
The IDF is written with its comments (`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
142
153
|
|
|
143
154
|
## Layout
|
|
144
155
|
|
|
@@ -10,8 +10,8 @@ Tools for [EnergyPlus](https://energyplus.net) input files:
|
|
|
10
10
|
- reformat IDF and IDD files, and the IDF embedded in EnergyPlus C++ unit tests, from the command line or as
|
|
11
11
|
pre-commit hooks.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
- GitHub: <https://github.com/jmarrec/energyplus-toolkit>
|
|
14
|
+
- PyPI: <https://pypi.org/project/energyplus-toolkit/>
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -54,18 +54,22 @@ parametric file, C++ code building the IDF...) is left unchanged, with a warning
|
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
56
|
- repo: https://github.com/jmarrec/energyplus-toolkit
|
|
57
|
-
rev: v0.2.
|
|
57
|
+
rev: v0.2.2
|
|
58
58
|
hooks:
|
|
59
59
|
- id: energyplus-format-idd
|
|
60
|
-
args: [
|
|
60
|
+
args: [-q]
|
|
61
|
+
# Example to limit to a specific file
|
|
62
|
+
files: ^idd/Energy\+\.idd\.in$
|
|
61
63
|
- id: energyplus-format-idf
|
|
62
64
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
63
65
|
- id: energyplus-format-unit-test
|
|
64
66
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
The IDF and unit test hooks need the IDD to use (`--idd`); the IDD hook doesn't take it.
|
|
70
|
+
`files` is optional: it limits a hook to the paths matching this regex, here the one IDD to format.
|
|
71
|
+
|
|
72
|
+
**Note:** For unit tests, put the hook before clang-format: reformatting then clang-format is stable.
|
|
69
73
|
|
|
70
74
|
## Library
|
|
71
75
|
|
|
@@ -81,7 +85,7 @@ from energyplus_toolkit.input.idf.idf_object import IdfObject
|
|
|
81
85
|
idd = IddFile.load(Path("Energy+.idd"))
|
|
82
86
|
|
|
83
87
|
# A whole IDF file: each object is linked to its IDD object
|
|
84
|
-
idf = IdfFile.load("in.idf", idd) # or IdfFile.loads(text, idd)
|
|
88
|
+
idf = IdfFile.load(Path("in.idf"), idd_file=idd) # or IdfFile.loads(text, idd)
|
|
85
89
|
for zone in idf.get_objects_by_type("Zone"):
|
|
86
90
|
print(zone.name, zone.getDouble(1), zone.getDouble(1, return_default=True)) # "Z2 None 0.0" if left blank
|
|
87
91
|
idf.save("out.idf", overwrite=True) # or str(idf)
|
|
@@ -93,20 +97,25 @@ print(zone)
|
|
|
93
97
|
# Zone,
|
|
94
98
|
# Z3, !- Name
|
|
95
99
|
# 45.0; !- Direction of Relative North {deg}
|
|
100
|
+
idf.add_object(obj=zone)
|
|
96
101
|
|
|
97
102
|
# Or built from scratch, with extensible groups
|
|
98
103
|
branch = IdfObject(idd_object=idd.getObject("Branch"))
|
|
99
104
|
branch.setString(0, "Supply Branch")
|
|
100
105
|
branch.push_extensible_group(["Pump:VariableSpeed", "Pump", "Pump-Inlet", "Pump-Outlet"])
|
|
101
106
|
|
|
107
|
+
idf.remove_object(zone)
|
|
108
|
+
idf.add_objects(objs=[zone, branch])
|
|
109
|
+
assert idf.objects[-2:] == [zone, branch]
|
|
110
|
+
|
|
102
111
|
# The IDD side
|
|
103
112
|
field = idd.getObject("Zone").fields[1]
|
|
104
113
|
print(field.name, field.properties.type, field.properties.units, field.properties.stringDefault)
|
|
105
114
|
# Direction of Relative North real deg 0
|
|
106
115
|
```
|
|
107
116
|
|
|
108
|
-
Values are validated against the IDD when set
|
|
109
|
-
(`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
117
|
+
Values are validated against the IDD when set: types, choices (the casing is normalized), bounds.
|
|
118
|
+
The IDF is written with its comments (`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
110
119
|
|
|
111
120
|
## Layout
|
|
112
121
|
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_field.py
RENAMED
|
@@ -272,6 +272,20 @@ class IddField:
|
|
|
272
272
|
# Where the group number goes in its name, for an extensible field
|
|
273
273
|
self._extensible_group_numbering: ExtensibleGroupNumbering | None = None
|
|
274
274
|
|
|
275
|
+
def _format_error(self, error_msg: str) -> str:
|
|
276
|
+
"""Prefix ``error_msg`` with where it happened. The field name isn't known yet at the start of parsing."""
|
|
277
|
+
where = f"In '{self.objectName}'"
|
|
278
|
+
if self.name:
|
|
279
|
+
where += f", field '{self.name}'"
|
|
280
|
+
return f"{where}: {error_msg}"
|
|
281
|
+
|
|
282
|
+
def _parse_float(self, tag: str, value: str) -> float:
|
|
283
|
+
"""Parse the numeric value of the ``tag`` property (default, minimum, maximum)."""
|
|
284
|
+
try:
|
|
285
|
+
return float(value)
|
|
286
|
+
except ValueError:
|
|
287
|
+
raise ValueError(self._format_error(f"Invalid \\{tag} value '{value}'")) from None
|
|
288
|
+
|
|
275
289
|
def _parse_property(self, text: str):
|
|
276
290
|
if not text:
|
|
277
291
|
return
|
|
@@ -306,7 +320,7 @@ class IddField:
|
|
|
306
320
|
if any(x in lower_text for x in ['autosize', 'autocalculate']):
|
|
307
321
|
self.properties.numericDefault = AUTOSIZE_VALUE
|
|
308
322
|
else:
|
|
309
|
-
self.properties.numericDefault =
|
|
323
|
+
self.properties.numericDefault = self._parse_float(tag=tag, value=string_default)
|
|
310
324
|
|
|
311
325
|
elif lower_text.startswith("deprecated"):
|
|
312
326
|
self.properties.deprecated = True
|
|
@@ -323,9 +337,7 @@ class IddField:
|
|
|
323
337
|
elif index == 'f':
|
|
324
338
|
if lower_text.startswith("field"):
|
|
325
339
|
# The name is on the field's first line ("A1 , \field Name"): another \field line is malformed
|
|
326
|
-
raise IddFieldParseError(
|
|
327
|
-
f"Duplicate \\field line '\\{text}' in field '{self.name}' of object '{self.objectName}'"
|
|
328
|
-
)
|
|
340
|
+
raise IddFieldParseError(self._format_error(f"Duplicate \\field line '\\{text}'"))
|
|
329
341
|
|
|
330
342
|
# === i ===
|
|
331
343
|
elif index == 'i':
|
|
@@ -338,7 +350,7 @@ class IddField:
|
|
|
338
350
|
if lower_text.startswith(tag := "key"):
|
|
339
351
|
key_name = text[len(tag) :].split('!')[0].strip()
|
|
340
352
|
if not key_name:
|
|
341
|
-
raise ValueError(
|
|
353
|
+
raise ValueError(self._format_error("Key name is empty"))
|
|
342
354
|
key_note = ""
|
|
343
355
|
if '!' in text:
|
|
344
356
|
key_note = text.split('!')[1].strip()
|
|
@@ -353,7 +365,7 @@ class IddField:
|
|
|
353
365
|
rest = rest[1:].strip()
|
|
354
366
|
else:
|
|
355
367
|
self.properties.minBoundType = BoundTypes.InclusiveBound
|
|
356
|
-
self.properties.minBoundValue =
|
|
368
|
+
self.properties.minBoundValue = self._parse_float(tag=tag, value=rest)
|
|
357
369
|
not_handled = False
|
|
358
370
|
elif lower_text.startswith(tag := "maximum"):
|
|
359
371
|
rest = text[len(tag) :].split('!')[0].strip()
|
|
@@ -362,11 +374,17 @@ class IddField:
|
|
|
362
374
|
rest = rest[1:].strip()
|
|
363
375
|
else:
|
|
364
376
|
self.properties.maxBoundType = BoundTypes.InclusiveBound
|
|
365
|
-
self.properties.maxBoundValue =
|
|
377
|
+
self.properties.maxBoundValue = self._parse_float(tag=tag, value=rest)
|
|
366
378
|
not_handled = False
|
|
367
379
|
|
|
368
|
-
elif lower_text.startswith("memo"):
|
|
369
|
-
|
|
380
|
+
elif lower_text.startswith(tag := "memo"):
|
|
381
|
+
LOG.warning(self._format_error("Memo property is deprecated; use Note property instead."))
|
|
382
|
+
note = text[len(tag) :].strip()
|
|
383
|
+
if not self.properties.note:
|
|
384
|
+
self.properties.note = note
|
|
385
|
+
else:
|
|
386
|
+
self.properties.note += "\n" + note
|
|
387
|
+
not_handled = False
|
|
370
388
|
# === n ===
|
|
371
389
|
elif index == 'n':
|
|
372
390
|
if lower_text.startswith(tag := "note"):
|
|
@@ -421,7 +439,7 @@ class IddField:
|
|
|
421
439
|
|
|
422
440
|
# === if none matched ===
|
|
423
441
|
if not_handled:
|
|
424
|
-
raise ValueError(f"Unknown field property text '{text}'
|
|
442
|
+
raise ValueError(self._format_error(f"Unknown field property text '{text}'"))
|
|
425
443
|
|
|
426
444
|
@staticmethod
|
|
427
445
|
def load(objectName: str, field_lines: list[str]) -> "IddField":
|
|
@@ -431,7 +449,7 @@ class IddField:
|
|
|
431
449
|
|
|
432
450
|
def _parse(self, field_lines: list[str]) -> None:
|
|
433
451
|
if not field_lines:
|
|
434
|
-
raise ValueError(
|
|
452
|
+
raise ValueError(self._format_error("No lines provided to parse for field"))
|
|
435
453
|
|
|
436
454
|
field_lines = [x.strip() for x in field_lines]
|
|
437
455
|
n_lines = len(field_lines)
|
|
@@ -439,7 +457,9 @@ class IddField:
|
|
|
439
457
|
line_number = 0
|
|
440
458
|
self.is_last_field = ';' in field_lines[line_number]
|
|
441
459
|
if '\\field' not in field_lines[line_number]:
|
|
442
|
-
raise IddFieldParseError(
|
|
460
|
+
raise IddFieldParseError(
|
|
461
|
+
self._format_error(f"First line of field does not contain '\\field': '{field_lines[line_number]}'")
|
|
462
|
+
)
|
|
443
463
|
self.field_id = field_lines[line_number].replace(',', ';').split(';', maxsplit=1)[0]
|
|
444
464
|
self.field_id = self.field_id.strip()
|
|
445
465
|
|
|
@@ -456,14 +476,14 @@ class IddField:
|
|
|
456
476
|
# Default numerics to real
|
|
457
477
|
self.properties.type = IddFieldType.RealType
|
|
458
478
|
else:
|
|
459
|
-
raise IddFieldParseError(f"Unknown field type identifier found: '{fieldTypeChar}'")
|
|
479
|
+
raise IddFieldParseError(self._format_error(f"Unknown field type identifier found: '{fieldTypeChar}'"))
|
|
460
480
|
|
|
461
481
|
# Parse each metadata comment line (\keyword ...)
|
|
462
482
|
while line_number < n_lines:
|
|
463
483
|
line = field_lines[line_number]
|
|
464
|
-
line_number += 1
|
|
465
484
|
if not line.startswith('\\'):
|
|
466
485
|
break
|
|
486
|
+
line_number += 1
|
|
467
487
|
|
|
468
488
|
# Delegate actual parsing
|
|
469
489
|
self._parse_property(text=line[1:])
|
|
@@ -474,19 +494,16 @@ class IddField:
|
|
|
474
494
|
line_number += 1
|
|
475
495
|
if not line or line.startswith('!'):
|
|
476
496
|
continue
|
|
477
|
-
raise IddFieldParseError(
|
|
478
|
-
f"Unexpected content found after field properties: '{line}', "
|
|
479
|
-
f"in field '{self.name}' of object '{self.objectName}'"
|
|
480
|
-
)
|
|
497
|
+
raise IddFieldParseError(self._format_error(f"Unexpected content found after field properties: '{line}'"))
|
|
481
498
|
|
|
482
499
|
# Validation
|
|
483
500
|
if self.properties.type == IddFieldType.ChoiceType and not self.keys:
|
|
484
|
-
LOG.error(
|
|
501
|
+
LOG.error(self._format_error("Field is of type choice but keys are empty"))
|
|
485
502
|
elif self.properties.type != IddFieldType.ChoiceType and self.keys:
|
|
486
|
-
LOG.error(
|
|
503
|
+
LOG.error(self._format_error("Field is not of type choice but has non-empty keys"))
|
|
487
504
|
|
|
488
505
|
if self.properties.type == IddFieldType.UnknownType:
|
|
489
|
-
raise IddFieldParseError(
|
|
506
|
+
raise IddFieldParseError(self._format_error("Field is of unknown type after parsing"))
|
|
490
507
|
|
|
491
508
|
def is_name_field(self) -> bool:
|
|
492
509
|
"""Return True if this field represents the name of the object.
|
|
@@ -511,7 +528,7 @@ class IddField:
|
|
|
511
528
|
``name_for_group`` appends it.
|
|
512
529
|
"""
|
|
513
530
|
if self.extensible:
|
|
514
|
-
raise ValueError(
|
|
531
|
+
raise ValueError(self._format_error("Field is already marked extensible"))
|
|
515
532
|
self.extensible = True
|
|
516
533
|
self._extensible_group_numbering = ExtensibleGroupNumbering.parse(name=self.name)
|
|
517
534
|
self.name = self._extensible_group_numbering.base_name
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_field.py
RENAMED
|
@@ -2,12 +2,16 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from enum import StrEnum
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from energyplus_toolkit.input.idd.idd_constants import AUTOSIZE_VALUE
|
|
7
8
|
from energyplus_toolkit.input.idd.idd_field import BoundTypes, IddField, IddFieldProperties, IddFieldType
|
|
8
9
|
from energyplus_toolkit.input.idf import idf_constants
|
|
9
10
|
from energyplus_toolkit.input.idf.idf_constants import format_idf_field
|
|
10
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from energyplus_toolkit.input.idf.idf_object import IdfObject
|
|
14
|
+
|
|
11
15
|
LOG = logging.getLogger("IdfField")
|
|
12
16
|
|
|
13
17
|
|
|
@@ -22,13 +26,31 @@ class FieldValidationError(ValueError):
|
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class IdfField(ABC):
|
|
25
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
29
|
+
def __init__(self, idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None) -> None:
|
|
26
30
|
self._value: str | float | int | None = None
|
|
27
31
|
self._default_value: str | float | int | None = None
|
|
28
32
|
self.idd_field: IddField = idd_field
|
|
29
33
|
self._default_value = idd_field.properties.stringDefault
|
|
30
34
|
self.is_last_field = idd_field.is_last_field
|
|
31
35
|
self.comment: str | None = None # user-provided field comment; None = use IDD field name
|
|
36
|
+
# The IdfObject that owns this field, to say where an error happened
|
|
37
|
+
self.parent: "IdfObject | None" = parent
|
|
38
|
+
# for an extensible field: its group in the parent
|
|
39
|
+
self.group_index: int | None = group_index
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
"""The field name, numbered by group for an extensible field: "Vertex 3 X-coordinate"."""
|
|
44
|
+
if self.group_index is None:
|
|
45
|
+
return self.idd_field.name
|
|
46
|
+
return self.idd_field.name_for_group(self.group_index)
|
|
47
|
+
|
|
48
|
+
def _format_error(self, error_msg: str) -> str:
|
|
49
|
+
"""Prefix ``error_msg`` with where it happened."""
|
|
50
|
+
where = f"In '{self.idd_field.objectName}'"
|
|
51
|
+
if self.parent is not None and (object_name := self.parent.name):
|
|
52
|
+
where += f" named '{object_name}'"
|
|
53
|
+
return f"{where}, field '{self.name}': {error_msg}"
|
|
32
54
|
|
|
33
55
|
@property
|
|
34
56
|
def required(self):
|
|
@@ -37,35 +59,37 @@ class IdfField(ABC):
|
|
|
37
59
|
return props.required and not props.stringDefault
|
|
38
60
|
|
|
39
61
|
@staticmethod
|
|
40
|
-
def from_idd_field(
|
|
62
|
+
def from_idd_field(
|
|
63
|
+
idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None
|
|
64
|
+
) -> "IdfField":
|
|
41
65
|
field_type = idd_field.properties.type
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
field_class: type[IdfField]
|
|
44
68
|
if field_type == IddFieldType.AlphaType:
|
|
45
|
-
|
|
69
|
+
field_class = AlphaField
|
|
46
70
|
elif field_type == IddFieldType.IntegerType:
|
|
47
|
-
|
|
71
|
+
field_class = IntegerField
|
|
48
72
|
elif field_type == IddFieldType.RealType:
|
|
49
|
-
|
|
73
|
+
field_class = RealField
|
|
50
74
|
elif field_type == IddFieldType.ChoiceType:
|
|
51
75
|
if (idd_field.objectName.lower(), idd_field.name.lower()) == ("schedule:week:compact", "daytype list"):
|
|
52
|
-
|
|
76
|
+
field_class = DayTypeListField
|
|
53
77
|
else:
|
|
54
|
-
|
|
78
|
+
field_class = ChoiceField
|
|
55
79
|
elif field_type == IddFieldType.NodeType:
|
|
56
|
-
|
|
80
|
+
field_class = NodeField
|
|
57
81
|
elif field_type == IddFieldType.ObjectListType:
|
|
58
|
-
|
|
82
|
+
field_class = ObjectListField
|
|
59
83
|
elif field_type == IddFieldType.ExternalListType:
|
|
60
|
-
|
|
84
|
+
field_class = ExternalListField
|
|
61
85
|
elif field_type == IddFieldType.URLType:
|
|
62
|
-
|
|
86
|
+
field_class = URLField
|
|
63
87
|
elif field_type == IddFieldType.HandleType:
|
|
64
|
-
|
|
88
|
+
field_class = HandleField
|
|
65
89
|
else:
|
|
66
|
-
raise ValueError(f"
|
|
90
|
+
raise ValueError(f"In '{idd_field.objectName}', field '{idd_field.name}': Unknown field type: {field_type}")
|
|
67
91
|
|
|
68
|
-
return
|
|
92
|
+
return field_class(idd_field=idd_field, parent=parent, group_index=group_index)
|
|
69
93
|
|
|
70
94
|
@abstractmethod
|
|
71
95
|
def setString(self, value: str):
|
|
@@ -74,7 +98,7 @@ class IdfField(ABC):
|
|
|
74
98
|
@abstractmethod
|
|
75
99
|
def validate(self):
|
|
76
100
|
if self.required and self.value(return_default=True) is None:
|
|
77
|
-
raise FieldValidationError("Required field with no default value")
|
|
101
|
+
raise FieldValidationError(self._format_error("Required field with no default value"))
|
|
78
102
|
|
|
79
103
|
def value(self, return_default: bool = False) -> str | float | int | None:
|
|
80
104
|
if self._value is not None:
|
|
@@ -88,7 +112,7 @@ class IdfField(ABC):
|
|
|
88
112
|
|
|
89
113
|
def reset(self):
|
|
90
114
|
if self.required and self.value(return_default=True) is None:
|
|
91
|
-
raise FieldValidationError("Cannot reset a required field with no default value")
|
|
115
|
+
raise FieldValidationError(self._format_error("Cannot reset a required field with no default value"))
|
|
92
116
|
self._value = None
|
|
93
117
|
|
|
94
118
|
def value_text(self) -> str:
|
|
@@ -96,12 +120,12 @@ class IdfField(ABC):
|
|
|
96
120
|
v = self.value(return_default=False)
|
|
97
121
|
return "" if v is None else str(v)
|
|
98
122
|
|
|
99
|
-
def text(self, is_last_field: bool = False
|
|
100
|
-
"""The field's IDF line.
|
|
123
|
+
def text(self, is_last_field: bool = False) -> str:
|
|
124
|
+
"""The field's IDF line."""
|
|
101
125
|
return format_idf_field(
|
|
102
126
|
value=self.value_text(),
|
|
103
127
|
is_last_field=is_last_field,
|
|
104
|
-
field_name=self.
|
|
128
|
+
field_name=self.name,
|
|
105
129
|
comment=self.comment,
|
|
106
130
|
units=self.idd_field.properties.units,
|
|
107
131
|
)
|
|
@@ -115,9 +139,6 @@ class IdfField(ABC):
|
|
|
115
139
|
|
|
116
140
|
|
|
117
141
|
class AlphaField(IdfField):
|
|
118
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
119
|
-
super().__init__(idd_field=idd_field)
|
|
120
|
-
|
|
121
142
|
def setString(self, value: str):
|
|
122
143
|
self._value = value.strip()
|
|
123
144
|
|
|
@@ -153,13 +174,13 @@ class DayTypeListField(AlphaField):
|
|
|
153
174
|
def setString(self, value: str):
|
|
154
175
|
v = value.strip()
|
|
155
176
|
if v and not any(day in v.upper() for day in self.DAY_TYPES):
|
|
156
|
-
raise FieldValidationError(f"
|
|
177
|
+
raise FieldValidationError(self._format_error(f"No valid day type in: {v}"))
|
|
157
178
|
self._value = v
|
|
158
179
|
|
|
159
180
|
|
|
160
181
|
class NumericField(IdfField):
|
|
161
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
162
|
-
super().__init__(idd_field=idd_field)
|
|
182
|
+
def __init__(self, idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None) -> None:
|
|
183
|
+
super().__init__(idd_field=idd_field, parent=parent, group_index=group_index)
|
|
163
184
|
self._value: float | int | None = None
|
|
164
185
|
|
|
165
186
|
self.autocalculatable: bool = False
|
|
@@ -204,8 +225,8 @@ class NumericField(IdfField):
|
|
|
204
225
|
# on an autosizable-only field, but switch it to the keyword the field actually supports.
|
|
205
226
|
if not self.autocalculatable:
|
|
206
227
|
if not self.autosizable:
|
|
207
|
-
raise FieldValidationError("Autocalculate not allowed")
|
|
208
|
-
LOG.warning(
|
|
228
|
+
raise FieldValidationError(self._format_error("Autocalculate not allowed"))
|
|
229
|
+
LOG.warning(self._format_error("Field is autosizable, not autocalculatable — switching to Autosize"))
|
|
209
230
|
self.autosize()
|
|
210
231
|
return
|
|
211
232
|
self._mode = NumericMode.AUTOCALCULATE
|
|
@@ -214,10 +235,8 @@ class NumericField(IdfField):
|
|
|
214
235
|
def autosize(self):
|
|
215
236
|
if not self.autosizable:
|
|
216
237
|
if not self.autocalculatable:
|
|
217
|
-
raise FieldValidationError("Autosize not allowed")
|
|
218
|
-
LOG.warning(
|
|
219
|
-
f"Field '{self.idd_field.name}' is autocalculatable, not autosizable — switching to Autocalculate"
|
|
220
|
-
)
|
|
238
|
+
raise FieldValidationError(self._format_error("Autosize not allowed"))
|
|
239
|
+
LOG.warning(self._format_error("Field is autocalculatable, not autosizable — switching to Autocalculate"))
|
|
221
240
|
self.autocalculate()
|
|
222
241
|
return
|
|
223
242
|
self._mode = NumericMode.AUTOSIZE
|
|
@@ -234,7 +253,7 @@ class NumericField(IdfField):
|
|
|
234
253
|
|
|
235
254
|
if not v:
|
|
236
255
|
if self.required:
|
|
237
|
-
raise FieldValidationError("Cannot set required numeric field to empty value")
|
|
256
|
+
raise FieldValidationError(self._format_error("Cannot set required numeric field to empty value"))
|
|
238
257
|
self._value = None
|
|
239
258
|
self._mode = NumericMode.NORMAL
|
|
240
259
|
return
|
|
@@ -258,9 +277,8 @@ class NumericField(IdfField):
|
|
|
258
277
|
if self._mode == NumericMode.NORMAL and self._value is not None:
|
|
259
278
|
self._validate_range(self._value)
|
|
260
279
|
|
|
261
|
-
@classmethod
|
|
262
280
|
@abstractmethod
|
|
263
|
-
def _parse_number(
|
|
281
|
+
def _parse_number(self, value: str) -> int | float:
|
|
264
282
|
"""Parse a string into a number of the appropriate type.
|
|
265
283
|
|
|
266
284
|
This should throw FieldValidationError if the string cannot be parsed.
|
|
@@ -271,29 +289,29 @@ class NumericField(IdfField):
|
|
|
271
289
|
if self.minimum_value is not None:
|
|
272
290
|
if self.min_bound_type == BoundTypes.InclusiveBound:
|
|
273
291
|
if value < self.minimum_value:
|
|
274
|
-
raise FieldValidationError(f"{value} must be >= {self.minimum_value}")
|
|
292
|
+
raise FieldValidationError(self._format_error(f"{value} must be >= {self.minimum_value}"))
|
|
275
293
|
elif self.min_bound_type == BoundTypes.ExclusiveBound:
|
|
276
294
|
if value <= self.minimum_value:
|
|
277
|
-
raise FieldValidationError(f"{value} must be > {self.minimum_value}")
|
|
295
|
+
raise FieldValidationError(self._format_error(f"{value} must be > {self.minimum_value}"))
|
|
278
296
|
|
|
279
297
|
if self.maximum_value is not None:
|
|
280
298
|
if self.max_bound_type == BoundTypes.InclusiveBound:
|
|
281
299
|
if value > self.maximum_value:
|
|
282
|
-
raise FieldValidationError(f"{value} must be <= {self.maximum_value}")
|
|
300
|
+
raise FieldValidationError(self._format_error(f"{value} must be <= {self.maximum_value}"))
|
|
283
301
|
elif self.max_bound_type == BoundTypes.ExclusiveBound:
|
|
284
302
|
if value >= self.maximum_value:
|
|
285
|
-
raise FieldValidationError(f"{value} must be < {self.maximum_value}")
|
|
303
|
+
raise FieldValidationError(self._format_error(f"{value} must be < {self.maximum_value}"))
|
|
286
304
|
|
|
287
305
|
def reset(self):
|
|
288
306
|
if self.required and self.value(return_default=True) is None:
|
|
289
|
-
raise FieldValidationError("Cannot reset a required field with no default value")
|
|
307
|
+
raise FieldValidationError(self._format_error("Cannot reset a required field with no default value"))
|
|
290
308
|
self._value = None
|
|
291
309
|
self._mode = NumericMode.NORMAL
|
|
292
310
|
|
|
293
311
|
|
|
294
312
|
class IntegerField(NumericField):
|
|
295
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
296
|
-
super().__init__(idd_field=idd_field)
|
|
313
|
+
def __init__(self, idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None) -> None:
|
|
314
|
+
super().__init__(idd_field=idd_field, parent=parent, group_index=group_index)
|
|
297
315
|
self._value: int | None = None
|
|
298
316
|
if self.minimum_value is not None:
|
|
299
317
|
self.minimum_value = int(self.minimum_value)
|
|
@@ -304,23 +322,22 @@ class IntegerField(NumericField):
|
|
|
304
322
|
|
|
305
323
|
def setInt(self, value: int) -> None:
|
|
306
324
|
if not isinstance(value, (float, int)):
|
|
307
|
-
raise TypeError("IntegerField value must be an int")
|
|
325
|
+
raise TypeError(self._format_error("IntegerField value must be an int"))
|
|
308
326
|
# Accept a float with no fractional part, eg 512.0, but don't truncate 512.5
|
|
309
327
|
if isinstance(value, float) and not value.is_integer(): # also False for nan and inf
|
|
310
|
-
raise FieldValidationError(f"Invalid integer: {value}")
|
|
328
|
+
raise FieldValidationError(self._format_error(f"Invalid integer: {value}"))
|
|
311
329
|
v = int(value)
|
|
312
330
|
self._validate_range(value=v)
|
|
313
331
|
self._value = v
|
|
314
332
|
self._mode = NumericMode.NORMAL
|
|
315
333
|
|
|
316
|
-
|
|
317
|
-
def _parse_number(cls, value: str) -> int:
|
|
334
|
+
def _parse_number(self, value: str) -> int:
|
|
318
335
|
"""Parse an integer, also accepting a float with no fractional part, eg "512.0".
|
|
319
336
|
|
|
320
337
|
Anything else is invalid. This is stricter than EnergyPlus, which rounds any number (2.1 -> 2).
|
|
321
338
|
"""
|
|
322
339
|
if "_" in value: # Python's int() and float() accept "1_000", IDF doesn't
|
|
323
|
-
raise FieldValidationError(f"Invalid integer: {value}")
|
|
340
|
+
raise FieldValidationError(self._format_error(f"Invalid integer: {value}"))
|
|
324
341
|
try:
|
|
325
342
|
return int(value)
|
|
326
343
|
except ValueError:
|
|
@@ -328,15 +345,15 @@ class IntegerField(NumericField):
|
|
|
328
345
|
try:
|
|
329
346
|
as_float = float(value)
|
|
330
347
|
except ValueError:
|
|
331
|
-
raise FieldValidationError(f"Invalid integer: {value}") from None
|
|
348
|
+
raise FieldValidationError(self._format_error(f"Invalid integer: {value}")) from None
|
|
332
349
|
if not as_float.is_integer(): # also False for nan and inf
|
|
333
|
-
raise FieldValidationError(f"Invalid integer: {value}")
|
|
350
|
+
raise FieldValidationError(self._format_error(f"Invalid integer: {value}"))
|
|
334
351
|
return int(as_float)
|
|
335
352
|
|
|
336
353
|
|
|
337
354
|
class RealField(NumericField):
|
|
338
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
339
|
-
super().__init__(idd_field=idd_field)
|
|
355
|
+
def __init__(self, idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None) -> None:
|
|
356
|
+
super().__init__(idd_field=idd_field, parent=parent, group_index=group_index)
|
|
340
357
|
self._value: float | None = None
|
|
341
358
|
self._written_as_integer = False # set by setString, see idf_constants.IDF_KEEP_INTEGER_REALS
|
|
342
359
|
|
|
@@ -346,7 +363,7 @@ class RealField(NumericField):
|
|
|
346
363
|
|
|
347
364
|
def setDouble(self, value: float) -> None:
|
|
348
365
|
if not isinstance(value, (float, int)):
|
|
349
|
-
raise TypeError("RealField value must be a float")
|
|
366
|
+
raise TypeError(self._format_error("RealField value must be a float"))
|
|
350
367
|
v = float(value)
|
|
351
368
|
self._validate_range(value=v)
|
|
352
369
|
self._value = v
|
|
@@ -363,17 +380,16 @@ class RealField(NumericField):
|
|
|
363
380
|
return str(int(v))
|
|
364
381
|
return super().value_text()
|
|
365
382
|
|
|
366
|
-
|
|
367
|
-
def _parse_number(cls, value: str) -> float:
|
|
383
|
+
def _parse_number(self, value: str) -> float:
|
|
368
384
|
try:
|
|
369
385
|
return float(value)
|
|
370
386
|
except ValueError:
|
|
371
|
-
raise FieldValidationError(f"Invalid float: {value}")
|
|
387
|
+
raise FieldValidationError(self._format_error(f"Invalid float: {value}")) from None
|
|
372
388
|
|
|
373
389
|
|
|
374
390
|
class ChoiceField(IdfField):
|
|
375
|
-
def __init__(self, idd_field: IddField) -> None:
|
|
376
|
-
super().__init__(idd_field=idd_field)
|
|
391
|
+
def __init__(self, idd_field: IddField, parent: "IdfObject | None" = None, group_index: int | None = None) -> None:
|
|
392
|
+
super().__init__(idd_field=idd_field, parent=parent, group_index=group_index)
|
|
377
393
|
|
|
378
394
|
self.valid_choices: set[str] = {k.name for k in idd_field.keys}
|
|
379
395
|
self._canonical_map: dict[str, str] = {k.lower(): k for k in self.valid_choices}
|
|
@@ -388,7 +404,7 @@ class ChoiceField(IdfField):
|
|
|
388
404
|
|
|
389
405
|
def setBool(self, value: bool):
|
|
390
406
|
if not self.is_boolean:
|
|
391
|
-
raise FieldValidationError("This choice field
|
|
407
|
+
raise FieldValidationError(self._format_error("This choice field is not a boolean field"))
|
|
392
408
|
self._value = "Yes" if value else "No"
|
|
393
409
|
|
|
394
410
|
def validate(self):
|
|
@@ -400,7 +416,7 @@ class ChoiceField(IdfField):
|
|
|
400
416
|
v = self._canonical_map.get(value.lower(), None)
|
|
401
417
|
if v is None:
|
|
402
418
|
raise FieldValidationError(
|
|
403
|
-
f"
|
|
419
|
+
self._format_error(f"Invalid choice: {value}. Valid choices are: {self.valid_choices}")
|
|
404
420
|
)
|
|
405
421
|
return v
|
|
406
422
|
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_object.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
|
-
from typing import NamedTuple
|
|
5
4
|
|
|
6
5
|
from energyplus_toolkit.input.idd.idd_constants import COMMENT_ONLY_OBJECT_NAME
|
|
7
6
|
from energyplus_toolkit.input.idd.idd_object import IddObject, IddObjectFormat
|
|
@@ -13,14 +12,6 @@ LOG = logging.getLogger("IdfObject")
|
|
|
13
12
|
LOG.setLevel(logging.INFO)
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
class FieldToWrite(NamedTuple):
|
|
17
|
-
"""A field, with the name for its "!-" comment: numbered by group for an extensible field
|
|
18
|
-
("Vertex 3 X-coordinate")."""
|
|
19
|
-
|
|
20
|
-
field: IdfField
|
|
21
|
-
name: str
|
|
22
|
-
|
|
23
|
-
|
|
24
15
|
_VERTEX_FIELD_RE = re.compile(r"Vertex ([0-9]+) ([XYZ])-coordinate", re.IGNORECASE)
|
|
25
16
|
|
|
26
17
|
|
|
@@ -48,48 +39,46 @@ def _vertex_line(fields: tuple[IdfField, IdfField, IdfField], vertex: str, is_la
|
|
|
48
39
|
return f"{indent}{values} !- X,Y,Z ==> Vertex {vertex}" + (f" {{{units}}}" if units else "")
|
|
49
40
|
|
|
50
41
|
|
|
51
|
-
def _vertices_format_lines(
|
|
42
|
+
def _vertices_format_lines(fields: list[IdfField]) -> list[str]:
|
|
52
43
|
"""\\format vertices: the X, Y and Z of a vertex on one line, the other fields one per line."""
|
|
53
|
-
last_field =
|
|
44
|
+
last_field = fields[-1]
|
|
54
45
|
lines = []
|
|
55
46
|
# groupby only groups CONSECUTIVE entries with the same key (unlike pandas), so the order is kept: the regular
|
|
56
47
|
# fields before, between and after the vertices each make their own group (key None), and each vertex its own
|
|
57
|
-
for vertex, group in itertools.groupby(
|
|
58
|
-
|
|
59
|
-
fields = tuple(entry.field for entry in group_entries)
|
|
48
|
+
for vertex, group in itertools.groupby(fields, key=lambda field: _vertex_number(field.name)):
|
|
49
|
+
group_fields = tuple(group)
|
|
60
50
|
# A custom comment ends up on the last field of a line: only Z's can be kept on the vertex line
|
|
61
|
-
if vertex is not None and len(
|
|
62
|
-
lines.append(_vertex_line(
|
|
51
|
+
if vertex is not None and len(group_fields) == 3 and all(f.comment is None for f in group_fields[:2]):
|
|
52
|
+
lines.append(_vertex_line(group_fields, vertex, is_last_field=group_fields[-1] is last_field))
|
|
63
53
|
else:
|
|
64
|
-
lines.extend(f.text(is_last_field=f is last_field
|
|
54
|
+
lines.extend(f.text(is_last_field=f is last_field) for f in group_fields)
|
|
65
55
|
return lines
|
|
66
56
|
|
|
67
57
|
|
|
68
|
-
def _compact_schedule_format_lines(
|
|
58
|
+
def _compact_schedule_format_lines(fields: list[IdfField]) -> list[str]:
|
|
69
59
|
"""\\format compactSchedule: an "Until: HH:MM" field shares its line with the value after it, like EnergyPlus'
|
|
70
60
|
Transition writes it: "Until: 24:00,1.0, !- Field 3". The other fields are one per line."""
|
|
71
|
-
last_field =
|
|
61
|
+
last_field = fields[-1]
|
|
72
62
|
lines = []
|
|
73
|
-
|
|
74
|
-
for
|
|
75
|
-
field = entry.field
|
|
63
|
+
fields_iter = iter(fields)
|
|
64
|
+
for field in fields_iter:
|
|
76
65
|
# A custom comment ends up on the last field of a line: the Until field's can't be kept if the value follows it.
|
|
77
66
|
# next() only takes the value when the Until field can share its line
|
|
78
67
|
if (
|
|
79
68
|
field.value_text().strip().lower().startswith("until")
|
|
80
69
|
and field.comment is None
|
|
81
|
-
and (value := next(
|
|
70
|
+
and (value := next(fields_iter, None)) is not None
|
|
82
71
|
):
|
|
83
72
|
lines.append(
|
|
84
73
|
format_idf_field(
|
|
85
|
-
value=f"{field.value_text()},{value.
|
|
86
|
-
is_last_field=value
|
|
87
|
-
field_name=
|
|
88
|
-
comment=value.
|
|
74
|
+
value=f"{field.value_text()},{value.value_text()}",
|
|
75
|
+
is_last_field=value is last_field,
|
|
76
|
+
field_name=field.name,
|
|
77
|
+
comment=value.comment,
|
|
89
78
|
)
|
|
90
79
|
)
|
|
91
80
|
else:
|
|
92
|
-
lines.append(field.text(is_last_field=field is last_field
|
|
81
|
+
lines.append(field.text(is_last_field=field is last_field))
|
|
93
82
|
return lines
|
|
94
83
|
|
|
95
84
|
|
|
@@ -102,28 +91,27 @@ _VALUES_PER_LINE = {IddObjectFormat.FluidProperty: 7, IddObjectFormat.Spectral:
|
|
|
102
91
|
_NUMBERED_FIELD_RE = re.compile(r"[0-9]+$")
|
|
103
92
|
|
|
104
93
|
|
|
105
|
-
def _values_per_line_format_lines(
|
|
94
|
+
def _values_per_line_format_lines(fields: list[IdfField], per_line: int) -> list[str]:
|
|
106
95
|
"""\\format FluidProperty and Spectral: the data fields ``per_line`` values per line, without comments
|
|
107
96
|
("0.300,0.000,0.045,0.045,"), the other fields (name, property type...) one per line."""
|
|
108
|
-
last_field =
|
|
97
|
+
last_field = fields[-1]
|
|
109
98
|
indent = " " * (idf_constants.IDF_LEADING + idf_constants.IDF_INDENT)
|
|
110
99
|
lines: list[str] = []
|
|
111
100
|
# groupby only groups CONSECUTIVE entries with the same key: the fields before the data, then the data
|
|
112
|
-
for is_data, group in itertools.groupby(
|
|
113
|
-
|
|
101
|
+
for is_data, group in itertools.groupby(fields, key=lambda field: bool(_NUMBERED_FIELD_RE.search(field.name))):
|
|
102
|
+
group_fields = list(group)
|
|
114
103
|
if not is_data:
|
|
115
|
-
lines.extend(
|
|
104
|
+
lines.extend(f.text(is_last_field=f is last_field) for f in group_fields)
|
|
116
105
|
continue
|
|
117
|
-
for start in range(0, len(
|
|
118
|
-
chunk =
|
|
119
|
-
fields = [entry.field for entry in chunk]
|
|
106
|
+
for start in range(0, len(group_fields), per_line):
|
|
107
|
+
chunk = group_fields[start : start + per_line]
|
|
120
108
|
# A custom comment ends up on the last field of a line: only that one can be kept on a line of values
|
|
121
|
-
if any(f.comment is not None for f in
|
|
122
|
-
lines.extend(
|
|
109
|
+
if any(f.comment is not None for f in chunk[:-1]):
|
|
110
|
+
lines.extend(f.text(is_last_field=f is last_field) for f in chunk)
|
|
123
111
|
continue
|
|
124
|
-
line = indent + ",".join(f.value_text() for f in
|
|
125
|
-
if
|
|
126
|
-
line += f" ! {
|
|
112
|
+
line = indent + ",".join(f.value_text() for f in chunk) + (";" if chunk[-1] is last_field else ",")
|
|
113
|
+
if chunk[-1].comment is not None:
|
|
114
|
+
line += f" ! {chunk[-1].comment}"
|
|
127
115
|
lines.append(line)
|
|
128
116
|
return lines
|
|
129
117
|
|
|
@@ -151,7 +139,7 @@ class IdfObject:
|
|
|
151
139
|
self.idd_object: IddObject = idd_object
|
|
152
140
|
self.comment: str = ""
|
|
153
141
|
# Fixed (non-extensible) fields, one per idd_object.fields entry.
|
|
154
|
-
self._fields: list[IdfField] = [IdfField.from_idd_field(f) for f in idd_object.fields]
|
|
142
|
+
self._fields: list[IdfField] = [IdfField.from_idd_field(f, parent=self) for f in idd_object.fields]
|
|
155
143
|
# Each inner list is one extensible group, sized to numExtensible.
|
|
156
144
|
self._extensible_groups: list[list[IdfField]] = []
|
|
157
145
|
# Number of fixed fields included in serialisation. Starts at minFields,
|
|
@@ -166,7 +154,10 @@ class IdfObject:
|
|
|
166
154
|
# ------------------------------------------------------------------
|
|
167
155
|
|
|
168
156
|
def _new_extensible_group(self) -> list[IdfField]:
|
|
169
|
-
|
|
157
|
+
group_index = len(self._extensible_groups)
|
|
158
|
+
return [
|
|
159
|
+
IdfField.from_idd_field(f, parent=self, group_index=group_index) for f in self.idd_object.extensible_fields
|
|
160
|
+
]
|
|
170
161
|
|
|
171
162
|
# ------------------------------------------------------------------
|
|
172
163
|
# Parsing
|
|
@@ -482,23 +473,17 @@ class IdfObject:
|
|
|
482
473
|
lines.append(f"{indent}{self.idd_object.name},{','.join(vals)};")
|
|
483
474
|
else:
|
|
484
475
|
lines.append(f"{indent}{self.idd_object.name},")
|
|
485
|
-
# Extensible fields are numbered by group, like in the IDD: "Vertex 1 X-coordinate", "Vertex 2 X-coordinate"
|
|
486
|
-
entries = [FieldToWrite(field=f, name=f.idd_field.name) for f in active_fixed] + [
|
|
487
|
-
FieldToWrite(field=f, name=f.idd_field.name_for_group(i))
|
|
488
|
-
for i, group in enumerate(self._extensible_groups)
|
|
489
|
-
for f in group
|
|
490
|
-
]
|
|
491
476
|
if self.idd_object.properties.format == IddObjectFormat.Vertices:
|
|
492
|
-
lines.extend(_vertices_format_lines(
|
|
477
|
+
lines.extend(_vertices_format_lines(all_fields))
|
|
493
478
|
elif self.idd_object.properties.format == IddObjectFormat.CompactSchedule:
|
|
494
|
-
lines.extend(_compact_schedule_format_lines(
|
|
479
|
+
lines.extend(_compact_schedule_format_lines(all_fields))
|
|
495
480
|
elif self.idd_object.properties.format in _VALUES_PER_LINE:
|
|
496
481
|
lines.extend(
|
|
497
|
-
_values_per_line_format_lines(
|
|
482
|
+
_values_per_line_format_lines(all_fields, _VALUES_PER_LINE[self.idd_object.properties.format])
|
|
498
483
|
)
|
|
499
484
|
else:
|
|
500
|
-
for i,
|
|
501
|
-
lines.append(field.text(is_last_field=i == len(
|
|
485
|
+
for i, field in enumerate(all_fields):
|
|
486
|
+
lines.append(field.text(is_last_field=i == len(all_fields) - 1))
|
|
502
487
|
|
|
503
488
|
lines.append("") # blank line at end
|
|
504
489
|
return "\n".join(lines)
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: energyplus-toolkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Tools for EnergyPlus input files: parse, write back and reformat IDD and IDF files, and the IDF in EnergyPlus C++ unit tests.
|
|
5
5
|
Author-email: Julien Marrec <contact@effibem.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://effibem.com
|
|
8
8
|
Project-URL: Repository, https://github.com/jmarrec/energyplus-toolkit
|
|
9
9
|
Project-URL: Issues, https://github.com/jmarrec/energyplus-toolkit/issues
|
|
10
10
|
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier:
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
20
21
|
Provides-Extra: test
|
|
21
22
|
Requires-Dist: pytest>=9.0.2; extra == "test"
|
|
22
23
|
Requires-Dist: pytest-cov>=7.0.0; extra == "test"
|
|
@@ -29,6 +30,7 @@ Requires-Dist: flake8>=7.3.0; extra == "dev"
|
|
|
29
30
|
Requires-Dist: flake8-docstrings>=1.7.0; extra == "dev"
|
|
30
31
|
Requires-Dist: black>=25.12.0; extra == "dev"
|
|
31
32
|
Requires-Dist: isort>=7.0.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
32
34
|
|
|
33
35
|
# energyplus-toolkit
|
|
34
36
|
|
|
@@ -42,8 +44,8 @@ Tools for [EnergyPlus](https://energyplus.net) input files:
|
|
|
42
44
|
- reformat IDF and IDD files, and the IDF embedded in EnergyPlus C++ unit tests, from the command line or as
|
|
43
45
|
pre-commit hooks.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
- GitHub: <https://github.com/jmarrec/energyplus-toolkit>
|
|
48
|
+
- PyPI: <https://pypi.org/project/energyplus-toolkit/>
|
|
47
49
|
|
|
48
50
|
## Installation
|
|
49
51
|
|
|
@@ -86,18 +88,22 @@ parametric file, C++ code building the IDF...) is left unchanged, with a warning
|
|
|
86
88
|
|
|
87
89
|
```yaml
|
|
88
90
|
- repo: https://github.com/jmarrec/energyplus-toolkit
|
|
89
|
-
rev: v0.2.
|
|
91
|
+
rev: v0.2.2
|
|
90
92
|
hooks:
|
|
91
93
|
- id: energyplus-format-idd
|
|
92
|
-
args: [
|
|
94
|
+
args: [-q]
|
|
95
|
+
# Example to limit to a specific file
|
|
96
|
+
files: ^idd/Energy\+\.idd\.in$
|
|
93
97
|
- id: energyplus-format-idf
|
|
94
98
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
95
99
|
- id: energyplus-format-unit-test
|
|
96
100
|
args: [--idd, idd/Energy+.idd.in, -q]
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
The IDF and unit test hooks need the IDD to use (`--idd`); the IDD hook doesn't take it.
|
|
104
|
+
`files` is optional: it limits a hook to the paths matching this regex, here the one IDD to format.
|
|
105
|
+
|
|
106
|
+
**Note:** For unit tests, put the hook before clang-format: reformatting then clang-format is stable.
|
|
101
107
|
|
|
102
108
|
## Library
|
|
103
109
|
|
|
@@ -113,7 +119,7 @@ from energyplus_toolkit.input.idf.idf_object import IdfObject
|
|
|
113
119
|
idd = IddFile.load(Path("Energy+.idd"))
|
|
114
120
|
|
|
115
121
|
# A whole IDF file: each object is linked to its IDD object
|
|
116
|
-
idf = IdfFile.load("in.idf", idd) # or IdfFile.loads(text, idd)
|
|
122
|
+
idf = IdfFile.load(Path("in.idf"), idd_file=idd) # or IdfFile.loads(text, idd)
|
|
117
123
|
for zone in idf.get_objects_by_type("Zone"):
|
|
118
124
|
print(zone.name, zone.getDouble(1), zone.getDouble(1, return_default=True)) # "Z2 None 0.0" if left blank
|
|
119
125
|
idf.save("out.idf", overwrite=True) # or str(idf)
|
|
@@ -125,20 +131,25 @@ print(zone)
|
|
|
125
131
|
# Zone,
|
|
126
132
|
# Z3, !- Name
|
|
127
133
|
# 45.0; !- Direction of Relative North {deg}
|
|
134
|
+
idf.add_object(obj=zone)
|
|
128
135
|
|
|
129
136
|
# Or built from scratch, with extensible groups
|
|
130
137
|
branch = IdfObject(idd_object=idd.getObject("Branch"))
|
|
131
138
|
branch.setString(0, "Supply Branch")
|
|
132
139
|
branch.push_extensible_group(["Pump:VariableSpeed", "Pump", "Pump-Inlet", "Pump-Outlet"])
|
|
133
140
|
|
|
141
|
+
idf.remove_object(zone)
|
|
142
|
+
idf.add_objects(objs=[zone, branch])
|
|
143
|
+
assert idf.objects[-2:] == [zone, branch]
|
|
144
|
+
|
|
134
145
|
# The IDD side
|
|
135
146
|
field = idd.getObject("Zone").fields[1]
|
|
136
147
|
print(field.name, field.properties.type, field.properties.units, field.properties.stringDefault)
|
|
137
148
|
# Direction of Relative North real deg 0
|
|
138
149
|
```
|
|
139
150
|
|
|
140
|
-
Values are validated against the IDD when set
|
|
141
|
-
(`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
151
|
+
Values are validated against the IDD when set: types, choices (the casing is normalized), bounds.
|
|
152
|
+
The IDF is written with its comments (`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
|
|
142
153
|
|
|
143
154
|
## Layout
|
|
144
155
|
|
|
@@ -4,18 +4,19 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "energyplus-toolkit"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Tools for EnergyPlus input files: parse, write back and reformat IDD and IDF files, and the IDF in EnergyPlus C++ unit tests."
|
|
9
|
+
readme = "README.md"
|
|
9
10
|
authors = [
|
|
10
11
|
{name = "Julien Marrec", email = "contact@effibem.com"},
|
|
11
12
|
]
|
|
12
|
-
|
|
13
|
-
license =
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
14
15
|
requires-python = ">=3.11"
|
|
15
16
|
classifiers = [
|
|
16
17
|
"Development Status :: 2 - Pre-Alpha",
|
|
17
18
|
"Intended Audience :: Developers",
|
|
18
|
-
"
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
19
20
|
"Natural Language :: English",
|
|
20
21
|
"Programming Language :: Python :: 3",
|
|
21
22
|
"Programming Language :: Python :: 3.11",
|
|
File without changes
|
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/format/cpp/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/__init__.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_constants.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_file.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_object.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idd/idd_regex.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/__init__.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_constants.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_file.py
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit/input/idf/idf_regex.py
RENAMED
|
File without changes
|
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{energyplus_toolkit-0.2.0 → energyplus_toolkit-0.2.2}/energyplus_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|