idfkit 1.1.0__tar.gz → 1.2.0__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 (357) hide show
  1. {idfkit-1.1.0 → idfkit-1.2.0}/.github/actions/setup-python-env/action.yml +6 -0
  2. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/benchmark.yml +6 -0
  3. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/main.yml +4 -0
  4. {idfkit-1.1.0 → idfkit-1.2.0}/CHANGELOG.md +78 -1
  5. {idfkit-1.1.0 → idfkit-1.2.0}/Makefile +1 -1
  6. {idfkit-1.1.0 → idfkit-1.2.0}/PKG-INFO +13 -14
  7. {idfkit-1.1.0 → idfkit-1.2.0}/README.md +12 -13
  8. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/document-and-objects.py +17 -0
  9. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/geometry-and-surfaces.py +34 -1
  10. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/geometry-builders-and-zoning.py +43 -2
  11. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/parsing-idf-epjson.py +16 -0
  12. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/simulation-execution.py +5 -2
  13. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/weather-data.py +17 -0
  14. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/SKILL.md +1 -0
  15. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/document-and-objects.md +11 -1
  16. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/geometry-and-surfaces.md +28 -1
  17. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/geometry-builders-and-zoning.md +32 -1
  18. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/parsing-idf-epjson.md +21 -0
  19. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/simulation-execution.md +5 -1
  20. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/weather-data.md +7 -1
  21. {idfkit-1.1.0 → idfkit-1.2.0}/pyproject.toml +4 -4
  22. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/check_naming_register.py +6 -5
  23. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/SKILL.md +1 -0
  24. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/document-and-objects.md +23 -1
  25. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/geometry-and-surfaces.md +47 -2
  26. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/geometry-builders-and-zoning.md +60 -3
  27. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/parsing-idf-epjson.md +27 -0
  28. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/simulation-execution.md +10 -3
  29. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/weather-data.md +19 -1
  30. idfkit-1.2.0/src/idfkit/_accessors.py +279 -0
  31. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/_conformance.py +1 -1
  32. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/_generated_types.pyi +23853 -1113
  33. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/generate_stubs.py +53 -23
  34. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/document.py +67 -11
  35. idfkit-1.2.0/src/idfkit/document.pyi +3621 -0
  36. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/exceptions.py +30 -1
  37. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/geometry.py +572 -43
  38. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/geometry_builders.py +114 -52
  39. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/idf_parser.py +77 -17
  40. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/objects.py +130 -8
  41. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schema.py +42 -5
  42. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/__init__.py +2 -1
  43. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/_common.py +7 -2
  44. idfkit-1.2.0/src/idfkit/simulation/_external_files.py +183 -0
  45. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/async_batch.py +8 -17
  46. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/async_runner.py +21 -7
  47. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/batch.py +18 -15
  48. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/cache.py +115 -2
  49. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/result.py +18 -7
  50. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/runner.py +21 -14
  51. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/surface_matching.py +3 -55
  52. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/visualization/model.py +36 -13
  53. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_cli.py +2 -1
  54. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_epw_sentinels.py +2 -2
  55. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/geocode.py +114 -20
  56. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/index.py +90 -17
  57. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/zoning.py +242 -33
  58. idfkit-1.2.0/tests/test_accessors.py +683 -0
  59. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_codegen.py +15 -0
  60. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_geometry.py +348 -9
  61. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_geometry_builders.py +55 -0
  62. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_idf_roundtrip.py +126 -1
  63. idfkit-1.2.0/tests/test_object_update.py +275 -0
  64. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_parsers.py +156 -0
  65. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_schema.py +26 -0
  66. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_async.py +61 -1
  67. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_batch.py +52 -1
  68. idfkit-1.2.0/tests/test_simulation_external_files.py +289 -0
  69. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_runner.py +136 -2
  70. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_surface_matching.py +40 -0
  71. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_visualization_model.py +133 -4
  72. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_cli.py +19 -0
  73. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_geocode.py +102 -12
  74. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_index.py +87 -0
  75. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_zoning.py +311 -7
  76. idfkit-1.2.0/tests/type_checks/update_fixture.py +49 -0
  77. {idfkit-1.1.0 → idfkit-1.2.0}/uv.lock +1 -1
  78. idfkit-1.1.0/src/idfkit/document.pyi +0 -696
  79. {idfkit-1.1.0 → idfkit-1.2.0}/.devcontainer/devcontainer.json +0 -0
  80. {idfkit-1.1.0 → idfkit-1.2.0}/.devcontainer/postCreateCommand.sh +0 -0
  81. {idfkit-1.1.0 → idfkit-1.2.0}/.github/CODEOWNERS +0 -0
  82. {idfkit-1.1.0 → idfkit-1.2.0}/.github/ep-testfiles.sparse +0 -0
  83. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/adopt-energyplus.yml +0 -0
  84. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/archive-retired-site.yml +0 -0
  85. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/conformance.yml +0 -0
  86. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/deploy-py-redirects.yml +0 -0
  87. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/notify-downstream.yml +0 -0
  88. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/on-release-main.yml +0 -0
  89. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/rehearse-all.yml +0 -0
  90. {idfkit-1.1.0 → idfkit-1.2.0}/.github/workflows/validate-codecov-config.yml +0 -0
  91. {idfkit-1.1.0 → idfkit-1.2.0}/.gitignore +0 -0
  92. {idfkit-1.1.0 → idfkit-1.2.0}/.pre-commit-config.yaml +0 -0
  93. {idfkit-1.1.0 → idfkit-1.2.0}/.pre-commit-hooks.yaml +0 -0
  94. {idfkit-1.1.0 → idfkit-1.2.0}/.python-version +0 -0
  95. {idfkit-1.1.0 → idfkit-1.2.0}/.vscode/extensions.json +0 -0
  96. {idfkit-1.1.0 → idfkit-1.2.0}/.vscode/settings.json +0 -0
  97. {idfkit-1.1.0 → idfkit-1.2.0}/AGENTS.md +0 -0
  98. {idfkit-1.1.0 → idfkit-1.2.0}/CLAUDE.md +0 -0
  99. {idfkit-1.1.0 → idfkit-1.2.0}/CONTRIBUTING.md +0 -0
  100. {idfkit-1.1.0 → idfkit-1.2.0}/LICENSE +0 -0
  101. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/hvac-loops.py +0 -0
  102. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/hvac-templates.py +0 -0
  103. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/reference-tracking.py +0 -0
  104. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/result-parsing.py +0 -0
  105. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/schedule-evaluation.py +0 -0
  106. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/schema-and-validation.py +0 -0
  107. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/thermal-properties.py +0 -0
  108. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/version-migration.py +0 -0
  109. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/visualization.py +0 -0
  110. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/snippets/writing-output.py +0 -0
  111. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/hvac-loops.md +0 -0
  112. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/hvac-templates.md +0 -0
  113. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/reference-tracking.md +0 -0
  114. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/result-parsing.md +0 -0
  115. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/schedule-evaluation.md +0 -0
  116. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/schema-and-validation.md +0 -0
  117. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/thermal-properties.md +0 -0
  118. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/version-migration.md +0 -0
  119. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/visualization.md +0 -0
  120. {idfkit-1.1.0 → idfkit-1.2.0}/agent_references/templates/writing-output.md +0 -0
  121. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/aggregate.py +0 -0
  122. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/bench.py +0 -0
  123. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/bench_eso.py +0 -0
  124. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/bench_progress.py +0 -0
  125. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/bench_typecheckers.py +0 -0
  126. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/eso_results.json +0 -0
  127. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/profile_field_access.py +0 -0
  128. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/profile_lookup.py +0 -0
  129. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/profile_overloads.py +0 -0
  130. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/profile_stubs.py +0 -0
  131. {idfkit-1.1.0 → idfkit-1.2.0}/benchmarks/progress_benchmark_results.json +0 -0
  132. {idfkit-1.1.0 → idfkit-1.2.0}/codecov.yaml +0 -0
  133. {idfkit-1.1.0 → idfkit-1.2.0}/old-sitemaps/README.md +0 -0
  134. {idfkit-1.1.0 → idfkit-1.2.0}/old-sitemaps/developers.idfkit.com.retired.txt +0 -0
  135. {idfkit-1.1.0 → idfkit-1.2.0}/old-sitemaps/developers.idfkit.com.txt +0 -0
  136. {idfkit-1.1.0 → idfkit-1.2.0}/old-sitemaps/js.idfkit.com.sitemap.xml +0 -0
  137. {idfkit-1.1.0 → idfkit-1.2.0}/old-sitemaps/py.idfkit.com.sitemap.xml +0 -0
  138. {idfkit-1.1.0 → idfkit-1.2.0}/redirects/README.md +0 -0
  139. {idfkit-1.1.0 → idfkit-1.2.0}/redirects/path-map.json +0 -0
  140. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/_governance_source.py +0 -0
  141. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/adopt_energyplus.py +0 -0
  142. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/build_redirect_site.py +0 -0
  143. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/build_weather_index.py +0 -0
  144. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/check_parity_ledger.py +0 -0
  145. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/check_redirects.py +0 -0
  146. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/check_release_conformance.py +0 -0
  147. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/check_release_version.py +0 -0
  148. {idfkit-1.1.0 → idfkit-1.2.0}/scripts/sweep_example_files.py +0 -0
  149. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/hvac-loops.md +0 -0
  150. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/hvac-templates.md +0 -0
  151. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/reference-tracking.md +0 -0
  152. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/result-parsing.md +0 -0
  153. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/schedule-evaluation.md +0 -0
  154. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/schema-and-validation.md +0 -0
  155. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/thermal-properties.md +0 -0
  156. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/version-migration.md +0 -0
  157. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/visualization.md +0 -0
  158. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/.agents/skills/developing-with-idfkit/references/writing-output.md +0 -0
  159. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/__init__.py +0 -0
  160. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/_compat.py +0 -0
  161. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/_compat_object.py +0 -0
  162. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/__init__.py +0 -0
  163. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/bake_references.py +0 -0
  164. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/generate_conformance.py +0 -0
  165. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/generate_doc_locations.py +0 -0
  166. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/codegen/generate_epw_sentinels.py +0 -0
  167. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/__init__.py +0 -0
  168. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_checker.py +0 -0
  169. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_cli.py +0 -0
  170. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_diff.py +0 -0
  171. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_extract.py +0 -0
  172. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_models.py +0 -0
  173. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/compat/_sarif.py +0 -0
  174. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/cst.py +0 -0
  175. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/docs.py +0 -0
  176. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/download.py +0 -0
  177. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/epjson_parser.py +0 -0
  178. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/introspection.py +0 -0
  179. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/__init__.py +0 -0
  180. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/async_runner.py +0 -0
  181. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/async_subprocess_backend.py +0 -0
  182. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/chain.py +0 -0
  183. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/diff.py +0 -0
  184. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/progress.py +0 -0
  185. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/protocol.py +0 -0
  186. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/report.py +0 -0
  187. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/runner.py +0 -0
  188. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/migration/subprocess_backend.py +0 -0
  189. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/py.typed +0 -0
  190. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/references.py +0 -0
  191. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/scene.py +0 -0
  192. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/__init__.py +0 -0
  193. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/builder.py +0 -0
  194. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/compact.py +0 -0
  195. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/day.py +0 -0
  196. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/day_types.py +0 -0
  197. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/evaluate.py +0 -0
  198. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/file.py +0 -0
  199. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/holidays.py +0 -0
  200. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/series.py +0 -0
  201. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/time_utils.py +0 -0
  202. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/types.py +0 -0
  203. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/week.py +0 -0
  204. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schedules/year.py +0 -0
  205. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V22-1-0/Energy+.schema.epJSON.gz +0 -0
  206. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V22-2-0/Energy+.schema.epJSON.gz +0 -0
  207. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V23-1-0/Energy+.schema.epJSON.gz +0 -0
  208. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V23-2-0/Energy+.schema.epJSON.gz +0 -0
  209. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V24-1-0/Energy+.schema.epJSON.gz +0 -0
  210. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V24-2-0/Energy+.schema.epJSON.gz +0 -0
  211. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V25-1-0/Energy+.schema.epJSON.gz +0 -0
  212. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V25-2-0/Energy+.schema.epJSON.gz +0 -0
  213. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V26-1-0/Energy+.schema.epJSON.gz +0 -0
  214. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V26-2-0/Energy+.schema.epJSON.gz +0 -0
  215. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V8-9-0/Energy+.schema.epJSON.gz +0 -0
  216. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-0-1/Energy+.schema.epJSON.gz +0 -0
  217. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-1-0/Energy+.schema.epJSON.gz +0 -0
  218. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-2-0/Energy+.schema.epJSON.gz +0 -0
  219. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-3-0/Energy+.schema.epJSON.gz +0 -0
  220. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-4-0/Energy+.schema.epJSON.gz +0 -0
  221. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-5-0/Energy+.schema.epJSON.gz +0 -0
  222. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/V9-6-0/Energy+.schema.epJSON.gz +0 -0
  223. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/schemas/doc_locations.json +0 -0
  224. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/config.py +0 -0
  225. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/expand.py +0 -0
  226. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/fs.py +0 -0
  227. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/outputs.py +0 -0
  228. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/__init__.py +0 -0
  229. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/_timeutil.py +0 -0
  230. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/csv.py +0 -0
  231. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/err.py +0 -0
  232. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/eso.py +0 -0
  233. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/html.py +0 -0
  234. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/rdd.py +0 -0
  235. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/parsers/sql.py +0 -0
  236. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/plotting/__init__.py +0 -0
  237. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/plotting/matplotlib.py +0 -0
  238. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/plotting/plotly.py +0 -0
  239. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/plotting/visualizations.py +0 -0
  240. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/progress.py +0 -0
  241. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/simulation/progress_bars.py +0 -0
  242. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/thermal/__init__.py +0 -0
  243. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/thermal/gas.py +0 -0
  244. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/thermal/properties.py +0 -0
  245. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/validation.py +0 -0
  246. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/versions.py +0 -0
  247. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/visualization/__init__.py +0 -0
  248. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/visualization/svg.py +0 -0
  249. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/__init__.py +0 -0
  250. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_browser/__init__.py +0 -0
  251. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_browser/assets/app.js +0 -0
  252. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_browser/assets/index.html +0 -0
  253. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_browser/assets/style.css +0 -0
  254. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/_browser/server.py +0 -0
  255. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/data/stations.json.gz +0 -0
  256. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/designday.py +0 -0
  257. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/download.py +0 -0
  258. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/epw.py +0 -0
  259. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/spatial.py +0 -0
  260. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/weather/station.py +0 -0
  261. {idfkit-1.1.0 → idfkit-1.2.0}/src/idfkit/writers.py +0 -0
  262. {idfkit-1.1.0 → idfkit-1.2.0}/tests/conftest.py +0 -0
  263. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/generate_eso_fixtures.py +0 -0
  264. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.csv +0 -0
  265. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.err +0 -0
  266. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.eso +0 -0
  267. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.mdd +0 -0
  268. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.mtr +0 -0
  269. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample.rdd +0 -0
  270. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample_fatal.err +0 -0
  271. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/simulation/sample_truncated.eso +0 -0
  272. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/weather/empty.ddy +0 -0
  273. {idfkit-1.1.0 → idfkit-1.2.0}/tests/fixtures/weather/sample.ddy +0 -0
  274. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/__init__.py +0 -0
  275. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_compact.py +0 -0
  276. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_day.py +0 -0
  277. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_day_types.py +0 -0
  278. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_evaluate.py +0 -0
  279. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_file.py +0 -0
  280. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_holidays.py +0 -0
  281. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_series.py +0 -0
  282. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_time_utils.py +0 -0
  283. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_types.py +0 -0
  284. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_week.py +0 -0
  285. {idfkit-1.1.0 → idfkit-1.2.0}/tests/schedules/test_year.py +0 -0
  286. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_adopt_energyplus.py +0 -0
  287. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_agent_references.py +0 -0
  288. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_basic.py +0 -0
  289. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_check_redirects.py +0 -0
  290. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_check_release_version.py +0 -0
  291. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_cli_migrate.py +0 -0
  292. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_compat_cli.py +0 -0
  293. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_compat_diff.py +0 -0
  294. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_compat_extract.py +0 -0
  295. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_docs.py +0 -0
  296. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_document.py +0 -0
  297. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_download.py +0 -0
  298. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_epjson_roundtrip.py +0 -0
  299. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_eppy_compat.py +0 -0
  300. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_exceptions.py +0 -0
  301. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_expand.py +0 -0
  302. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_extensible_canonical_api.py +0 -0
  303. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_extensible_eppy_compat.py +0 -0
  304. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_extensible_normalization.py +0 -0
  305. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_governance_source_matches.py +0 -0
  306. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_introspection.py +0 -0
  307. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_logging.py +0 -0
  308. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_async_runner.py +0 -0
  309. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_chain.py +0 -0
  310. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_config.py +0 -0
  311. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_diff.py +0 -0
  312. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_e2e.py +0 -0
  313. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_exceptions.py +0 -0
  314. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_report.py +0 -0
  315. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_runner.py +0 -0
  316. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_migration_subprocess_backend.py +0 -0
  317. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_objects.py +0 -0
  318. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_references.py +0 -0
  319. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_roundtrip.py +0 -0
  320. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_scene.py +0 -0
  321. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_schedule_builder.py +0 -0
  322. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_cache.py +0 -0
  323. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_config.py +0 -0
  324. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_csv_parser.py +0 -0
  325. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_e2e.py +0 -0
  326. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_err_parser.py +0 -0
  327. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_eso_parser.py +0 -0
  328. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_fs.py +0 -0
  329. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_html_parser.py +0 -0
  330. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_outputs.py +0 -0
  331. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_plotting.py +0 -0
  332. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_prep_outputs.py +0 -0
  333. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_preprocessor_timeout.py +0 -0
  334. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_progress.py +0 -0
  335. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_progress_bars.py +0 -0
  336. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_rdd_parser.py +0 -0
  337. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_result.py +0 -0
  338. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_sql_parser.py +0 -0
  339. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_simulation_version_mismatch.py +0 -0
  340. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_strict_typing.py +0 -0
  341. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_thermal.py +0 -0
  342. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_type_name_lookup.py +0 -0
  343. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_validation.py +0 -0
  344. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_versions.py +0 -0
  345. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_visualization.py +0 -0
  346. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_designday.py +0 -0
  347. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_download.py +0 -0
  348. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_offline.py +0 -0
  349. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_spatial.py +0 -0
  350. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_weather_station.py +0 -0
  351. {idfkit-1.1.0 → idfkit-1.2.0}/tests/test_writers.py +0 -0
  352. {idfkit-1.1.0 → idfkit-1.2.0}/tests/weather/__init__.py +0 -0
  353. {idfkit-1.1.0 → idfkit-1.2.0}/tests/weather/epw_fixtures.py +0 -0
  354. {idfkit-1.1.0 → idfkit-1.2.0}/tests/weather/test_epw.py +0 -0
  355. {idfkit-1.1.0 → idfkit-1.2.0}/tests/weather/test_epw_sentinels.py +0 -0
  356. {idfkit-1.1.0 → idfkit-1.2.0}/tests/weather/test_index_climate_zone.py +0 -0
  357. {idfkit-1.1.0 → idfkit-1.2.0}/tox.ini +0 -0
@@ -26,6 +26,12 @@ runs:
26
26
  cache-suffix: ${{ inputs.python-version }}
27
27
  prune-cache: false
28
28
 
29
+ # Later steps call `uv run` without --python, and uv would otherwise pick the runner's system
30
+ # interpreter (3.12 on ubuntu-latest), so every job would test 3.12 whatever its matrix entry.
31
+ - name: Pin uv to the requested interpreter for later steps
32
+ run: echo "UV_PYTHON=${{ inputs.python-version }}" >> "$GITHUB_ENV"
33
+ shell: bash
34
+
29
35
  - name: Install Python dependencies
30
36
  run: uv sync --frozen --python ${{ inputs.python-version }}
31
37
  shell: bash
@@ -74,7 +74,10 @@ jobs:
74
74
  with:
75
75
  ref: ${{ needs.setup.outputs.ref }}
76
76
 
77
+ # Pinned to the interpreter earlier benchmark runs measured on, so results stay comparable.
77
78
  - uses: ./.github/actions/setup-python-env
79
+ with:
80
+ python-version: "3.12"
78
81
 
79
82
  - name: Install benchmark deps
80
83
  run: uv sync --frozen --group benchmark
@@ -99,7 +102,10 @@ jobs:
99
102
  with:
100
103
  ref: ${{ needs.setup.outputs.ref }}
101
104
 
105
+ # Pinned to the interpreter earlier benchmark runs measured on, so results stay comparable.
102
106
  - uses: ./.github/actions/setup-python-env
107
+ with:
108
+ python-version: "3.12"
103
109
 
104
110
  - name: Install benchmark deps
105
111
  run: uv sync --frozen --group benchmark
@@ -22,8 +22,12 @@ jobs:
22
22
  path: ~/.cache/pre-commit
23
23
  key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
24
24
 
25
+ # The maintainer tools make check runs read TOML with tomllib (Python 3.11+). Pyright
26
+ # still checks the library against Python 3.10 (pythonVersion in pyproject.toml).
25
27
  - name: Set up the environment
26
28
  uses: ./.github/actions/setup-python-env
29
+ with:
30
+ python-version: "3.12"
27
31
 
28
32
  - name: Run checks
29
33
  run: make check
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-10-28
11
+
12
+ This release moves to `conformance-2026.21`. Through `conformance-2026.19`, the level the TypeScript library also declares, the corpus adds five cases for the multi-field `update` shared with the TypeScript library, an `update` operation and a `post-operation-epjson` assertion (`conformance-2026.17`), and removes the divergence entries for those cases in both languages: the ten Python entries that `IDFObject.update` closes (`conformance-2026.18`) and the six TypeScript ones (`conformance-2026.19`). `conformance-2026.20` then removes the two Python round-trip entries that #183 closes. An idfkit release without `IDFObject.update` fails the five update cases at this level, and one that still stores a blank fixed field as `""` fails those two. `conformance-2026.21` removes the three Python entries that #187 closes, so a release that still drops an unterminated final object fails `malformed-truncated-object` and `preserve-unterminated-final`.
13
+
14
+ ### Added
15
+
16
+ - **Every object type in a document's schema became reachable as a typed attribute
17
+ on `IDFDocument`, not only the hand-written shorthands.** Each type has one
18
+ `snake_case` plural, declared in `document.pyi`: `doc.air_loop_hvacs` is
19
+ `IDFCollection[AirLoopHVAC]`, and `doc.coil_cooling_dx_single_speeds` and
20
+ `doc.zone_hvac_equipment_connections` resolve likewise. Names are derived from
21
+ the schema by rule, with a short override table for the types the rule splits
22
+ badly (`Output:SQLite` is `doc.output_sqlite`), and a test fails when a new
23
+ release adds an acronym that has not been reviewed. Existing shorthands kept
24
+ their meaning, and the spellings that resolved before, such as
25
+ `doc.Site_Location` and `doc.ZONE`, still resolve. A failed lookup names the
26
+ closest matches and their object types; the message is built only when read, so
27
+ `hasattr()` and `getattr(doc, name, default)` stay fast
28
+ ([#202](https://github.com/idfkit/idfkit/pull/202)).
29
+ - `idfkit.geometry.polygon_difference_parts_2d(outer, *inners)` subtracts one or more polygons and returns hole-free, non-overlapping, counter-clockwise pieces, largest first, that together cover the difference. A result with a hole is cut by a vertical line through each hole. Every piece is a valid EnergyPlus surface outline or `create_block` footprint. ([#164](https://github.com/idfkit/idfkit/issues/164), [#244](https://github.com/idfkit/idfkit/pull/244))
30
+ - `idfkit.geometry.triangulate_2d(poly)` ear-clips a convex or concave 2D polygon into triangles, returned as index triples into the input in its own winding. It tolerates collinear and duplicate vertices, keyhole outlines with doubled bridge vertices and polygons pinched at a vertex, and it still uses every vertex of a self-intersecting polygon. The 3D viewers and `intersect_and_match` now share it. ([#246](https://github.com/idfkit/idfkit/pull/246))
31
+ - `idfkit.geometry_builders.split_horizontal_surface_by_regions(doc, surface, regions)` splits a horizontal surface under several regions in one step and returns `(region_surfaces, remaining_surfaces)`. `link_blocks` now uses it for each roof, so adjacent floors resting on one roof are subtracted together. ([#164](https://github.com/idfkit/idfkit/issues/164), [#244](https://github.com/idfkit/idfkit/pull/244))
32
+ - `idfkit.zoning.create_courtyard_block(doc, name, outer, courtyards, floor_to_floor, num_stories)` builds a block whose footprint has one or more courtyards cut out. The footprint is split into hole-free pieces with `polygon_difference_parts_2d`, each piece becomes one zone per story named `"<name> Part <k>"` (`"<name> Story <n> Part <k>"` for several stories), and the walls along the cuts between pieces are paired interior walls rather than exterior walls. ([#245](https://github.com/idfkit/idfkit/pull/245))
33
+ - `idfkit.zoning.footprint_courtyard_parts(outer_width, outer_depth, inner_width, inner_depth)` returns a centred rectangular courtyard footprint as two hole-free U-shaped halves. ([#245](https://github.com/idfkit/idfkit/pull/245))
34
+ - `IDFObject.update` sets several fields in one call, given as a mapping, keyword arguments, or both; where both name the same field, the keyword wins, as with `dict.update`. The call is all or nothing: every entry is examined before any is applied, and if one is refused, no field changes. An entry is refused when its name is not a field of the type, when it is `name` (use `IDFDocument.rename`), when it is a legacy flat extensible name such as `vertex_3_x_coordinate`, or when it gives the extensible wrapper key a value single assignment rejects: something other than a list, or a list holding an item that is not a dict or group, or a group with an unknown field. Unknown names are refused whenever the object has a schema field order, including in a document opened with `strict=False`, where single assignment stores them; an object constructed detached without one accepts any name, as single assignment does. Accepted entries are applied by ordinary attribute assignment, so reference and change tracking are those of consecutive assignments. The method returns `None`; the TypeScript `IdfObject.update` returns the object, a difference recorded in the naming register. The generated stubs type `update` per object type, so a type checker reports a misspelled field name or a wrongly typed value given as a keyword; the positional mapping is typed `Mapping[str, Any] | None`, as at run time, so that a mapping built at run time is accepted. ([#235](https://github.com/idfkit/idfkit/issues/235), [#236](https://github.com/idfkit/idfkit/pull/236))
35
+ - `InvalidFieldError` accepts an optional keyword-only `refused`, the entries of a refused `update` group, and then lists every one in its message and in its `refused` attribute, a tuple of `idfkit.exceptions.RefusedEntry`. Without it, every message is unchanged. ([#236](https://github.com/idfkit/idfkit/pull/236))
36
+ - **`GeocodingError` gained `kind` and `status`.** `kind` is `"not-found"`,
37
+ `"unreachable"`, `"http"` or `"invalid-response"`, and `status` holds the HTTP
38
+ code when `kind` is `"http"`, so a caller can tell a place name that matches
39
+ nothing from a service that could not be reached without matching message
40
+ text. `geocode` and `detect_location` set both on every error they raise. An
41
+ HTTP error from Nominatim or ipapi.co, previously reported only as "Failed to
42
+ geocode", now names its status. An ipapi.co payload that reports a rate limit
43
+ is `"http"` with `status` `429`, and a truncated response body is
44
+ `"unreachable"` rather than escaping as `http.client.IncompleteRead`.
45
+ Coordinates that are blank, boolean or not finite are `"invalid-response"`.
46
+ `@idfkit/weather` carries the same fields
47
+ ([idfkit/idfkit-js#88](https://github.com/idfkit/idfkit-js/issues/88), [#249](https://github.com/idfkit/idfkit/pull/249)).
48
+
49
+ ### Changed
50
+
51
+ - **Breaking:** `polygon_difference_2d` raises `ValueError` when the difference has a hole (the subtracted polygon lies strictly inside) or falls into several pieces, and names `polygon_difference_parts_2d` as the replacement. It used to return a single "bridge/slit" ring for these cases, which had the wrong area or coincident vertices. The subtracted polygon no longer has to lie inside the outer one, and either winding is accepted. ([#164](https://github.com/idfkit/idfkit/issues/164), [#244](https://github.com/idfkit/idfkit/pull/244))
52
+ - **Breaking:** A blank fixed field (any field outside a repeating group, in every object type) was stored in the model as `""`, numeric fields included, although `write_idf` and `write_epjson` both omitted it, so re-parsing idfkit's own IDF output did not equal the document it was written from: a `Zone` written with eight blank trailing commas parsed to 12 fields and came back with 4. Such a field is now left out of the model, as ConvertInputFormat and idfkit-js leave it out, and assigning `None` or `""` to a field now clears it, as it already cleared an extensible slot. A blank field now reads as `None`, as a field cut off by an earlier semicolon always has, in attribute, item and positional access and in the eppy-compatible `fieldvalues`; code that compared such a field with `""` should test for `None`. An explicit `0` is still a value, and `write_idf` still writes an interior blank in its position. ([#183](https://github.com/idfkit/idfkit/issues/183), [#242](https://github.com/idfkit/idfkit/pull/242))
53
+ - `idfkit tmy --near ADDRESS` now exits `1` (no matches) instead of `3`
54
+ (network failure) when the address resolves to no place. Exit `3` remains for
55
+ a geocoder that could not be reached, answered with an HTTP error, or returned
56
+ an unreadable body
57
+ ([idfkit/idfkit-js#88](https://github.com/idfkit/idfkit-js/issues/88), [#249](https://github.com/idfkit/idfkit/pull/249)).
58
+
59
+ ### Deprecated
60
+
61
+ - `footprint_courtyard` emits a `DeprecationWarning`. It returns a "slit" ring that repeats a vertex and runs out and back along a zero-width cut to the courtyard, which `create_block` turns into overlapping, opposite-facing exterior walls and a self-touching floor and roof that EnergyPlus rejects (the defect behind [#164](https://github.com/idfkit/idfkit/issues/164)). Its output is unchanged; use `create_courtyard_block` or `footprint_courtyard_parts` instead. ([#245](https://github.com/idfkit/idfkit/pull/245))
62
+
63
+ ### Fixed
64
+
65
+ - `StationIndex.search` missed queries in the form "City, state" and accented queries, and now finds them. Tokens are split on commas and semicolons as well as whitespace, so `"Golden, CO"` no longer keeps the comma in `"golden,"`, and on dots and hyphens as station names are, so `"St. Louis, MO"` and `"Winston-Salem, NC"` find their stations; a token equal to a station's state or country code qualifies the match rather than having to appear in its name, and counts as matched in a partial match, so `"Boulder, CO"` ranks Boulder, Colorado first; a token shorter than three characters must equal a whole word of the name, so `"co"` no longer prefix-matches Mbanza Congo and Cotonou; and query and station names are compared with diacritics folded, so `"Montréal"` finds Montreal and `"Tromsø"` finds Tromso. ([#248](https://github.com/idfkit/idfkit/issues/248), [#250](https://github.com/idfkit/idfkit/pull/250))
66
+ - `create_block`, `create_courtyard_block` and `ZonedBlock.build` with `air_boundary=True` now raise a `ValueError` naming the version requirement on a document older than EnergyPlus 9.2, before adding anything. `Construction:AirBoundary` first appeared in 9.2, and they previously added every zone and surface of the block and then failed with `UnknownObjectTypeError`, leaving the document holding a block with blank constructions on its interior walls. ([#247](https://github.com/idfkit/idfkit/pull/247))
67
+ - A batch job refused before EnergyPlus runs, such as one without `weather` that is not design-day-only, no longer leaves an empty temporary directory behind when it names no `output_dir`. `simulate_batch` and `async_simulate_batch` created one with `tempfile.mkdtemp()` for every such job and never removed it. The failed result's `run_dir` is now `idfkit.simulation.REFUSED_RUN_DIR`, a placeholder path under the null device that cannot be created, and its output accessors such as `errors` return empty results for it without reading the filesystem. ([#228](https://github.com/idfkit/idfkit/issues/228), [#239](https://github.com/idfkit/idfkit/pull/239))
68
+ - `SimulationCache` no longer returns a previous run's results after a file the model reads has changed. The key covered the model, the weather file and the run flags, so editing a `Schedule:File` CSV staged into `output_dir`, or one referenced by absolute path, returned the earlier run silently. The key now also hashes the contents of every file the model names for EnergyPlus to read: `Schedule:File`, `Schedule:File:Shading`, the `Table:Lookup`, `Table:IndependentVariable`, `Table:MultiVariableLookup` and `Table:TwoIndependentVariables` external files, `Construction:WindowDataFile` (including its default `Window5DataFile.dat`), the ASHRAE 205 representation files of `Chiller:Electric:ASHRAE205` and `Coil:DX:ASHRAE205:Performance`, and FMUs imported through `ExternalInterface:FunctionalMockupUnitImport`. The list is derived from the model's own schema, as the string fields marked `retaincase` whose names refer to a file or path, so file fields added by later EnergyPlus releases are covered too. A relative path is resolved as EnergyPlus resolves it, against `output_dir` and then the EnergyPlus executable's directory. Restoring a file's earlier content hits the earlier entry. A model with a `PythonPlugin:Instance`, or an `ExternalInterface` in `PtolemyServer` or `FunctionalMockupUnitExport` mode, can read inputs no key can cover, so `simulate`, `async_simulate` and the batch helpers now neither look it up nor store it, and log a warning. `SimulationCache.compute_key` takes a new `search_dirs` argument for resolving relative paths. A model that names no external file keeps the key it had, so existing cache entries stay valid. ([#241](https://github.com/idfkit/idfkit/pull/241))
69
+ - A simulation run into an `output_dir` that already held a weather file of the same name, left by an earlier run with a different weather file, now replaces that stale copy. EnergyPlus previously ran with the earlier weather file while `SimulationCache.compute_key` hashed the new one, so the cache stored results under the wrong key. A copy with identical content, or a weather file that already lives in `output_dir`, is left untouched. ([#227](https://github.com/idfkit/idfkit/issues/227), [#237](https://github.com/idfkit/idfkit/pull/237))
70
+ - `load_idf()` and `get_idf_version()` raised `VersionNotFoundError` on an IDF whose `Version` object carries more than three components, such as the `Version, 23.1.0.002;` that DesignBuilder writes. Components past the patch are now ignored, so that file resolves to 23.1.0, as it does in idfkit-js. The epJSON parser already read such strings this way. ([#238](https://github.com/idfkit/idfkit/pull/238))
71
+ - `polygon_difference_2d` returns a clean simple polygon. For an edge or corner cut-out the ring used to contain coincident consecutive vertices, a zero-width slit, so `create_block` built degenerate surfaces and EnergyPlus failed. Subtracting a polygon fully inside the outer one returned the wrong area: 4212.5 m² instead of 5300 m² for a 40 × 25 m island in a 105 × 60 m footprint. The difference is now computed from a planar overlay of both outlines with only the standard library. ([#164](https://github.com/idfkit/idfkit/issues/164), [#244](https://github.com/idfkit/idfkit/pull/244))
72
+ - The bundled `developing-with-idfkit` agent reference no longer suggests `footprint_courtyard`, or building each `polygon_difference_parts_2d` piece with its own `create_block` call, which left the walls along the cuts as exterior walls. It now points to `create_courtyard_block`. ([#245](https://github.com/idfkit/idfkit/pull/245))
73
+ - `polygon_difference_parts_2d` no longer leaves sliver pieces when two holes' middles nearly line up. Each hole got its own vertical cut, so courtyards whose middles were 1 cm apart came back as five pieces, three of them 1 cm wide, and `create_courtyard_block` made each one a zone. The largest hole is now cut through its middle and a hole an earlier cut already crosses gets no cut of its own, giving two pieces. ([#245](https://github.com/idfkit/idfkit/pull/245))
74
+ - `split_horizontal_surface` and `link_blocks` no longer leave a wrong-area, slit-shaped exposed roof when an upper block sits strictly inside the lower block's roof. The exposed frame is split into hole-free surfaces, and each extra piece is named `"<roof name> Remainder"` and copies the roof's construction, zone and boundary conditions. ([#164](https://github.com/idfkit/idfkit/issues/164), [#244](https://github.com/idfkit/idfkit/pull/244))
75
+ - `view_model()` and the other 3D viewers in `idfkit.visualization` drew concave surfaces, such as L- and U-shaped floors and roofs, with spurious triangles spanning their notches. Surfaces were fan-triangulated from their first vertex, which is only correct for convex polygons; concave ones are now ear-clipped on their own plane, keeping the surface's winding, and convex ones keep the fan. Collinear and duplicate vertices are skipped, keyhole polygons with doubled bridge vertices are clipped correctly, and a self-intersecting polygon is still drawn instead of raising. ([#240](https://github.com/idfkit/idfkit/pull/240), [#246](https://github.com/idfkit/idfkit/pull/246))
76
+ - `intersect_and_match` and `intersect_match` no longer give a concave surface drawn as a keyhole (a courtyard roof or ceiling with doubled bridge vertices), or one pinched at a vertex, the wrong area when they split it. Its triangulation clipped triangles that lay outside the surface. A 96 m² keyhole ceiling with one 4 m² floor above it was split into fragments totalling 168 m²; they now total 96 m². ([#246](https://github.com/idfkit/idfkit/pull/246))
77
+ - An IDF file whose last object has no closing `;`, such as `Version, 24.1;\nZone, Z1, 0`, no longer loads with that object silently dropped. A strict parse (the default) raises `IDFParseError` with an `Unterminated object near "..." (missing ";")` finding of code `ParseError`, positioned at the line the object starts on and naming its type. With `strict_parsing=False` or `load_idf_with_diagnostics` the rest of the file still loads, and the finding is logged as a warning and returned. Trailing whitespace and comments are not affected. The same change removes exponential backtracking in the object pattern and in the `preserve_formatting=True` syntax-tree pattern, which made an unterminated tail of thirty characters take more than one hundred seconds to read, and `iter_idf_objects` now skips such a tail with a warning instead of scanning it. ([#187](https://github.com/idfkit/idfkit/issues/187), [#243](https://github.com/idfkit/idfkit/pull/243))
78
+
79
+ ## [1.1.1] - 2026-10-19
80
+
81
+ ### Fixed
82
+
83
+ - `import idfkit` raised `ValueError: mutable default <class 'mappingproxy'> for field choice_spellings is not allowed` on Python 3.11. The two spelling tables of `ParsingCache` that #226 added used a `MappingProxyType` as their dataclass default, which Python 3.11 refuses because that type is hashable only from Python 3.12. They now use a `default_factory`. The test matrix did not catch it because every job ran its tests on the runner's system Python 3.12; each job now runs on the version it names. ([#232](https://github.com/idfkit/idfkit/pull/232))
84
+
10
85
  ## [1.1.0] - 2026-10-16
11
86
 
12
87
  This release moves to `conformance-2026.16`. The corpus level changes no case and no check; it removes the five Python entries from the accepted-divergence register that #179, #180, #181 and #182 close. An idfkit release that predates these fixes reports those five cases as unexpected failures at this level.
@@ -902,7 +977,9 @@ Initial public release.
902
977
  - Performance benchmarks comparing idfkit against eppy and opyplus. ([#5](https://github.com/idfkit/idfkit/pull/5))
903
978
  - MkDocs Material documentation site with a full API reference, an eppy migration guide, and a getting-started Jupyter notebook. ([#2](https://github.com/idfkit/idfkit/pull/2))
904
979
 
905
- [Unreleased]: https://github.com/idfkit/idfkit/compare/v1.1.0...HEAD
980
+ [Unreleased]: https://github.com/idfkit/idfkit/compare/v1.2.0...HEAD
981
+ [1.2.0]: https://github.com/idfkit/idfkit/compare/v1.1.1...v1.2.0
982
+ [1.1.1]: https://github.com/idfkit/idfkit/compare/v1.1.0...v1.1.1
906
983
  [1.1.0]: https://github.com/idfkit/idfkit/compare/v1.0.0...v1.1.0
907
984
  [1.0.0]: https://github.com/idfkit/idfkit/compare/v1.0.0-rc.6...v1.0.0
908
985
  [1.0.0-rc.6]: https://github.com/idfkit/idfkit/compare/v1.0.0-rc.5...v1.0.0-rc.6
@@ -89,7 +89,7 @@ check: check-stubs check-conformance-level check-epw-sentinels check-doc-locatio
89
89
  @echo "🚀 Linting code: Running pre-commit"
90
90
  @uv run pre-commit run -a
91
91
  @echo "🚀 Static type checking: Running pyright"
92
- @uv run pyright src/ agent_references/snippets
92
+ @uv run pyright src/ agent_references/snippets tests/type_checks
93
93
  @echo "🚀 Checking for obsolete dependencies: Running deptry"
94
94
  @uv run deptry src
95
95
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: idfkit
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A fast, modern EnergyPlus IDF/epJSON parser with O(1) lookups and reference tracking
5
5
  Project-URL: Homepage, https://py.idfkit.com/
6
6
  Project-URL: Repository, https://github.com/idfkit/idfkit
@@ -57,27 +57,26 @@ Description-Content-Type: text/markdown
57
57
 
58
58
  **A fast, modern EnergyPlus IDF/epJSON toolkit for Python.**
59
59
 
60
- > [!IMPORTANT]
61
- > **The next release renames how you write a model to disk.** `write_idf(doc, path)`
62
- > becomes `save_idf(doc, path)`, and `write_epjson(doc, path)` becomes
63
- > `save_epjson(doc, path)`. `write_*` now only returns a string.
60
+ > [!NOTE]
61
+ > **Upgrading from 0.x.** Since 1.0.0, writing a model to disk uses
62
+ > `save_idf(doc, path)` and `save_epjson(doc, path)`. `write_idf` and
63
+ > `write_epjson` only return a string.
64
64
  >
65
- > Worth knowing before you upgrade, because neither failure names the cause:
65
+ > Code that still calls `write_*` with a path fails without naming the cause:
66
66
  > `write_epjson(doc, path)` raises `TypeError: can't multiply sequence by
67
- > non-int of type 'PosixPath'`, and `write_idf(doc, path)` fails silently,
68
- > writing nothing and leaving an empty file that surfaces later as
67
+ > non-int of type 'PosixPath'`, and `write_idf(doc, path)` writes nothing,
68
+ > leaving an empty file that surfaces later as
69
69
  > `Could not detect EnergyPlus version in file`.
70
70
  >
71
71
  > Search for `write_idf(` and `write_epjson(` called with two positional
72
- > arguments, or run a type checker, which rejects both. Full notes in the
72
+ > arguments, or run a type checker, which rejects both. Full notes are in the
73
73
  > [changelog](CHANGELOG.md#migration).
74
74
 
75
75
  > [!NOTE]
76
- > idfkit is in **beta**. The API may change between minor versions. We're looking
77
- > for early adopters and testers — especially users of eppy who want
78
- > better performance and a modern API. If you try it out, please
79
- > [open an issue](https://github.com/idfkit/idfkit/issues) with feedback,
80
- > bug reports, or feature requests.
76
+ > idfkit follows [semantic versioning](https://semver.org/): breaking API
77
+ > changes occur only in major releases and are recorded in the
78
+ > [changelog](CHANGELOG.md). Feedback, bug reports, and feature requests are
79
+ > welcome as [GitHub issues](https://github.com/idfkit/idfkit/issues).
81
80
 
82
81
  idfkit lets you load, create, query, and modify EnergyPlus models with an
83
82
  intuitive Python API. It is designed as a drop-in replacement for
@@ -7,27 +7,26 @@
7
7
 
8
8
  **A fast, modern EnergyPlus IDF/epJSON toolkit for Python.**
9
9
 
10
- > [!IMPORTANT]
11
- > **The next release renames how you write a model to disk.** `write_idf(doc, path)`
12
- > becomes `save_idf(doc, path)`, and `write_epjson(doc, path)` becomes
13
- > `save_epjson(doc, path)`. `write_*` now only returns a string.
10
+ > [!NOTE]
11
+ > **Upgrading from 0.x.** Since 1.0.0, writing a model to disk uses
12
+ > `save_idf(doc, path)` and `save_epjson(doc, path)`. `write_idf` and
13
+ > `write_epjson` only return a string.
14
14
  >
15
- > Worth knowing before you upgrade, because neither failure names the cause:
15
+ > Code that still calls `write_*` with a path fails without naming the cause:
16
16
  > `write_epjson(doc, path)` raises `TypeError: can't multiply sequence by
17
- > non-int of type 'PosixPath'`, and `write_idf(doc, path)` fails silently,
18
- > writing nothing and leaving an empty file that surfaces later as
17
+ > non-int of type 'PosixPath'`, and `write_idf(doc, path)` writes nothing,
18
+ > leaving an empty file that surfaces later as
19
19
  > `Could not detect EnergyPlus version in file`.
20
20
  >
21
21
  > Search for `write_idf(` and `write_epjson(` called with two positional
22
- > arguments, or run a type checker, which rejects both. Full notes in the
22
+ > arguments, or run a type checker, which rejects both. Full notes are in the
23
23
  > [changelog](CHANGELOG.md#migration).
24
24
 
25
25
  > [!NOTE]
26
- > idfkit is in **beta**. The API may change between minor versions. We're looking
27
- > for early adopters and testers — especially users of eppy who want
28
- > better performance and a modern API. If you try it out, please
29
- > [open an issue](https://github.com/idfkit/idfkit/issues) with feedback,
30
- > bug reports, or feature requests.
26
+ > idfkit follows [semantic versioning](https://semver.org/): breaking API
27
+ > changes occur only in major releases and are recorded in the
28
+ > [changelog](CHANGELOG.md). Feedback, bug reports, and feature requests are
29
+ > welcome as [GitHub issues](https://github.com/idfkit/idfkit/issues).
31
30
 
32
31
  idfkit lets you load, create, query, and modify EnergyPlus models with an
33
32
  intuitive Python API. It is designed as a drop-in replacement for
@@ -63,6 +63,23 @@ for material in doc.materials:
63
63
  # --8<-- [end:lookup]
64
64
 
65
65
 
66
+ # --8<-- [start:accessors]
67
+ # Any type in the schema is reachable as a typed attribute, not only common ones.
68
+ loops = doc.air_loop_hvacs # IDFCollection[AirLoopHVAC]
69
+ coils = doc.coil_cooling_dx_single_speeds # Coil:Cooling:DX:SingleSpeed
70
+ also_loops = doc["AirLoopHVAC"] # equivalent
71
+ shades = doc.shading_building_detaileds # Shading:Building:Detailed
72
+
73
+ # A typo names the closest matches and their object types:
74
+ # >>> doc.shading_bulding
75
+ # AttributeError: 'IDFDocument' object has no attribute 'shading_bulding'.
76
+ # Did you mean:
77
+ # shading_building (Shading:Building:Detailed)
78
+ # shading_buildings (Shading:Building)
79
+ # shading_fins (Shading:Fin)
80
+ # --8<-- [end:accessors]
81
+
82
+
66
83
  # --8<-- [start:modify]
67
84
  zone = doc["Zone"]["Office"]
68
85
  zone.ceiling_height = 3.5 # plain field
@@ -148,10 +148,43 @@ inner = [(1, 1), (4, 1), (4, 4), (1, 4)]
148
148
  polygon_area_2d(a) # 50.0
149
149
  polygon_contains_2d(a, inner) # True (every vertex of inner is inside a)
150
150
  polygon_intersection_2d(a, b) # overlapping rectangle
151
- polygon_difference_2d(a, b) # a minus b
151
+ polygon_difference_2d(a, b) # a minus b: [(0, 0), (5, 0), (5, 5), (0, 5)]
152
152
  # --8<-- [end:polygon-2d]
153
153
 
154
154
 
155
+ # --8<-- [start:polygon-difference-parts]
156
+ from idfkit import footprint_rectangle
157
+ from idfkit.geometry import polygon_difference_parts_2d
158
+ from idfkit.zoning import create_courtyard_block
159
+
160
+ site = footprint_rectangle(105, 60)
161
+ courtyard = footprint_rectangle(40, 25, origin=(35, 20)) # strictly inside: leaves a hole
162
+ office = footprint_rectangle(33.3, 9.0) # corner cut-out
163
+
164
+ # polygon_difference_2d(site, courtyard) raises ValueError: the result has a hole.
165
+ pieces = polygon_difference_parts_2d(site, courtyard, office)
166
+
167
+ # To build them, keep the pieces in one block so the cut walls become interior walls:
168
+ create_courtyard_block(doc, "Wing", site, [courtyard, office], floor_to_floor=3.5)
169
+ # --8<-- [end:polygon-difference-parts]
170
+
171
+
172
+ # --8<-- [start:triangulate-2d]
173
+ from idfkit.geometry import triangulate_2d
174
+
175
+ l_shape = [(10, 5), (5, 5), (5, 10), (0, 10), (0, 0), (10, 0)]
176
+ triangles = triangulate_2d(l_shape) # [(5, 0, 1), (1, 2, 3), (1, 3, 4), (1, 4, 5)]
177
+ total = sum(polygon_area_2d([l_shape[i] for i in tri]) for tri in triangles) # 75.0
178
+ # --8<-- [end:triangulate-2d]
179
+
180
+
181
+ # --8<-- [start:mistake-hole-good]
182
+ from idfkit.zoning import create_courtyard_block
183
+
184
+ create_courtyard_block(doc, "Block", site, [courtyard], floor_to_floor=3.5)
185
+ # --8<-- [end:mistake-hole-good]
186
+
187
+
155
188
  # --8<-- [start:mistake-area-good]
156
189
  calculate_surface_area(surface)
157
190
  # --8<-- [end:mistake-area-good]
@@ -27,16 +27,34 @@ from idfkit import (
27
27
  footprint_rectangle,
28
28
  footprint_l_shape,
29
29
  footprint_u_shape,
30
- footprint_courtyard,
31
30
  )
32
31
 
33
32
  rect = footprint_rectangle(width=50, depth=30)
34
33
  ell = footprint_l_shape(width=40, depth=20, wing_width=20, wing_depth=15)
35
34
  u = footprint_u_shape(width=40, depth=30, courtyard_width=15, courtyard_depth=10)
36
- courtyard = footprint_courtyard(outer_width=40, outer_depth=30, inner_width=15, inner_depth=10)
37
35
  # --8<-- [end:footprints]
38
36
 
39
37
 
38
+ # --8<-- [start:courtyard]
39
+ from idfkit.zoning import create_courtyard_block, footprint_courtyard_parts
40
+
41
+ # One call: the footprint minus the courtyard(s), split into hole-free pieces,
42
+ # one zone per piece per story, cut walls between pieces paired as interior walls.
43
+ create_courtyard_block(
44
+ doc,
45
+ "Office",
46
+ footprint_rectangle(50, 40),
47
+ [footprint_rectangle(20, 10, origin=(15, 15))],
48
+ floor_to_floor=3.5,
49
+ num_stories=3,
50
+ )
51
+ # Zones: "Office Story 1 Part 1", "Office Story 1 Part 2", ...
52
+
53
+ # Just the pieces (two U-shaped halves), e.g. to inspect or zone them yourself
54
+ halves = footprint_courtyard_parts(outer_width=50, outer_depth=40, inner_width=20, inner_depth=10)
55
+ # --8<-- [end:courtyard]
56
+
57
+
40
58
  # --8<-- [start:zoning-schemes]
41
59
  from idfkit import ZoningScheme, ZoneFootprint
42
60
 
@@ -133,6 +151,22 @@ new_a, new_b = split_horizontal_surface(
133
151
  # --8<-- [end:split-surface]
134
152
 
135
153
 
154
+ # --8<-- [start:split-surface-regions]
155
+ from idfkit.geometry_builders import split_horizontal_surface_by_regions
156
+
157
+ region_surfaces, remaining_surfaces = split_horizontal_surface_by_regions(
158
+ doc,
159
+ surface,
160
+ regions=[
161
+ [(10, 5), (15, 5), (15, 15), (10, 15)],
162
+ [(15, 5), (20, 5), (20, 15), (15, 15)],
163
+ ],
164
+ )
165
+ # region_surfaces: one new surface per region, in order
166
+ # remaining_surfaces: the original surface first, then any extra remainder pieces
167
+ # --8<-- [end:split-surface-regions]
168
+
169
+
136
170
  # --8<-- [start:mistake-ccw-good]
137
171
  ccw_rect = footprint_rectangle(50, 30) # CCW by construction
138
172
  # --8<-- [end:mistake-ccw-good]
@@ -145,6 +179,13 @@ for zone in doc["Zone"]:
145
179
  # --8<-- [end:mistake-hvac-good]
146
180
 
147
181
 
182
+ # --8<-- [start:mistake-courtyard-good]
183
+ create_courtyard_block(
184
+ doc, "Office", footprint_rectangle(50, 40), [footprint_rectangle(20, 10, origin=(15, 15))], floor_to_floor=3.5
185
+ )
186
+ # --8<-- [end:mistake-courtyard-good]
187
+
188
+
148
189
  # --8<-- [start:mistake-link-good]
149
190
  link_blocks(doc)
150
191
  # --8<-- [end:mistake-link-good]
@@ -43,6 +43,16 @@ except IDFParseError as e:
43
43
  # --8<-- [end:strict-parsing]
44
44
 
45
45
 
46
+ # --8<-- [start:blank-fields]
47
+ # Zone, Office, 0, 0, 0, 0, , , ; -- type, multiplier and ceiling height left blank
48
+ zone = doc["Zone"]["Office"]
49
+ zone.ceiling_height # None, exactly as if the semicolon had come earlier
50
+ "ceiling_height" in zone.data # False: a blank field is not stored
51
+ if zone.ceiling_height is None:
52
+ zone.ceiling_height = "Autocalculate"
53
+ # --8<-- [end:blank-fields]
54
+
55
+
46
56
  # --8<-- [start:version]
47
57
  from idfkit import get_idf_version
48
58
 
@@ -106,6 +116,12 @@ if report.success and report.migrated_model is not None:
106
116
  # --8<-- [end:mistake-version-good]
107
117
 
108
118
 
119
+ # --8<-- [start:mistake-blank-good]
120
+ if zone.multiplier is None: # blank or never written: both read as None
121
+ ...
122
+ # --8<-- [end:mistake-blank-good]
123
+
124
+
109
125
  # --8<-- [start:mistake-preserve-good]
110
126
  doc = load_idf("building.idf", preserve_formatting=True)
111
127
  save_idf(doc, "out.idf") # byte-identical
@@ -94,7 +94,7 @@ asyncio.run(main())
94
94
 
95
95
 
96
96
  # --8<-- [start:batch]
97
- from idfkit.simulation import simulate_batch, SimulationJob
97
+ from idfkit.simulation import REFUSED_RUN_DIR, simulate_batch, SimulationJob
98
98
 
99
99
  jobs = []
100
100
  for wwr in (0.2, 0.3, 0.4, 0.5):
@@ -106,7 +106,10 @@ for wwr in (0.2, 0.3, 0.4, 0.5):
106
106
 
107
107
  batch = simulate_batch(jobs, max_workers=4)
108
108
  for job, result in zip(jobs, batch):
109
- print(job.label, result.errors.summary())
109
+ if result.run_dir == REFUSED_RUN_DIR:
110
+ print(job.label, "refused:", result.stderr) # EnergyPlus never ran
111
+ else:
112
+ print(job.label, result.errors.summary())
110
113
  # --8<-- [end:batch]
111
114
 
112
115
 
@@ -60,6 +60,23 @@ results = index.nearest(*geocode("350 Fifth Avenue, New York, NY"))
60
60
  # --8<-- [end:search-address]
61
61
 
62
62
 
63
+ # --8<-- [start:geocode-errors]
64
+ from idfkit.weather import GeocodingError, geocode
65
+
66
+
67
+ def locate(query: str) -> tuple[float, float] | str:
68
+ try:
69
+ return geocode(query)
70
+ except GeocodingError as exc:
71
+ if exc.kind == "not-found": # Nominatim answered: no such place
72
+ return f"No place called {query!r} was found."
73
+ # "unreachable", "http" (exc.status holds the code), or "invalid-response"
74
+ return "The place-name service could not be reached; try again shortly."
75
+
76
+
77
+ # --8<-- [end:geocode-errors]
78
+
79
+
63
80
  # --8<-- [start:search-coords]
64
81
  hits = index.nearest(41.978, -87.904, limit=10)
65
82
  # --8<-- [end:search-coords]
@@ -17,6 +17,7 @@ Read `references/<topic>.md` for focused, task-oriented guidance. Each reference
17
17
  | Find or update cross-references between objects | [reference-tracking.md](references/reference-tracking.md) |
18
18
  | Compute surface area, zone volume, azimuth, WWR | [geometry-and-surfaces.md](references/geometry-and-surfaces.md) |
19
19
  | Build a building footprint and zone it | [geometry-builders-and-zoning.md](references/geometry-builders-and-zoning.md) |
20
+ | Build a footprint with a courtyard or hole (no slit polygons) | [geometry-builders-and-zoning.md](references/geometry-builders-and-zoning.md) |
20
21
  | Stand up an HVAC system quickly with `HVACTemplate:*` | [hvac-templates.md](references/hvac-templates.md) |
21
22
  | Hand-author `AirLoopHVAC` / `PlantLoop` / `CondenserLoop` | [hvac-loops.md](references/hvac-loops.md) |
22
23
  | Run EnergyPlus simulations (sync, async, batch) | [simulation-execution.md](references/simulation-execution.md) |
@@ -24,7 +24,7 @@ The `IDFDocument` is the in-memory representation of an EnergyPlus model. Every
24
24
  | `doc.schema` | `EpJSONSchema` for the document's version. |
25
25
  | `doc[obj_type]` | The `IDFCollection` for the type. Type names are case-insensitive, as they are in EnergyPlus, so `doc["zone"]` and `doc["Zone"]` are the same collection. Never raises: an absent or misspelled type gives an empty collection that is detached from the document, so reading a type never inserts it. |
26
26
  | `doc.get_collection(obj_type)` | The same operation, typed for dynamic `str` keys. It delegates to `doc[obj_type]`; there is no behavioural difference. |
27
- | `doc.<attr>` | Python-name accessors for common types: `doc.zones`, `doc.buildings`, `doc.materials`, `doc.constructions`, `doc.hvac_templates`, etc. |
27
+ | `doc.<attr>` | Typed attribute accessor for any schema type: the `snake_case` plural (`doc.air_loop_hvacs` is `IDFCollection[AirLoopHVAC]`). Hand-written shorthands also resolve and keep their meaning (`doc.building_surfaces` → `BuildingSurface:Detailed`, `doc.ideal_loads` → `ZoneHVAC:IdealLoadsAirSystem`). A typo raises `AttributeError` naming the closest matches. |
28
28
  | `doc.add(obj_type, name=None, **fields)` | Create and insert an object. Returns the new `IDFObject`. |
29
29
  | `doc.rename(obj_type, old, new)` | Rename + cascade updates through every reference. |
30
30
  | `doc.removeidfobject(obj)` | Delete an object. |
@@ -52,6 +52,16 @@ Singletons (objects EnergyPlus requires exactly one of, like `Building` or `Simu
52
52
 
53
53
  Collections support `.first()` (when you know there's a singleton), `.values()`, name-keyed `[name]` access, and `in` membership tests.
54
54
 
55
+ Any object type in the schema is also reachable as an attribute: its `snake_case` plural, typed like the subscript form. Use the attribute in application code, and `doc["Type:Name"]` when the type name comes from EnergyPlus documentation, an epJSON file or a variable. A misspelling raises `AttributeError` naming the closest matches:
56
+
57
+ ```python
58
+ --8<-- "agent_references/snippets/document-and-objects.py:accessors"
59
+ ```
60
+
61
+ Names are derived from the schema by rule, so object types added in a new EnergyPlus release are reachable with no change to idfkit. Three types the rule cannot split are named explicitly (`Output:SQLite` is `doc.output_sqlite`), and singular nouns ending in `s` get a real plural (`doc.window_material_gases`). The plural of every type, plus the shorthands, is declared in the type stub, so editors complete it and type checkers see `IDFCollection` of the generated class. The same names appear in `dir(doc)` for IPython and Jupyter.
62
+
63
+ Resolution is strict about separators: `doc.z_o_n_e`, `doc.zone_`, `doc.airloophvacs` and the singular `doc.air_loop_hvac` all raise rather than guess. The raw type name with `_` in place of `:` resolves in any case (`doc.Site_Location`, `doc.ZONE`), as it did before derived names existed, but is untyped. A name owned by a real member wins over any accessor: `doc.version` is the version tuple, and the `Version` objects are `doc.versions`.
64
+
55
65
  ## Modifying objects
56
66
 
57
67
  Field access is via attribute. Setting a value re-validates against the schema and updates the reference graph automatically:
@@ -83,7 +83,7 @@ For higher-level ceiling↔floor helpers, see [geometry-builders-and-zoning.md](
83
83
 
84
84
  ## 2D polygon utilities
85
85
 
86
- For computing footprint overlaps or differences (e.g. courtyard cut-outs):
86
+ For computing footprint overlaps or differences (e.g. corner cut-outs):
87
87
 
88
88
  ```python
89
89
  --8<-- "agent_references/snippets/geometry-and-surfaces.py:polygon-2d"
@@ -91,8 +91,35 @@ For computing footprint overlaps or differences (e.g. courtyard cut-outs):
91
91
 
92
92
  These take 2D `(x, y)` tuples (not `Vector3D`) because the typical use is on building footprints in plan view.
93
93
 
94
+ `polygon_difference_2d` returns one clean, counter-clockwise, simple polygon with no duplicate or coincident vertices, ready for `create_block`. The subtracted polygon may cross the outer boundary, sit flush with a corner, or extend outside it. A single ring cannot describe a result with a hole (the subtracted polygon strictly inside) or a result in several pieces, so `polygon_difference_2d` raises `ValueError` for those. Use `polygon_difference_parts_2d` from `idfkit.geometry` there. It subtracts any number of polygons and returns hole-free pieces, largest first, each one a valid EnergyPlus surface outline or `create_block` footprint. Building each piece with its own `create_block` call leaves the walls along the cuts as exterior walls; `create_courtyard_block` (in `idfkit.zoning`) builds the pieces as zones of one block so those walls are paired interior walls:
95
+
96
+ ```python
97
+ --8<-- "agent_references/snippets/geometry-and-surfaces.py:polygon-difference-parts"
98
+ ```
99
+
100
+ To cut a concave outline into triangles (for a mesh, or for area-weighted sampling), use `triangulate_2d` from `idfkit.geometry`. It returns index triples into the input, in the input's winding, and it handles keyhole outlines whose bridge vertices are doubled:
101
+
102
+ ```python
103
+ --8<-- "agent_references/snippets/geometry-and-surfaces.py:triangulate-2d"
104
+ ```
105
+
94
106
  ## Common mistakes
95
107
 
108
+ !!! failure "stitching a hole into one polygon with a slit"
109
+
110
+ ```python
111
+ # Joining the outer ring and the hole ring with a zero-width "bridge" edge
112
+ # gives coincident vertices, so create_block makes degenerate surfaces and EnergyPlus fatals.
113
+ slit = [*outer, outer[0], hole[0], *reversed(hole), hole[0]]
114
+ create_block(doc, "Block", slit, floor_to_floor=3.5)
115
+ ```
116
+
117
+ !!! success "split it into hole-free pieces in one block"
118
+
119
+ ```python
120
+ --8<-- "agent_references/snippets/geometry-and-surfaces.py:mistake-hole-good"
121
+ ```
122
+
96
123
  !!! failure "manually summing vertex coordinates"
97
124
 
98
125
  ```python
@@ -27,6 +27,16 @@ idfkit provides parametric footprint generators that return `list[tuple[float, f
27
27
 
28
28
  Or pass any CCW `list[tuple[float, float]]` you generate yourself.
29
29
 
30
+ ## Courtyards (footprints with holes)
31
+
32
+ EnergyPlus surfaces cannot have holes, so a footprint with a courtyard has to become several simple pieces. Use `create_courtyard_block` from `idfkit.zoning`: it subtracts the courtyard polygons from the outer footprint with `polygon_difference_parts_2d`, makes one zone per piece per story (`"<name> Part <k>"`, largest first), and builds the pieces as `CUSTOM` zones of one block, so the walls along the cuts are interior `Surface` walls paired between the pieces' zones, not exterior walls. Courtyards may also touch or cross the outer edge (a notch). `footprint_courtyard_parts` returns the two hole-free halves of a centred rectangular courtyard if you want the polygons themselves:
33
+
34
+ ```python
35
+ --8<-- "agent_references/snippets/geometry-builders-and-zoning.py:courtyard"
36
+ ```
37
+
38
+ Each piece is concave, so there is no `zoning=` argument: every piece is one zone per story. For a finer split, compute the pieces yourself and pass them (or your own subdivision of them) as `custom_zones` to `create_block` with `ZoningScheme.CUSTOM`; walls whose edges match in reverse become interior walls.
39
+
30
40
  ## Zoning schemes
31
41
 
32
42
  `ZoningScheme` controls how each story is partitioned:
@@ -41,7 +51,7 @@ Or pass any CCW `list[tuple[float, float]]` you generate yourself.
41
51
 
42
52
  `create_block` also accepts:
43
53
 
44
- - `air_boundary=True` — apply `Construction:AirBoundary` to all inter-zone walls. Use for open-plan spaces that share air but are still thermally distinct.
54
+ - `air_boundary=True` — apply `Construction:AirBoundary` to all inter-zone walls. Use for open-plan spaces that share air but are still thermally distinct. Needs EnergyPlus 9.2+; on an older document it raises `ValueError` before adding anything.
45
55
  - `base_elevation` — ground floor Z (default 0). When > 0, the ground-floor boundary becomes `Outdoors` instead of `Ground`.
46
56
 
47
57
  ## Multi-block buildings (setbacks)
@@ -88,6 +98,12 @@ When a footprint changes and you want to keep one surface but redraw it:
88
98
  --8<-- "agent_references/snippets/geometry-builders-and-zoning.py:split-surface"
89
99
  ```
90
100
 
101
+ The remainder is always EnergyPlus-safe: no holes and no zero-width slits. When the region sits strictly inside the surface, the remainder would be a frame with a hole, so it is cut into hole-free pieces. The original surface keeps the largest piece, and each other piece becomes a new `"<name> Remainder"` surface that copies its construction, zone and boundary conditions. To get those surfaces back, or to split under several adjacent regions at once (as `link_blocks` does for every floor resting on one roof), use `split_horizontal_surface_by_regions`:
102
+
103
+ ```python
104
+ --8<-- "agent_references/snippets/geometry-builders-and-zoning.py:split-surface-regions"
105
+ ```
106
+
91
107
  ## Common mistakes
92
108
 
93
109
  !!! failure "passing a clockwise footprint"
@@ -120,6 +136,21 @@ When a footprint changes and you want to keep one surface but redraw it:
120
136
 
121
137
  See [hvac-templates.md](hvac-templates.md).
122
138
 
139
+ !!! failure "the deprecated `footprint_courtyard` slit polygon"
140
+
141
+ ```python
142
+ fp = footprint_courtyard(50, 40, 20, 10) # DeprecationWarning
143
+ create_block(doc, "Office", fp, floor_to_floor=3.5)
144
+ # The ring repeats a vertex and runs out and back along a zero-width slit:
145
+ # two overlapping exterior walls and a self-touching floor. EnergyPlus fails.
146
+ ```
147
+
148
+ !!! success "`create_courtyard_block`"
149
+
150
+ ```python
151
+ --8<-- "agent_references/snippets/geometry-builders-and-zoning.py:mistake-courtyard-good"
152
+ ```
153
+
123
154
  !!! failure "forgetting `link_blocks` between setbacks"
124
155
 
125
156
  ```python