idfpy 25.2.2__tar.gz → 25.2.3__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.
Files changed (99) hide show
  1. {idfpy-25.2.2 → idfpy-25.2.3}/PKG-INFO +1 -1
  2. idfpy-25.2.3/idfpy/ext/construction/__init__.py +7 -0
  3. idfpy-25.2.3/idfpy/ext/construction/functions.py +78 -0
  4. idfpy-25.2.3/idfpy/ext/construction/mixins.py +38 -0
  5. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/constructions.py +3 -1
  6. {idfpy-25.2.2 → idfpy-25.2.3}/pyproject.toml +1 -1
  7. idfpy-25.2.3/tests/test_ext_construction.py +136 -0
  8. {idfpy-25.2.2 → idfpy-25.2.3}/.github/workflows/publish-pypi.yml +0 -0
  9. {idfpy-25.2.2 → idfpy-25.2.3}/.github/workflows/release-version.yml +0 -0
  10. {idfpy-25.2.2 → idfpy-25.2.3}/.github/workflows/sync-energyplus.yml +0 -0
  11. {idfpy-25.2.2 → idfpy-25.2.3}/.gitignore +0 -0
  12. {idfpy-25.2.2 → idfpy-25.2.3}/.vscode/launch.json +0 -0
  13. {idfpy-25.2.2 → idfpy-25.2.3}/LICENSE +0 -0
  14. {idfpy-25.2.2 → idfpy-25.2.3}/README.md +0 -0
  15. {idfpy-25.2.2 → idfpy-25.2.3}/benchmarks/bench_idf.py +0 -0
  16. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/__init__.py +0 -0
  17. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/__main__.py +0 -0
  18. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/cli.py +0 -0
  19. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/__init__.py +0 -0
  20. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/field_parser.py +0 -0
  21. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/model_generator.py +0 -0
  22. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/schema_parser.py +0 -0
  23. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/template_filters.py +0 -0
  24. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/templates/idf_model_py.jinja2 +0 -0
  25. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/templates/init_py.jinja2 +0 -0
  26. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/templates/init_pyi.jinja2 +0 -0
  27. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/templates/ref_meta_py.jinja2 +0 -0
  28. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/codegen/templates/refs_py.jinja2 +0 -0
  29. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/ext/__init__.py +0 -0
  30. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/ext/geometry/__init__.py +0 -0
  31. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/ext/geometry/functions.py +0 -0
  32. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/ext/geometry/mixins.py +0 -0
  33. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/idf.py +0 -0
  34. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/__init__.py +0 -0
  35. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/__init__.pyi +0 -0
  36. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/_base.py +0 -0
  37. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/_errors.py +0 -0
  38. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/_metadata.py +0 -0
  39. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/_ref_meta.py +0 -0
  40. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/_refs.py +0 -0
  41. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/advanced_construction.py +0 -0
  42. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/air_distribution.py +0 -0
  43. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/availability_managers.py +0 -0
  44. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/coils.py +0 -0
  45. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/condensers.py +0 -0
  46. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/curves.py +0 -0
  47. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/daylighting.py +0 -0
  48. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/demand_limiting.py +0 -0
  49. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/economics.py +0 -0
  50. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/electric_load.py +0 -0
  51. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/ems.py +0 -0
  52. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/evap_coolers.py +0 -0
  53. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/external_interface.py +0 -0
  54. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/fans.py +0 -0
  55. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/faults.py +0 -0
  56. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/fluids.py +0 -0
  57. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/hvac_design.py +0 -0
  58. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/hvac_templates.py +0 -0
  59. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/internal_gains.py +0 -0
  60. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/location.py +0 -0
  61. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/misc.py +0 -0
  62. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/node_branch.py +0 -0
  63. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/outputs.py +0 -0
  64. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/plant_control.py +0 -0
  65. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/plant_equipment.py +0 -0
  66. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/pumps.py +0 -0
  67. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/python_plugins.py +0 -0
  68. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/refrigeration.py +0 -0
  69. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/room_air.py +0 -0
  70. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/schedules.py +0 -0
  71. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/setpoint_managers.py +0 -0
  72. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/simulation.py +0 -0
  73. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/solar.py +0 -0
  74. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/thermal_zones.py +0 -0
  75. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/unitary.py +0 -0
  76. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/user_defined.py +0 -0
  77. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/water_heaters.py +0 -0
  78. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/water_systems.py +0 -0
  79. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_airflow.py +0 -0
  80. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_controls.py +0 -0
  81. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_equipment.py +0 -0
  82. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_forced_air.py +0 -0
  83. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_radiative.py +0 -0
  84. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/models/zone_terminals.py +0 -0
  85. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/py.typed +0 -0
  86. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/sim/__init__.py +0 -0
  87. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/sim/_runner.py +0 -0
  88. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/sim/config.py +0 -0
  89. {idfpy-25.2.2 → idfpy-25.2.3}/idfpy/sim/result.py +0 -0
  90. {idfpy-25.2.2 → idfpy-25.2.3}/tests/__init__.py +0 -0
  91. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test.idf +0 -0
  92. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_api_unification.py +0 -0
  93. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_epjson_doe.py +0 -0
  94. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_ext.py +0 -0
  95. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_merge_dict.py +0 -0
  96. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_metadata.py +0 -0
  97. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_models.py +0 -0
  98. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_ref_system.py +0 -0
  99. {idfpy-25.2.2 → idfpy-25.2.3}/tests/test_typing_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: idfpy
3
- Version: 25.2.2
3
+ Version: 25.2.3
4
4
  Summary: Type-safe Pydantic models for all EnergyPlus IDF objects
5
5
  Project-URL: Repository, https://github.com/ITOTI-Y/idfpy
6
6
  Project-URL: Issues, https://github.com/ITOTI-Y/idfpy/issues
@@ -0,0 +1,7 @@
1
+ """Construction extension — ordered layers and window/opaque classification."""
2
+
3
+ from .mixins import ConstructionLayersMixin
4
+
5
+ MIXIN_MAP: dict[str, type] = {
6
+ 'Construction': ConstructionLayersMixin,
7
+ }
@@ -0,0 +1,78 @@
1
+ """Material classification and layer resolution for layered constructions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Final
6
+
7
+ from idfpy.models.constructions import (
8
+ WindowMaterialBlind,
9
+ WindowMaterialGas,
10
+ WindowMaterialGasMixture,
11
+ WindowMaterialGlazing,
12
+ WindowMaterialGlazingGroupThermochromic,
13
+ WindowMaterialGlazingRefractionExtinctionMethod,
14
+ WindowMaterialScreen,
15
+ WindowMaterialShade,
16
+ WindowMaterialSimpleGlazingSystem,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from idfpy.models._base import IDFBaseModel
21
+ from idfpy.models.constructions import Construction
22
+
23
+ # Window-material layer types accepted by ``Construction`` layer fields.
24
+ WINDOW_MATERIAL_TYPES: Final = (
25
+ WindowMaterialBlind,
26
+ WindowMaterialGas,
27
+ WindowMaterialGasMixture,
28
+ WindowMaterialGlazing,
29
+ WindowMaterialGlazingGroupThermochromic,
30
+ WindowMaterialGlazingRefractionExtinctionMethod,
31
+ WindowMaterialScreen,
32
+ WindowMaterialShade,
33
+ WindowMaterialSimpleGlazingSystem,
34
+ )
35
+
36
+
37
+ def is_window_material(material: IDFBaseModel) -> bool:
38
+ """Whether ``material`` is a window-material layer (glazing, gas or shading)."""
39
+ return isinstance(material, WINDOW_MATERIAL_TYPES)
40
+
41
+
42
+ def construction_layers(construction: Construction) -> list[IDFBaseModel]:
43
+ """Resolve the layers of a bound construction.
44
+
45
+ Args:
46
+ construction: Construction bound to an IDF container.
47
+
48
+ Returns:
49
+ Materials from outside to inside; repeated names yield repeated
50
+ entries and empty layer fields are skipped.
51
+
52
+ Raises:
53
+ RuntimeError: If the construction is not bound to an IDF.
54
+ LookupError: If a layer name does not resolve to any material.
55
+ """
56
+ c = construction
57
+ slots = (
58
+ (c.outside_layer, c.outside_layer_ref),
59
+ (c.layer_2, c.layer_2_ref),
60
+ (c.layer_3, c.layer_3_ref),
61
+ (c.layer_4, c.layer_4_ref),
62
+ (c.layer_5, c.layer_5_ref),
63
+ (c.layer_6, c.layer_6_ref),
64
+ (c.layer_7, c.layer_7_ref),
65
+ (c.layer_8, c.layer_8_ref),
66
+ (c.layer_9, c.layer_9_ref),
67
+ (c.layer_10, c.layer_10_ref),
68
+ )
69
+ layers: list[IDFBaseModel] = []
70
+ for name, material in slots:
71
+ if not name:
72
+ continue
73
+ if material is None:
74
+ raise LookupError(
75
+ f'Construction {c.name!r} references unknown material {name!r}'
76
+ )
77
+ layers.append(material)
78
+ return layers
@@ -0,0 +1,38 @@
1
+ """Layer access and classification mixin for ``Construction``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from idfpy.models._base import IDFBaseModel
9
+ from idfpy.models.constructions import Construction
10
+
11
+ # ``functions`` imports the window-material classes from
12
+ # ``idfpy.models.constructions``, which itself imports this mixin; the
13
+ # imports below are deferred to break that cycle.
14
+
15
+
16
+ class ConstructionLayersMixin:
17
+ """Ordered layer access and window/opaque classification."""
18
+
19
+ @property
20
+ def layers(self: Construction) -> list[IDFBaseModel]:
21
+ """Materials from outside to inside (see ``construction_layers``)."""
22
+ from .functions import construction_layers
23
+
24
+ return construction_layers(self)
25
+
26
+ @property
27
+ def is_window_construction(self: Construction) -> bool:
28
+ """Whether every layer is a window material (glazing, gas or shading)."""
29
+ from .functions import construction_layers, is_window_material
30
+
31
+ return all(is_window_material(m) for m in construction_layers(self))
32
+
33
+ @property
34
+ def is_opaque_construction(self: Construction) -> bool:
35
+ """Whether no layer is a window material."""
36
+ from .functions import construction_layers, is_window_material
37
+
38
+ return not any(is_window_material(m) for m in construction_layers(self))
@@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal # noqa: F401
11
11
 
12
12
  from pydantic import Field
13
13
 
14
+ from idfpy.ext.construction.mixins import ConstructionLayersMixin
15
+
14
16
  from ._base import IDFBaseModel
15
17
  from ._refs import (
16
18
  BivariateFunctionsRef,
@@ -107,7 +109,7 @@ class WindowMaterialGlazingGroupThermochromicTemperatureDataItem(IDFBaseModel):
107
109
  return idf._resolve_forward(v, ['GlazingMaterialName'])
108
110
 
109
111
 
110
- class Construction(IDFBaseModel):
112
+ class Construction(ConstructionLayersMixin, IDFBaseModel):
111
113
  """Start with outside layer and work your way to the inside layer Up to 10
112
114
  layers total, 8 for windows Enter the material name for each layer"""
113
115
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "idfpy"
7
- version = "25.2.2"
7
+ version = "25.2.3"
8
8
  description = "Type-safe Pydantic models for all EnergyPlus IDF objects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1,136 @@
1
+ """Tests for the construction ext plugin: layer resolution and classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ import idfpy.models as models
8
+ from idfpy import IDF
9
+ from idfpy.ext.construction.functions import is_window_material
10
+ from idfpy.models._ref_meta import REF_PROVIDERS
11
+ from idfpy.models.constructions import (
12
+ Construction,
13
+ Material,
14
+ WindowMaterialGas,
15
+ WindowMaterialGlazing,
16
+ WindowMaterialSimpleGlazingSystem,
17
+ )
18
+
19
+ OPAQUE_LAYER_TYPES = {
20
+ 'Material',
21
+ 'Material:AirGap',
22
+ 'Material:InfraredTransparent',
23
+ 'Material:NoMass',
24
+ 'Material:RoofVegetation',
25
+ }
26
+ WINDOW_LAYER_TYPES = {
27
+ 'WindowMaterial:Blind',
28
+ 'WindowMaterial:Gas',
29
+ 'WindowMaterial:GasMixture',
30
+ 'WindowMaterial:Glazing',
31
+ 'WindowMaterial:Glazing:RefractionExtinctionMethod',
32
+ 'WindowMaterial:GlazingGroup:Thermochromic',
33
+ 'WindowMaterial:Screen',
34
+ 'WindowMaterial:Shade',
35
+ 'WindowMaterial:SimpleGlazingSystem',
36
+ }
37
+
38
+
39
+ def _layer_material_classes() -> dict[str, type[models.IDFBaseModel]]:
40
+ layer_types = {
41
+ object_type
42
+ for object_type, fields in REF_PROVIDERS.items()
43
+ if any('MaterialName' in groups for _, groups in fields)
44
+ }
45
+ classes = (getattr(models, name) for name in models.__all__)
46
+ return {
47
+ cls.idf_object_type(): cls
48
+ for cls in classes
49
+ if isinstance(cls, type)
50
+ and issubclass(cls, models.IDFBaseModel)
51
+ and getattr(cls, '_idf_object_type', None) in layer_types
52
+ }
53
+
54
+
55
+ @pytest.fixture
56
+ def idf() -> IDF:
57
+ idf = IDF()
58
+ idf.add(
59
+ Material(
60
+ name='Brick',
61
+ roughness='Rough',
62
+ thickness=0.1,
63
+ conductivity=0.9,
64
+ density=1900.0,
65
+ specific_heat=800.0,
66
+ )
67
+ )
68
+ idf.add(
69
+ WindowMaterialGlazing(
70
+ name='Clear3mm', optical_data_type='SpectralAverage', thickness=0.003
71
+ )
72
+ )
73
+ idf.add(WindowMaterialGas(name='Air13mm', gas_type='Air', thickness=0.013))
74
+ idf.add(
75
+ WindowMaterialSimpleGlazingSystem(
76
+ name='SGS', u_factor=2.5, solar_heat_gain_coefficient=0.4
77
+ )
78
+ )
79
+ return idf
80
+
81
+
82
+ def _add(idf: IDF, name: str, *layers: str) -> Construction:
83
+ fields = ['outside_layer', *(f'layer_{i}' for i in range(2, 11))]
84
+ construction = Construction(name=name, **dict(zip(fields, layers, strict=False)))
85
+ idf.add(construction)
86
+ return construction
87
+
88
+
89
+ class TestIsWindowMaterial:
90
+ def test_partition_of_layer_material_types(self):
91
+ classes = _layer_material_classes()
92
+ assert set(classes) == OPAQUE_LAYER_TYPES | WINDOW_LAYER_TYPES
93
+
94
+ window = {
95
+ t for t, cls in classes.items() if is_window_material(cls.model_construct())
96
+ }
97
+ assert window == WINDOW_LAYER_TYPES
98
+
99
+
100
+ class TestLayers:
101
+ def test_order_and_repeats(self, idf: IDF):
102
+ construction = _add(idf, 'Dbl', 'Clear3mm', 'Air13mm', 'Clear3mm')
103
+ glass = idf.get(WindowMaterialGlazing, 'Clear3mm')
104
+ gas = idf.get(WindowMaterialGas, 'Air13mm')
105
+ assert construction.layers == [glass, gas, glass]
106
+
107
+ def test_unknown_material_raises(self, idf: IDF):
108
+ construction = _add(idf, 'Broken', 'Brick', 'Missing')
109
+ with pytest.raises(
110
+ LookupError, match="'Broken' references unknown material 'Missing'"
111
+ ):
112
+ _ = construction.layers
113
+
114
+ def test_unbound_raises(self):
115
+ construction = Construction(name='Loose', outside_layer='Brick')
116
+ with pytest.raises(RuntimeError):
117
+ _ = construction.layers
118
+
119
+
120
+ class TestClassification:
121
+ @pytest.mark.parametrize(
122
+ ('layers', 'is_window', 'is_opaque'),
123
+ [
124
+ (('Brick',), False, True),
125
+ (('SGS',), True, False),
126
+ (('Clear3mm', 'Air13mm', 'Clear3mm'), True, False),
127
+ (('Brick', 'Clear3mm'), False, False),
128
+ (('Clear3mm', 'Brick'), False, False),
129
+ ],
130
+ )
131
+ def test_window_and_opaque(
132
+ self, idf: IDF, layers: tuple[str, ...], is_window: bool, is_opaque: bool
133
+ ):
134
+ construction = _add(idf, 'C', *layers)
135
+ assert construction.is_window_construction is is_window
136
+ assert construction.is_opaque_construction is is_opaque
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes