idfpy 25.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. idfpy/__init__.py +23 -0
  2. idfpy/__main__.py +3 -0
  3. idfpy/cli.py +49 -0
  4. idfpy/codegen/__init__.py +4 -0
  5. idfpy/codegen/field_parser.py +264 -0
  6. idfpy/codegen/model_generator.py +645 -0
  7. idfpy/codegen/schema_parser.py +334 -0
  8. idfpy/codegen/template_filters.py +711 -0
  9. idfpy/codegen/templates/idf_model_py.jinja2 +104 -0
  10. idfpy/codegen/templates/init_py.jinja2 +114 -0
  11. idfpy/codegen/templates/ref_meta_py.jinja2 +48 -0
  12. idfpy/codegen/templates/refs_py.jinja2 +49 -0
  13. idfpy/ext/__init__.py +8 -0
  14. idfpy/ext/geometry/__init__.py +14 -0
  15. idfpy/ext/geometry/functions.py +189 -0
  16. idfpy/ext/geometry/mixins.py +100 -0
  17. idfpy/idf.py +1370 -0
  18. idfpy/models/__init__.py +18487 -0
  19. idfpy/models/_base.py +203 -0
  20. idfpy/models/_errors.py +54 -0
  21. idfpy/models/_metadata.py +116 -0
  22. idfpy/models/_ref_meta.py +10060 -0
  23. idfpy/models/_refs.py +982 -0
  24. idfpy/models/advanced_construction.py +3950 -0
  25. idfpy/models/air_distribution.py +711 -0
  26. idfpy/models/availability_managers.py +784 -0
  27. idfpy/models/coils.py +16932 -0
  28. idfpy/models/condensers.py +2734 -0
  29. idfpy/models/constructions.py +4644 -0
  30. idfpy/models/curves.py +1147 -0
  31. idfpy/models/daylighting.py +528 -0
  32. idfpy/models/demand_limiting.py +507 -0
  33. idfpy/models/economics.py +1417 -0
  34. idfpy/models/electric_load.py +3673 -0
  35. idfpy/models/ems.py +386 -0
  36. idfpy/models/evap_coolers.py +562 -0
  37. idfpy/models/external_interface.py +341 -0
  38. idfpy/models/fans.py +905 -0
  39. idfpy/models/faults.py +1165 -0
  40. idfpy/models/fluids.py +1528 -0
  41. idfpy/models/hvac_design.py +1120 -0
  42. idfpy/models/hvac_templates.py +9841 -0
  43. idfpy/models/internal_gains.py +3454 -0
  44. idfpy/models/location.py +1666 -0
  45. idfpy/models/misc.py +8223 -0
  46. idfpy/models/node_branch.py +905 -0
  47. idfpy/models/outputs.py +1733 -0
  48. idfpy/models/plant_control.py +3620 -0
  49. idfpy/models/plant_equipment.py +5276 -0
  50. idfpy/models/pumps.py +750 -0
  51. idfpy/models/python_plugins.py +236 -0
  52. idfpy/models/refrigeration.py +2644 -0
  53. idfpy/models/room_air.py +1315 -0
  54. idfpy/models/schedules.py +594 -0
  55. idfpy/models/setpoint_managers.py +1318 -0
  56. idfpy/models/simulation.py +672 -0
  57. idfpy/models/solar.py +711 -0
  58. idfpy/models/thermal_zones.py +3445 -0
  59. idfpy/models/unitary.py +2571 -0
  60. idfpy/models/user_defined.py +749 -0
  61. idfpy/models/water_heaters.py +2061 -0
  62. idfpy/models/water_systems.py +548 -0
  63. idfpy/models/zone_airflow.py +2030 -0
  64. idfpy/models/zone_controls.py +1117 -0
  65. idfpy/models/zone_equipment.py +580 -0
  66. idfpy/models/zone_forced_air.py +4335 -0
  67. idfpy/models/zone_radiative.py +2005 -0
  68. idfpy/models/zone_terminals.py +2099 -0
  69. idfpy/py.typed +0 -0
  70. idfpy/sim/__init__.py +173 -0
  71. idfpy/sim/_runner.py +189 -0
  72. idfpy/sim/config.py +17 -0
  73. idfpy/sim/result.py +81 -0
  74. idfpy-25.1.0.dist-info/METADATA +261 -0
  75. idfpy-25.1.0.dist-info/RECORD +78 -0
  76. idfpy-25.1.0.dist-info/WHEEL +4 -0
  77. idfpy-25.1.0.dist-info/entry_points.txt +2 -0
  78. idfpy-25.1.0.dist-info/licenses/LICENSE +21 -0
idfpy/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """idfpy - EnergyPlus IDF models and file handling.
2
+
3
+ Type-safe Pydantic models for all EnergyPlus IDF object types,
4
+ plus IDF file read/write functionality.
5
+
6
+ Generated from Energy+.schema.epJSON version 26.1.
7
+ """
8
+
9
+ from importlib.metadata import version
10
+
11
+ from idfpy.idf import IDF
12
+ from idfpy.models._base import IDFBaseModel
13
+ from idfpy.models._errors import RefError, RefValidationError, UnknownObjectTypeError
14
+
15
+ __version__ = version('idfpy')
16
+ __all__ = [
17
+ 'IDF',
18
+ 'IDFBaseModel',
19
+ 'RefError',
20
+ 'RefValidationError',
21
+ 'UnknownObjectTypeError',
22
+ '__version__',
23
+ ]
idfpy/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from idfpy.cli import app
2
+
3
+ app()
idfpy/cli.py ADDED
@@ -0,0 +1,49 @@
1
+ """idfpy command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ from typer import Option, Typer
9
+
10
+ app = Typer(name='idfpy', help='EnergyPlus IDF toolkit')
11
+
12
+
13
+ @app.command()
14
+ def codegen(
15
+ schema: Annotated[
16
+ Path, Option('--schema', '-s', help='Path to Energy+.schema.epJSON')
17
+ ],
18
+ output: Annotated[
19
+ Path, Option('--output', '-o', help='Output directory for generated models')
20
+ ] = Path('idfpy/models'),
21
+ ) -> None:
22
+ """Generate Pydantic models from EnergyPlus schema."""
23
+ from idfpy.codegen import ModelGenerator, SchemaParser
24
+
25
+ parser = SchemaParser(schema_path=schema)
26
+ specs = parser.parse()
27
+ schema_version = parser.get_version()
28
+ generator = ModelGenerator(output_dir=output)
29
+ generator.generate_all(specs, schema_version=schema_version)
30
+
31
+
32
+ @app.command()
33
+ def run(
34
+ idf: Annotated[Path, Option('--idf', '-i', help='Path to IDF file')],
35
+ weather: Annotated[
36
+ Path, Option('--weather', '-w', help='Path to EPW weather file')
37
+ ],
38
+ output: Annotated[
39
+ Path | None, Option('--output', '-o', help='Output directory')
40
+ ] = None,
41
+ readvars: Annotated[
42
+ bool, Option('--readvars', '-r', help='Run ReadVarsESO to convert ESO to CSV')
43
+ ] = False,
44
+ ) -> None:
45
+ """Run EnergyPlus simulation."""
46
+ from idfpy.sim import simulate
47
+
48
+ result = simulate(idf, weather=weather, output_dir=output, readvars=readvars)
49
+ raise SystemExit(result.return_code)
@@ -0,0 +1,4 @@
1
+ from .model_generator import ModelGenerator
2
+ from .schema_parser import SchemaParser
3
+
4
+ __all__ = ['ModelGenerator', 'SchemaParser']
@@ -0,0 +1,264 @@
1
+ """EnergyPlus JSON Schema parser for code generation.
2
+
3
+ This module parses Energy+.schema.epJSON to extract field and object
4
+ specifications for generating type-safe Pydantic models.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ _UNSET = object()
14
+
15
+
16
+ @dataclass
17
+ class FieldSpec:
18
+ """EnergyPlus field specification.
19
+
20
+ Represents a single field definition extracted from the schema,
21
+ including type information, constraints, and metadata.
22
+
23
+ Attributes:
24
+ name: Original field name from schema (e.g., "direction_of_relative_north").
25
+ python_name: Python-compatible name in snake_case.
26
+ field_type: JSON schema type ("number", "string", "integer", "array").
27
+ default: Default value if specified.
28
+ required: Whether the field is required.
29
+ enum_values: List of allowed values for enum fields.
30
+ units: Physical units (e.g., "m", "deg", "W").
31
+ minimum: Minimum allowed value (inclusive).
32
+ maximum: Maximum allowed value (inclusive).
33
+ exclusive_minimum: Exclusive minimum value.
34
+ exclusive_maximum: Exclusive maximum value.
35
+ object_list: Reference to other object types (for object_list fields).
36
+ items_spec: Nested FieldSpec for array item types.
37
+ note: Field description/note from schema.
38
+ data_type: Additional data type hint (e.g., "object_list").
39
+ anyof_specs: List of alternative type specs for anyOf fields.
40
+ nested_fields: List of nested field specs for object fields.
41
+ """
42
+
43
+ name: str
44
+ python_name: str
45
+ field_type: str
46
+ default: Any = _UNSET
47
+ required: bool = False
48
+ enum_values: list[str] | None = None
49
+ units: str | None = None
50
+ minimum: float | None = None
51
+ maximum: float | None = None
52
+ exclusive_minimum: float | None = None
53
+ exclusive_maximum: float | None = None
54
+ object_list: list[str] | None = None
55
+ reference: list[str] | None = None
56
+ reference_class_name: list[str] | None = None
57
+ items_spec: FieldSpec | None = None
58
+ item_class_name: str | None = None
59
+ note: str | None = None
60
+ data_type: str | None = None
61
+ anyof_specs: list[FieldSpec] | None = None
62
+ nested_fields: list[FieldSpec] | None = None
63
+ is_name: bool = False
64
+
65
+
66
+ class FieldParser:
67
+ """Parser for EnergyPlus schema field definitions.
68
+
69
+ Extracts FieldSpec instances from JSON schema field definitions,
70
+ handling various field types including nested objects and arrays.
71
+ """
72
+
73
+ _CAMEL_TO_SNAKE_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
74
+
75
+ def parse_field(self, name: str, field_schema: dict[str, Any]) -> FieldSpec:
76
+ """Parse a single field definition from schema.
77
+
78
+ Args:
79
+ name: Original field name.
80
+ field_schema: Field definition dictionary from schema.
81
+
82
+ Returns:
83
+ Parsed FieldSpec instance.
84
+ """
85
+ python_name = self._to_python_name(name)
86
+
87
+ if 'anyOf' in field_schema:
88
+ return self._parse_anyof_field(name, python_name, field_schema)
89
+
90
+ field_type = field_schema.get('type', 'string')
91
+
92
+ spec = FieldSpec(
93
+ name=name,
94
+ python_name=python_name,
95
+ field_type=field_type,
96
+ default=field_schema.get('default', _UNSET),
97
+ enum_values=field_schema.get('enum'),
98
+ units=field_schema.get('units'),
99
+ minimum=field_schema.get('minimum'),
100
+ maximum=field_schema.get('maximum'),
101
+ exclusive_minimum=field_schema.get('exclusiveMinimum'),
102
+ exclusive_maximum=field_schema.get('exclusiveMaximum'),
103
+ object_list=field_schema.get('object_list'),
104
+ reference=field_schema.get('reference'),
105
+ reference_class_name=field_schema.get('reference-class-name'),
106
+ note=field_schema.get('note'),
107
+ data_type=field_schema.get('data_type'),
108
+ )
109
+
110
+ # Normalize: string field with numeric default → convert to str
111
+ # (e.g., Version.version_identifier has type="string" but default=25.1)
112
+ if spec.field_type == 'string' and type(spec.default) in (int, float):
113
+ spec.default = str(spec.default)
114
+
115
+ if field_type == 'array' and 'items' in field_schema:
116
+ spec.items_spec = self._parse_array_items(field_schema['items'])
117
+
118
+ return spec
119
+
120
+ def _parse_anyof_field(
121
+ self, name: str, python_name: str, field_schema: dict[str, Any]
122
+ ) -> FieldSpec:
123
+ """Parse a field with anyOf type alternatives.
124
+
125
+ Args:
126
+ name: Original field name.
127
+ python_name: Python-compatible field name.
128
+ field_schema: Field definition with anyOf.
129
+
130
+ Returns:
131
+ FieldSpec with anyof_specs populated.
132
+ """
133
+ anyof_specs = []
134
+ primary_type = 'string' # Default fallback
135
+
136
+ for alt_schema in field_schema.get('anyOf', []):
137
+ alt_type = alt_schema.get('type', 'string')
138
+
139
+ if alt_type != 'null' and primary_type == 'string':
140
+ primary_type = alt_type
141
+
142
+ alt_spec = FieldSpec(
143
+ name=name,
144
+ python_name=python_name,
145
+ field_type=alt_type,
146
+ enum_values=alt_schema.get('enum'),
147
+ minimum=alt_schema.get('minimum'),
148
+ maximum=alt_schema.get('maximum'),
149
+ )
150
+ anyof_specs.append(alt_spec)
151
+
152
+ default = field_schema.get('default', _UNSET)
153
+ # Normalize: string field with numeric default → convert to str
154
+ if primary_type == 'string' and type(default) in (int, float):
155
+ default = str(default)
156
+
157
+ return FieldSpec(
158
+ name=name,
159
+ python_name=python_name,
160
+ field_type=primary_type,
161
+ default=default,
162
+ units=field_schema.get('units'),
163
+ reference=field_schema.get('reference'),
164
+ note=field_schema.get('note'),
165
+ anyof_specs=anyof_specs,
166
+ )
167
+
168
+ def _parse_array_items(self, items_schema: dict[str, Any]) -> FieldSpec:
169
+ """Parse array items specification.
170
+
171
+ Handles nested object definitions within array items,
172
+ such as vertices with x/y/z coordinates.
173
+
174
+ Args:
175
+ items_schema: Array items definition from schema.
176
+
177
+ Returns:
178
+ FieldSpec representing the array item type.
179
+ """
180
+ item_type = items_schema.get('type', 'object')
181
+
182
+ if item_type == 'object' and 'properties' in items_schema:
183
+ nested_fields = []
184
+ required_set = set(items_schema.get('required', []))
185
+ for prop_name, prop_schema in items_schema['properties'].items():
186
+ nested_spec = self.parse_field(prop_name, prop_schema)
187
+ nested_spec.required = prop_name in required_set
188
+ nested_fields.append(nested_spec)
189
+
190
+ return FieldSpec(
191
+ name='_item',
192
+ python_name='_item',
193
+ field_type='object',
194
+ nested_fields=nested_fields,
195
+ )
196
+
197
+ return FieldSpec(
198
+ name='_item',
199
+ python_name='_item',
200
+ field_type=item_type,
201
+ enum_values=items_schema.get('enum'),
202
+ minimum=items_schema.get('minimum'),
203
+ maximum=items_schema.get('maximum'),
204
+ )
205
+
206
+ def _to_python_name(self, name: str) -> str:
207
+ """Convert field name to Python snake_case.
208
+
209
+ Args:
210
+ name: Original field name (may contain spaces, hyphens, etc.).
211
+
212
+ Returns:
213
+ Python-compatible snake_case name.
214
+
215
+ Examples:
216
+ >>> parser = FieldParser()
217
+ >>> parser._to_python_name('direction_of_relative_north')
218
+ 'direction_of_relative_north'
219
+ >>> parser._to_python_name('X Origin')
220
+ 'x_origin'
221
+ >>> parser._to_python_name('vertex-x-coordinate')
222
+ 'vertex_x_coordinate'
223
+ >>> parser._to_python_name('100% Outdoor Air in Cooling')
224
+ 'n100_outdoor_air_in_cooling'
225
+ """
226
+ result = name.replace(' ', '_').replace('-', '_')
227
+
228
+ result = self._CAMEL_TO_SNAKE_PATTERN.sub('_', result)
229
+
230
+ result = result.lower()
231
+
232
+ result = re.sub(r'_+', '_', result)
233
+
234
+ result = result.strip('_')
235
+
236
+ # Python identifiers cannot start with a digit - prefix with 'n'
237
+ if result and result[0].isdigit():
238
+ result = 'n' + result
239
+
240
+ return result
241
+
242
+ def parse_fields_from_properties(
243
+ self,
244
+ properties: dict[str, Any],
245
+ required_fields: list[str] | None = None,
246
+ ) -> list[FieldSpec]:
247
+ """Parse all fields from a properties dictionary.
248
+
249
+ Args:
250
+ properties: Dictionary of field name to field schema.
251
+ required_fields: List of required field names.
252
+
253
+ Returns:
254
+ List of parsed FieldSpec instances.
255
+ """
256
+ required_set = set(required_fields or [])
257
+ fields = []
258
+
259
+ for name, schema in properties.items():
260
+ spec = self.parse_field(name, schema)
261
+ spec.required = name in required_set
262
+ fields.append(spec)
263
+
264
+ return fields