[refactor] Implementation of Multiblock
This refactor introduces a modern, metadata-driven PVConfiguration3D while preserving external compatibility via a wrapper PV_Configuration_3D. It also renames MultiBlock_PASE to MultiBlockPASE, adds utility functions for robust geometry merging and name matching.
Module — Current State (“as is”)
The module currently creates a single PolyData object derived from the simulation parameters. This PolyData represents the full set of PV modules in the scene. Each PV module is initially built using custom low-level code that defines its vertices and cells manually. Once a single prototype module is defined, it is replicated across the scene using PyVista’s glyph method.
Module — Target Design (“to be”)
The module should instead build a collection of PolyData objects, each representing an individual or logical group of PV elements. These objects will be organized inside a dedicated, customized pyvista.MultiBlock container, designed to preserve structure and enable modular operations.
Each entry in this collection will be described and tracked through a resilient index, ensuring that every element can be reliably identified, queried, and updated.
On top of this foundation, high-level query methods will provide intuitive access to specific parts of the PV installation — for example, selecting all modules belonging to a given central, block, or orientation.
Finally, initial PV modules will be created using native PyVista geometry builders rather than manual vertex/cell definitions, ensuring consistency, readability, and improved maintainability.
Mains new features
Row index = stable object ID (OID)
PVConfiguration3D builds and manages a tabular index (Pandas DataFrame) where each row represents one PV panel (or face). That index is treated as a stable object identifier (OID) and is used to:
- locate geometry in space (get_position_by_oid, get_block_by_oid);
- filter/query subsets and build merged PolyData (polydata_by_property, polydata_by_central, polydata_all_centrals);
- maintain consistency after structure updates (reindex, assert_consistency);
- remove elements deterministically (remove_by_oid).
This OID decouples geometry generation from selection/aggregation, making unit tests and downstream algorithms (e.g. ray casting) more robust and reproducible.
Multiblock = hierarchical scene container
PVConfiguration3D (and the thin wrapper PV_Configuration_3D) produce PyVista MultiBlock objects that mirror the scene hierarchy:
Central -> Block -> (optional) Sub-blocks/Parts -> Panels
Benefits:
fast bulk operations (combine, extract surface, area/height/normal computations);
selective extraction/merge (by central, by block name, or via flags);
compatibility with legacy consumers: PV_Configuration_3D.PV_central_MB still exposes a multiblock for “PV” parts, while PV_central_PD exposes a merged PolyData.
Unit Test suite: tests/PHOTOVOLTAICS/test_configuration_subscenes.py
Purpose
Validate the recent API in PHOTOVOLTAICS/configuration.py for building a scene with three PV plants and exercising sub-scene logic, geometry queries, and ObjectID integrity.
Common setup
-
Build one scene containing three plants with different geometry/pose:
-
Prefixes applied to block names:
PV_A_*,PV_B_*,PV_C_*. -
After each creation: rename blocks and call
reindex()so name/OID indices stay consistent.
-
-
Helper:
_merge_multiblock_to_poly(via PyVista’smerge) used only in the other suite.
Tests
1. Flag filtering + options
-
Test:
test_flag_filtering_basic_and_union_and_options -
API exercised:
get_polydata_by_flag,name_matches_flag -
Checks:
-
Filtering by single flag returns only blocks with that prefix (
PV_A,PV_B,PV_C). -
Multi-flag union (
["PV_A","PV_C"]) excludesPV_B. -
Case/
exactbehavior ofname_matches_flag.
-
2. Lowest corners (single + multiblock + degenerate)
-
Test:
test_lowest_corners_single_and_multiblock_and_degenerate -
API:
get_lowest_corners,get_lowest_corners_multiblock -
Checks:
-
A single
PolyDatareturns exactly two distinct XY corners with identical Zmin. -
For
"PV_B", returns an array of shape(N, 2, 3). -
Degenerate input (
PolyData()empty) raisesValueError.
-
3. Areas / Zmin / Normals consistency
-
Test:
test_areas_zmin_normals_consistency -
API:
get_area_multiblock,get_z_min_multiblock,get_normal_multiblock -
Checks:
-
All areas for
"PV_C"are strictly positive. -
Median Zmin ordering follows configured heights:
PV_A < PV_B < PV_C. -
Normal vectors are finite and non-zero (norm > 0).
-
4. ObjectID lifecycle + consistency
-
Test:
test_objectid_cycle_remove_and_consistency -
API:
df(ObjectID index),get_position_by_oid,get_block_by_oid,remove_by_oid,assert_consistency -
Checks:
-
Pick an existing OID → position and block are retrievable.
-
After
remove_by_oid,assert_consistency()passes; OID disappears fromdfand lookups returnNone. -
Optional second remove fails cleanly (returns
Falseor raises a controlled exception).
-
5. Legacy alias parity (PV_Configuration_3D)
-
Test:
test_alias_pv_configuration_3d_multiblock_subset_equivalence -
API:
PV_Configuration_3D(...).PV_central_MBvsget_polydata_by_flag("PV") -
Checks:
- Same number of blocks and same block names for a single-plant scene.
Test suite: tests/PHOTOVOLTAICS/test_multiblock_geometry_equivalence.py
Purpose
Prove that different querying/merging strategies produce equivalent geometry, independent of block order or construction path. Uses a robust comparator.
Utility
-
tests/utils/poly_compare.py→compare_polydata(a, b, tol, …)
Compares geometry + topology (points/cells), ignoring point/cell order and orientation (configurable), with numeric tolerance. Optional point/cell-data comparison is supported (disabled in these tests).
Common setup
-
Same 3-plant scene with
PV_A,PV_B,PV_Cprefixes. -
Helper
_merge_multiblock_to_poly(mb)merges blocks into a singlePolyDatawithmerge_points=True.
Tests
1. Union via multi-flag vs manual concatenation
-
Test:
test_union_by_list_vs_manual_concat_equivalent_geometry -
Strategy:
-
cfg.get_polydata_by_flag(["PV_A","PV_B","PV_C"])vs -
Concatenate
PV_C,PV_A,PV_Bin a shuffled order.
-
-
Assertion:
compare_polydata(merged1, merged2)isTrue.
2. A ∪ C: direct vs two independent queries
-
Test:
test_ac_union_equivalence_direct_vs_two_queries -
Strategy:
- Direct multi-flag query vs merge of two separate single-flag results.
-
Assertion: Same merged geometry.
3. Flag selection vs OID-driven selection (prefix A)
-
Test:
test_flag_vs_oid_selection_same_geometry_for_A -
Strategy:
-
get_polydata_by_flag("PV_A")vs -
Find all OIDs whose names start with
PV_A_, select byget_position_by_oid, then merge.
-
-
Assertion: Same merged geometry.
4. Whole-scene equivalence across build paths
-
Test:
test_whole_scene_equivalence_various_paths -
Strategy:
-
Multi-flag list
["PV_A","PV_B","PV_C"]vs sequential per-prefix union in a different order. -
Sanity on legacy alias:
PV_Configuration_3D(...).PV_central_MBvsget_polydata_by_flag("PV")for a single plant.
-
-
Assertion: Both comparisons produce equivalent merged geometry.
5. Comparator robustness (tolerance & order)
-
Test:
test_compare_polydata_tolerance_and_order_insensitivity -
Strategy:
- Two squares with permuted point order and reversed face orientation; small random jitter within
tol.
- Two squares with permuted point order and reversed face orientation; small random jitter within
-
Assertion:
compare_polydatareturnsTrue(parameterized ontol).
Test suite: tests/PHOTOVOLTAICS/test_new_vs_legacy_equivalence.py
Purpose
Verify backward compatibility between the refactored wrapper (PVConfiguration3D / PV_Configuration_3D) and the legacy implementation (LEG_PV_Configuration3D) by asserting identical absolute placement (bounds) at plant/block level for identical inputs.
Legacy class renamed and written in the script. A small bug on the initial z position of first panel has been corrected (it introduces an small offset of 5cm above the ground).
Utility
No external comparator required.
Geometry-level comparison is intentionally not used here due to potential triangulation differences between generators; parity is asserted via np.allclose on bounding boxes with numeric tolerance (atol=1e-4, rtol=1e-2).
Common setup
- Identical input parameters and sun vectors for both legacy and new implementations.
- Build both scenes and extract either merged
PolyDataor per-central entries fromMultiBlockas needed. - Bounds are compared in world coordinates.
Tests
1. Single-plant absolute placement
-
Test:
test_single_plant_bounds_equivalence_new_vs_legacy -
Strategy:
- Instantiate
LEG_PV_Configuration3DandPV_Configuration_3Dwith the same parameters; obtainlegacy_polyandnew_polyand comparebounds.
- Instantiate
-
Assertion: Bounds equivalence holds for all tested thicknesses ->
np.allclose(legacy_poly.bounds, new_poly.bounds, atol=1e-4, rtol=0.01)isTrue.
2. Parameter sweep: panel thickness
-
Test:
test_bounds_equivalence_parameterized_on_panel_thickness -
Strategy:
- Repeat the single-plant check across multiple
PanelThicknessvalues to ensure placement invariance under this parameter.
- Repeat the single-plant check across multiple
-
Assertion: Bounds equivalence holds for all tested thicknesses ->
np.allclose(legacy_poly.bounds, new_poly.bounds, atol=1e-4, rtol=0.01)isTrue.
3. Multi-central per-block placement
-
Test:
test_multicentral_bounds_equivalence_new_vs_legacy -
Strategy:
- For a scene with multiple centrals/blocks, iterate over corresponding entries from legacy and new outputs and compare each item’s
bounds.
- For a scene with multiple centrals/blocks, iterate over corresponding entries from legacy and new outputs and compare each item’s
-
Assertion: For every central/block
i,np.allclose(legacy_poly[i].bounds, new_poly[i].bounds, atol=1e-4, rtol=0.01)isTrue.
4. Wrapper surface check (exposed views)
-
Test:
test_wrapper_exposes_expected_views_without_geometry_shift -
Strategy:
- Use the new wrapper’s exposed views (e.g.,
PV_central_PD) and verify that absolute placement remains consistent with the legacy counterpart for equivalent selections.
- Use the new wrapper’s exposed views (e.g.,
-
Assertion: Same bounds at the view level; no shift introduced by the wrapper (visual check).