PHX Exporter & Importer Patterns¶
Guide for understanding existing exporters/importers and adding new ones.
Common Pipeline¶
All exporters share the same first two steps — read an HBJSON file into a Honeybee model, then convert it to a PHX project:
from PHX.from_HBJSON import read_HBJSON_file, create_project
# 1. Read HBJSON -> Honeybee Model
hb_json_dict = read_HBJSON_file.read_hb_json_from_file(source_path)
hb_model = read_HBJSON_file.convert_hbjson_dict_to_hb_model(hb_json_dict)
# Note: convert_hbjson_dict_to_hb_model always normalizes the model to Meters.
# 2. Convert HB Model -> PHX Project
phx_project = create_project.convert_hb_model_to_PhxProject(
hb_model,
_group_components=True, # Group components by assembly type
_merge_faces=False, # True | False | float (custom tolerance)
_merge_spaces_by_erv=False, # Merge spaces served by the same ERV
_merge_exhaust_vent_devices=False,
)
_merge_faces accepts bool | float — when a float is passed, it is used as the merge tolerance instead of _hb_model.tolerance.
Each CLI entry point wires up these parameters differently:
| Entry point | _group_components |
_merge_faces |
_merge_spaces_by_erv |
_merge_exhaust_vent_devices |
|---|---|---|---|---|
hbjson_to_wufi_xml.py |
CLI arg | CLI arg | CLI arg | CLI arg |
hbjson_to_metr_json.py |
CLI arg | CLI arg | CLI arg | CLI arg |
hbjson_to_phpp.py |
True (hardcoded) |
False (default) |
False (default) |
False (default) |
hbjson_to_ppp.py |
False (hardcoded) |
False (hardcoded) |
True (hardcoded) |
False (hardcoded) |
Then each exporter serializes the PhxProject to its target format.
WUFI XML Exporter (to_WUFI_XML/)¶
Pattern: Schema-based recursive conversion to XML DOM
Modules¶
| Module | Role |
|---|---|
xml_schemas.py |
Schema functions — one per PHX class — that return list[xml_writable] describing the XML structure |
xml_writables.py |
Three writable types (XML_Node, XML_List, XML_Object) plus a xml_writable type alias |
xml_converter.py |
Discovers schema functions by PHX class name via getattr on xml_schemas |
xml_builder.py |
Recursively walks the writable tree, builds an XML DOM, returns toprettyxml() |
xml_txt_to_file.py |
Writes UTF-8 XML with an optional timestamped copy |
_bug_fixes.py |
WUFI-Passive workarounds applied before XML generation |
How it works¶
-
Schema functions in
xml_schemas.pyare named with a leading underscore (_PhxProject,_PhxVariant, etc.). Top-level schemas match the PHX class name; helper schemas use WUFI-centric names (e.g.,_Systems,_DistributionDHW,_PH_ClimateLocation). Each returns alist[xml_writable]. -
Writable types (
xml_writables.py):XML_Node— leaf element with a text/numeric value and an optional attributeXML_List— container whosecountattribute is auto-computed fromlen(node_items)(overridable)XML_Object— references a PHX object; optionally accepts_schema_nameto override the default class-name lookup
-
Schema lookup (
xml_converter.py):get_PHX_object_conversion_schema()constructsf"_{_phx_object.__class__.__name__}"by default, or uses an explicit_schema_namefromXML_Object. Looks up viagetattr(xml_schemas, name). RaisesNoXMLSchemaFoundErrorif not found. -
Builder (
xml_builder.py):generate_WUFI_XML_from_object()recursively walks the writable tree using duck-typing (hasattrchecks fornode_object,node_items), builds aminidomXML DOM, and returnsdoc.toprettyxml().def generate_WUFI_XML_from_object( _phx_object: Any, _header: str = "WUFIplusProject", _schema_name: str | None = None, ) -> str: -
File writer (
xml_txt_to_file.py):write_XML_text_file(_file_address, _xml_text, _write_copy=True)writes UTF-8 XML. When_write_copy=True(default), also writes a timestamped copy ({stem}_{M}_{D}_{h}_{m}_{s}{ext}). OnPermissionError(e.g., file locked by WUFI), writes only the timestamped copy. -
Bug fixes (
_bug_fixes.py):split_cooling_into_multiple_systems()works around a WUFI-Passive v3.x limitation where a single cooling device cannot exceed 200 kW. If total heat-pump cooling capacity exceeds 200 kW, it splits across multiple mechanical system collections. This runs in the WUFI XML CLI entry point after the common pipeline and before XML generation.
Usage¶
from PHX.to_WUFI_XML import xml_builder, xml_txt_to_file
xml_txt = xml_builder.generate_WUFI_XML_from_object(phx_project)
xml_txt_to_file.write_XML_text_file(target_path, xml_txt)
METr JSON Exporter (to_METr_JSON/)¶
Pattern: Schema-based class-name dispatch to plain dicts, serialized as JSON
Modules¶
| Module | Role |
|---|---|
metr_schemas.py |
Schema functions — one per PHX class — that accept a PHX object and return a dict |
metr_converter.py |
Discovers schema functions by PHX class name via getattr on metr_schemas |
metr_builder.py |
Walks the PHX object graph using the converter, builds a nested dict |
metr_json_to_file.py |
Writes UTF-8 JSON |
How it works¶
Follows the same class-name dispatch pattern as the WUFI XML exporter, but simpler:
-
Schema functions (
metr_schemas.py) are plain functions named_ClassNamethat accept a PHX object and return adict. No intermediate writable types. -
Schema lookup (
metr_converter.py):get_schema_function()constructsf"_{_phx_object.__class__.__name__}"by default, or uses an explicit_schema_name. RaisesNoMETrSchemaFoundErrorif not found. -
Builder (
metr_builder.py) provides two entry points:generate_metr_json_dict(_phx_object, _schema_name=None) -> dictgenerate_metr_json_text(_phx_object, _schema_name=None) -> str— wraps the above withjson.dumps(indent=2, ensure_ascii=False)
-
File writer (
metr_json_to_file.py):write_metr_json_file(_file_path, _json_text)writes UTF-8 text.
Usage¶
from PHX.to_METr_JSON import metr_builder, metr_json_to_file
metr_dict = metr_builder.generate_metr_json_dict(phx_project)
metr_text = metr_builder.generate_metr_json_text(phx_project)
metr_json_to_file.write_metr_json_file(target_path, metr_text)
PHPP Exporter (PHPP/)¶
Pattern: Sheet-by-sheet Excel writing via xlwings
Modules¶
| Module/Package | Role |
|---|---|
phpp_app.py |
PHPPConnection — main interface to a PHPP workbook; orchestrates all write operations |
sheet_io/ |
24 per-sheet read/write controllers (io_areas.py, io_climate.py, io_windows.py, etc.) plus io_exceptions.py |
phpp_model/ |
Data classes representing PHPP rows; generate XlItem objects for writing |
phpp_localization/ |
PHPP version and language detection; shape-file JSON for cell-address mapping |
How it works¶
-
PHPPConnection(phpp_app.py) wraps anXLConnection(fromPHX/xl/xl_app.py). On init, it auto-detects the PHPP version and language from the Data worksheet, loads the matching shape file, and instantiates all sheet controller objects as attributes. -
Sheet controllers (
sheet_io/) handle per-worksheet read/write. 24 controllers cover: Areas, Climate, Components, Verification, U-Values, Windows, Shading, Ventilation, AddnlVent, DHW+Distribution, Electricity, Variants, Overview, PER, SolarDHW, SolarPV, CoolingDemand, CoolingPeakLoad, CoolingUnits, HeatingDemand, HeatingPeakLoad, IHG-NonRes, Use-NonRes, Elec-NonRes. -
PHPP data models (
phpp_model/) are dataclasses that generateXlItemobjects.XlItem(defined inPHX/xl/xl_data.py) carries a sheet name, cell address, write value, optional SI/IP unit conversion, and optional cell/font color. -
Localization (
phpp_localization/) provides shape-file JSON that maps logical field names to cell addresses for a given PHPP version. Currently ships with English-only shape files for PHPP v9 (9.6A, 9.7IP) and v10 (10.3, 10.4A, 10.4IP, 10.6, 10.6IP). The version detection code recognizes German (DE) and Spanish (ES) worksheet names for navigation, but no DE/ES shape files are provided. -
PHPPConnectionexposes 20write_*methods — 17 functional write operations plus 3 non-residential stubs (write_non_res_utilization_profiles,write_non_res_space_lighting,write_non_res_IHG).
Usage¶
from PHX.xl import xl_app
from PHX.PHPP import phpp_app
xl = xl_app.XLConnection(xl_framework=xw, output=print)
phpp_conn = phpp_app.PHPPConnection(xl)
with phpp_conn.xl.in_silent_mode():
phpp_conn.write_certification_config(phx_project)
phpp_conn.write_climate_data(phx_project)
phpp_conn.write_project_constructions(phx_project)
# ... 14 more write operations
in_silent_mode() is a context manager on XLConnection that disables screen updating, display alerts, and sets calculation to manual on enter; restores all three and triggers recalculation on exit.
PPP Exporter (to_PPP/)¶
Pattern: Section-based text serialization
Modules¶
| Module | Role |
|---|---|
ppp_sections.py |
PppSection and PppFile dataclasses |
ppp_schemas.py |
Schema functions that return list[PppSection] for each domain area |
ppp_builder.py |
Orchestrates section generation with cross-reference maps |
ppp_txt_to_file.py |
Writes UTF-16LE text (no BOM) |
How it works¶
-
Data structures (
ppp_sections.py):PppSection—name: str,rows: int,cols: int,values: list[str]; hasto_lines()methodPppFile—sections: list[PppSection]; hasto_lines()method that injectsEND_MARKERafter designated sections
-
Schema functions (
ppp_schemas.py) returnlist[PppSection]and cover: meta, EBF, surfaces, thermal bridges, windows, shading, ventilation, U-values, user components, and overbuilt sections. The module also defines slot-limit constants (MAX_SURFACES=100,MAX_WINDOWS=152, etc.) and cross-reference map type aliases (AssemblyMap,GlazingMap,FrameMap,SurfaceIndexMap). -
Builder (
ppp_builder.py):build_ppp_file(project: PhxProject) -> PppFilebuilds four cross-reference maps (assembly, glazing, frame, surface-index) mapping identifiers to PPP slot indices, then calls each schema function in sequence to produce thePppFile. -
File writer (
ppp_txt_to_file.py):write_ppp_file(_filepath, _ppp_file)writes UTF-16LE encoded text with no BOM.
Usage¶
from PHX.to_PPP import ppp_builder, ppp_txt_to_file
ppp_file = ppp_builder.build_ppp_file(phx_project)
ppp_txt_to_file.write_ppp_file(target_path, ppp_file)
WUFI XML Importer (from_WUFI_XML/)¶
Pattern: lxml parsing -> Pydantic v2 validation -> PHX builder functions
Modules¶
| Module | Role |
|---|---|
read_WUFI_XML_file.py |
Parses WUFI XML into a nested Python dict of Tag objects via lxml XMLPullParser |
wufi_file_types.py |
Pydantic v2 custom types with built-in SI unit conversion (e.g., Watts, M, DegreeC) |
wufi_file_schema.py |
Pydantic v2 BaseModel classes mirroring the WUFI XML structure |
phx_schemas.py |
Builder functions that convert Pydantic WUFI objects into PHX model objects |
phx_converter.py |
Single-function entry point: convert_WUFI_XML_to_PHX_project() |
How it works¶
-
XML parsing (
read_WUFI_XML_file.py):get_WUFI_XML_file_as_dict()reads the XML file in 1024-byte chunks vialxml.etree.XMLPullParser(recover=True, encoding="utf-8"), then recursively converts the element tree into a nested dict usingxml_to_dict(). Leaf values becomeTag(text, tag, attrib)dataclass instances. List-like nodes (detected by acountXML attribute or specific tag names) become Python lists. -
Unit types (
wufi_file_types.py): Custom types (subclassingfloatorint) that implement__get_pydantic_core_schema__for Pydantic v2. Two base classes:BaseConverter— for values with aunitattribute; converts to SI viaph_units.convert()BaseCaster— for values needing type-cast only; handlesNone/"NONE"strings
Concrete types cover power (
Watts,KiloWatt), energy (kWh,kWh_per_M2), length (M,MM), temperature (DegreeC,DegreeDeltaK), airflow (M3_per_Hour,ACH), and many more. -
Pydantic schema (
wufi_file_schema.py):WufiBaseModelapplies a@model_validator(mode="before")that callsunpack_xml_tag()on every field — convertingTagobjects into either bare strings or{"value": ..., "unit_type": ...}dicts that the unit types understand. The root model isWUFIplusProject. Key sub-models includeWufiVariant,WufiBuilding,WufiZone,WufiComponent,WufiAssembly,WufiWindowType,WufiSystem,WufiDevice,WufiFoundationInterface, and many more. -
PHX builders (
phx_schemas.py): Functions named_PhxClassName(or_WufiClassNamefor WUFI-specific types) that consume Pydantic objects and produce PHX model objects. A central dispatcheras_phx_obj(_model, _schema_name, **kwargs)looks up builders viagetattron the module. Type libraries (windows, assemblies, shades, schedules) are built first, then each variant's building, certification, and HVAC systems. -
Entry point (
phx_converter.py):convert_WUFI_XML_to_PHX_project(_wufi_xml_project: WUFIplusProject) -> PhxProject— a thin wrapper that calls_PhxProject()fromphx_schemas.
Full pipeline¶
WUFI XML file (UTF-8)
| get_WUFI_XML_file_as_dict()
v
dict[str, Tag | list | dict]
| WUFIplusProject.model_validate(data)
| unpack_xml_tag() -> unit type validation -> SI values
v
WUFIplusProject (Pydantic, fully typed, all SI)
| convert_WUFI_XML_to_PHX_project()
| _PhxProject() -> type libraries first, then variants
v
PhxProject (in-memory PHX model)
Adding a New Exporter¶
To add a new export target (e.g., to_NewFormat/):
- Create a new package under
PHX/following the naming conventionto_<FORMAT>/ - Choose a schema pattern:
- For tree-structured outputs (XML, JSON): class-name dispatch with schema functions (see
to_WUFI_XML/orto_METr_JSON/) - For flat/tabular outputs: section-based builders (see
to_PPP/)
- For tree-structured outputs (XML, JSON): class-name dispatch with schema functions (see
- Implement the layers:
- Schema module — functions that describe how each PHX class maps to the target format
- Converter module —
getattr-based schema lookup by PHX class name - Builder module — entry point that walks the
PhxProjectobject graph - File writer — handles target encoding and file I/O
- Create a CLI entry point (e.g.,
hbjson_to_newformat.py) that wires up the common pipeline parameters - Add tests in
tests/test_to_<format>/with reference output files