Skip to content

[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’s merge) 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"]) excludes PV_B.

    • Case/exact behavior of name_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 PolyData returns exactly two distinct XY corners with identical Zmin.

    • For "PV_B", returns an array of shape (N, 2, 3).

    • Degenerate input (PolyData() empty) raises ValueError.

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 from df and lookups return None.

    • Optional second remove fails cleanly (returns False or 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_MB vs get_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.pycompare_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_C prefixes.

  • Helper _merge_multiblock_to_poly(mb) merges blocks into a single PolyData with merge_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_B in a shuffled order.

  • Assertion: compare_polydata(merged1, merged2) is True.

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 by get_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_MB vs get_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.
  • Assertion: compare_polydata returns True (parameterized on tol).

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 PolyData or per-central entries from MultiBlock as 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_Configuration3D and PV_Configuration_3D with the same parameters; obtain legacy_poly and new_poly and compare bounds.
  • Assertion: Bounds equivalence holds for all tested thicknesses -> np.allclose(legacy_poly.bounds, new_poly.bounds, atol=1e-4, rtol=0.01) is True.

2. Parameter sweep: panel thickness

  • Test: test_bounds_equivalence_parameterized_on_panel_thickness

  • Strategy:

    • Repeat the single-plant check across multiple PanelThickness values to ensure placement invariance under this parameter.
  • Assertion: Bounds equivalence holds for all tested thicknesses -> np.allclose(legacy_poly.bounds, new_poly.bounds, atol=1e-4, rtol=0.01) is True.

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.
  • Assertion: For every central/block i,np.allclose(legacy_poly[i].bounds, new_poly[i].bounds, atol=1e-4, rtol=0.01) is True.

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.
  • Assertion: Same bounds at the view level; no shift introduced by the wrapper (visual check).

Edited by Nicolas