diff --git a/docs/changelog.md b/docs/changelog.md index 1a7c1fb..f83b16c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,7 @@ - importing gds function and example [PR#102](https://github.com/simbilod/meshwell/pull/102) - refactor meshing into more manageable chunks, fixing [issue#87](https://github.com/simbilod/meshwell/issues/87) [PR#105](https://github.com/simbilod/meshwell/pull/105) [PR#106](https://github.com/simbilod/meshwell/pull/106) +- refactor meshing into CAD + meshing to checkpoint [PR#107](https://github.com/simbilod/meshwell/pull/107) ## 1.3.8 diff --git a/meshwell/gmsh_entity.py b/meshwell/gmsh_entity.py index 6e84671..b20fe2e 100644 --- a/meshwell/gmsh_entity.py +++ b/meshwell/gmsh_entity.py @@ -32,7 +32,7 @@ class GMSH_entity(BaseModel): @validator("physical_name", pre=True, always=True) def _set_physical_name(cls, physical_name: Optional[str | tuple[str]]): if isinstance(physical_name, str): - return [physical_name] + return (physical_name,) else: return physical_name diff --git a/meshwell/labeledentity.py b/meshwell/labeledentity.py index 1fb1ec3..7589c9e 100644 --- a/meshwell/labeledentity.py +++ b/meshwell/labeledentity.py @@ -20,6 +20,25 @@ class LabeledEntities(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) + def to_dict(self) -> dict: + """Convert entity to dictionary representation. + + Returns: + Dictionary containing serializable entity data + """ + return { + "index": self.index, + "dimtags": self.dimtags, + "physical_name": self.physical_name, + "resolutions": [r.model_dump() for r in self.resolutions] + if self.resolutions + else None, + "keep": self.keep, + "boundaries": self.boundaries, + "interfaces": self.interfaces, + "mesh_edge_name_interfaces": self.mesh_edge_name_interfaces, + } + def _fuse_self(self, dimtags: List[Union[int, str]]) -> List[Union[int, str]]: if len(dimtags) == 0: return [] diff --git a/meshwell/model.py b/meshwell/model.py index 39f8657..948af68 100644 --- a/meshwell/model.py +++ b/meshwell/model.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from os import cpu_count from typing import Dict, Optional, List, Tuple import numpy as np @@ -26,26 +27,21 @@ class Model: def __init__( self, + filename: str = "temp", point_tolerance: float = 1e-3, n_threads: int = cpu_count(), ): - # Initialize model - if gmsh.is_initialized(): - gmsh.clear() - gmsh.initialize() - - # Set paralllel processing - gmsh.option.setNumber("General.NumThreads", n_threads) - gmsh.option.setNumber("Mesh.MaxNumThreads1D", n_threads) - gmsh.option.setNumber("Mesh.MaxNumThreads2D", n_threads) - gmsh.option.setNumber("Mesh.MaxNumThreads3D", n_threads) - gmsh.option.setNumber("Geometry.OCCParallel", 1) + """TODO: rethink how we deal with GMSH Model here.""" + + # Model naming + self.model = gmsh.model + self.filename = filename + self.n_threads = n_threads # Point snapping self.point_tolerance = point_tolerance # CAD engine - self.model = gmsh.model self.occ = self.model.occ # Track some gmsh entities for bottom-up volume definition @@ -146,82 +142,258 @@ def sync_model(self): self.points = new_points self.segments = new_segments + def _initialize_model(self): + """Ensure GMSH is initialized before operations.""" + if gmsh.is_initialized(): + gmsh.finalize() + gmsh.initialize() + else: + gmsh.initialize() + + # Clear model and points + gmsh.clear() + self.points = {} + self.segments = {} + + self.model.add(self.filename) + self.model.setFileName(self.filename) + gmsh.option.setNumber("General.NumThreads", self.n_threads) + gmsh.option.setNumber("Mesh.MaxNumThreads1D", self.n_threads) + gmsh.option.setNumber("Mesh.MaxNumThreads2D", self.n_threads) + gmsh.option.setNumber("Mesh.MaxNumThreads3D", self.n_threads) + gmsh.option.setNumber("Geometry.OCCParallel", 1) + + def cad( + self, + entities_list: List, + filename: Optional[str | Path] = None, + fuse_entities_by_name: bool = False, + addition_delimiter: str = "+", + addition_intersection_physicals: bool = True, + addition_addition_physicals: bool = True, + addition_structural_physicals: bool = True, + interface_delimiter: str = "___", + boundary_delimiter: str = "None", + progress_bars: bool = False, + ) -> Tuple[List, int]: + """Process CAD operations and save to .xao format. + + Args: + entities_list: List of entities to process + filename: Optional output file path (overrides self.filename) + fuse_entities_by_name: Whether to fuse entities with same name + addition_delimiter: Delimiter for additive entities + addition_*_physicals: Flags for handling additive entity names + interface_delimiter: Delimiter for interface names + boundary_delimiter: Delimiter for boundary names + progress_bars: Show progress bars during processing + + Returns: + Tuple of (processed entity list, maximum dimension) + """ + self._initialize_model() + + # Process entities and get max dimension + final_entity_list, max_dim = self._process_entities( + entities_list=entities_list, + progress_bars=progress_bars, + fuse_entities_by_name=fuse_entities_by_name, + addition_delimiter=addition_delimiter, + addition_intersection_physicals=addition_intersection_physicals, + addition_addition_physicals=addition_addition_physicals, + addition_structural_physicals=addition_structural_physicals, + ) + + # Tag entities and boundaries + self._tag_mesh_components( + final_entity_list=final_entity_list, + max_dim=max_dim, + interface_delimiter=interface_delimiter, + boundary_delimiter=boundary_delimiter, + ) + + # Use provided filename or default to self.filename + output_file = Path(filename if filename is not None else self.filename) + + # Save CAD to .xao format + gmsh.write(str(output_file.with_suffix(".xao"))) + + # Save entity list to JSON to preserve dimtag <--> physical name mapping + entities_json = str(output_file.with_suffix(".json")) + with open(entities_json, "w") as f: + json.dump([e.to_dict() for e in final_entity_list], f) + + return final_entity_list, max_dim + + def _load_cad_model( + self, + entities_list: List, + filename: Optional[str | Path] = None, + ) -> Tuple[List, int]: + """Load CAD model from .xao file and validate entities. + + Args: + entities_list: Optional list of entities for refinement + filename: Optional file path (overrides self.filename) + + Returns: + Tuple of (modified entity list, maximum dimension) + """ + self._initialize_model() + + # Check for additive entities + if entities_list: + additive_entities = [e for e in entities_list if e.additive is True] + if additive_entities: + raise ValueError( + "Meshing from a loaded CAD file currently does not support additive entities. " + f"Found {len(additive_entities)} additive entities: " + f"{[e.physical_name for e in additive_entities]}" + ) + + # Load and validate entities + modified_entities = entities_list + + # Use provided filename or default to self.filename + input_file = Path(filename if filename is not None else self.filename) + + # Load saved entities from JSON + json_file = str(input_file.with_suffix(".json")) + with open(json_file) as f: + loaded_entities = json.load(f) + + # Get mappings for validation + input_mapping = self._calculate_input_entity_indices(entities_list) + loaded_mapping = self._get_labeled_entity_indices(loaded_entities) + + if set(input_mapping.keys()) != set(loaded_mapping.keys()): + raise RuntimeError( + "entities_list not compatible with provided cad_xao_file!" + ) + + # Create modified entities list combining CAD geometry with input parameters + modified_entities = [ + LabeledEntities( + index=loaded_mapping[key]["index"], # from CAD + dimtags=loaded_mapping[key]["dimtags"], # from CAD + physical_name=loaded_mapping[key]["physical_name"], # from either + keep=input_mapping[key].mesh_bool, # from input + model=self.model, + resolutions=input_mapping[key].resolutions, # from input + boundaries=loaded_mapping[key]["boundaries"], # from CAD + interfaces=loaded_mapping[key]["interfaces"], # from CAD + mesh_edge_name_interfaces=loaded_mapping[key][ + "mesh_edge_name_interfaces" + ], # from CAD + ) + for key in input_mapping.keys() + ] + + # Load model and calculate dimension + gmsh.merge(str(input_file.with_suffix(".xao"))) + max_dim = 0 + for dim in range(4): + if self.model.getEntities(dim): + max_dim = dim + + return modified_entities, max_dim + def mesh( self, entities_list: List, + filename: Optional[str | Path] = None, + from_cad: bool = False, + cad_filename: Optional[str | Path] = None, background_remeshing_file: Optional[Path] = None, default_characteristic_length: float = 0.5, global_scaling: float = 1.0, global_2D_algorithm: int = 6, global_3D_algorithm: int = 1, - filename: Optional[str | Path] = None, verbosity: Optional[int] = 0, - progress_bars: bool = False, - interface_delimiter: str = "___", + periodic_entities: Optional[List[Tuple[str, str]]] = None, + optimization_flags: Optional[tuple[tuple[str, int]]] = None, + finalize: bool = True, boundary_delimiter: str = "None", + # CAD arguments (not usef if cad_xao_file provided) + fuse_entities_by_name: bool = False, addition_delimiter: str = "+", addition_intersection_physicals: bool = True, addition_addition_physicals: bool = True, addition_structural_physicals: bool = True, - gmsh_version: Optional[float] = None, - finalize: bool = True, - periodic_entities: List[Tuple[str, str]] = None, - fuse_entities_by_name: bool = False, - optimization_flags: tuple[tuple[str, int]] | None = None, - ) -> meshio.Mesh: - """Creates a GMSH mesh with proper physical tagging.""" - # Initialize mesh settings - self._initialize_mesh_settings( - verbosity, - default_characteristic_length, - global_2D_algorithm, - global_3D_algorithm, - gmsh_version, - ) + interface_delimiter: str = "___", + progress_bars: bool = False, + ) -> Optional[meshio.Mesh]: + """Generate mesh from .xao file. - # Process background mesh if provided - if background_remeshing_file: - self._handle_background_mesh(background_remeshing_file) + Args: + entities_list: Optional list of entities for refinement + filename: Optional output file path (overrides self.filename) + from_cad: Whether to load from existing CAD file + background_remeshing_file: Optional background mesh file + default_characteristic_length: Default mesh size + global_scaling: Global mesh scaling factor + global_2D/3D_algorithm: Meshing algorithm selection + verbosity: GMSH verbosity level + periodic_entities: List of periodic entity pairs + optimization_flags: Mesh optimization parameters + finalize: Whether to finalize GMSH + boundary_delimiter: Delimiter for boundary names - # Process entities and get max dimension - final_entity_list, max_dim = self._process_entities( - entities_list, - progress_bars, - fuse_entities_by_name, - addition_delimiter, - addition_intersection_physicals, - addition_addition_physicals, - addition_structural_physicals, - ) + Returns: + meshio.Mesh object if successful + """ + self._initialize_model() - # Tag entities and boundaries - self._tag_mesh_components( - final_entity_list, - max_dim, - interface_delimiter, - boundary_delimiter, + # Use provided filename or default to self.filename + output_file = Path(filename if filename is not None else self.filename) + + # Load CAD model or evaluate it + if from_cad: + entities_list, max_dim = self._load_cad_model(entities_list, cad_filename) + + else: + entities_list, max_dim = self.cad( + entities_list=entities_list, + filename=output_file, + fuse_entities_by_name=fuse_entities_by_name, + addition_delimiter=addition_delimiter, + addition_intersection_physicals=addition_intersection_physicals, + addition_addition_physicals=addition_addition_physicals, + addition_structural_physicals=addition_structural_physicals, + interface_delimiter=interface_delimiter, + boundary_delimiter=boundary_delimiter, + progress_bars=progress_bars, + ) + + # Initialize mesh settings + self._initialize_mesh_settings( + verbosity=verbosity, + default_characteristic_length=default_characteristic_length, + global_2D_algorithm=global_2D_algorithm, + global_3D_algorithm=global_3D_algorithm, + gmsh_version=None, ) # Handle periodic boundaries if specified - if periodic_entities: - self._apply_periodic_boundaries(final_entity_list, periodic_entities) + if periodic_entities and entities_list: + self._apply_periodic_boundaries(entities_list, periodic_entities) # Apply mesh refinement self._apply_mesh_refinement( - background_remeshing_file, - final_entity_list, - boundary_delimiter, + background_remeshing_file=background_remeshing_file, + final_entity_list=entities_list, + boundary_delimiter=boundary_delimiter, ) # Generate and return mesh return self._generate_final_mesh( - filename, - max_dim, - global_3D_algorithm, - global_scaling, - verbosity, - optimization_flags, - finalize, + filename=output_file, + max_dim=max_dim, + global_3D_algorithm=global_3D_algorithm, + global_scaling=global_scaling, + verbosity=verbosity, + optimization_flags=optimization_flags, + finalize=finalize, ) def _initialize_mesh_settings( @@ -287,6 +459,43 @@ def _process_entities( return structural_entity_list, max_dim + def _calculate_input_entity_indices( + self, entity_list: List + ) -> set[tuple[int, str | tuple[str, ...]]]: + """Get set of unique (index, physical_name) pairs from list of entities. + + Args: + entity_list: List of entity objects to process + + Returns: + Set of tuples containing unique (index, physical_name) pairs + """ + # Filter and order entities + structural_entities = [e for e in entity_list if not e.additive] + structural_entities = order_entities(structural_entities) + + # Create set of unique (index, physical_name) pairs + return { + (index, entity.physical_name): entity + for index, entity in enumerate(structural_entities) + } + + def _get_labeled_entity_indices( + self, labeled_entities: List[LabeledEntities] + ) -> set[tuple[int, str | tuple[str, ...]]]: + """Get set of unique (index, physical_name) pairs from list of LabeledEntities. + + Args: + entities: List of LabeledEntities objects + + Returns: + Set of tuples containing unique (index, physical_name) pairs + """ + return { + (entity["index"], tuple(entity["physical_name"])): entity + for entity in labeled_entities + } + def _process_structural_entities( self, structural_entities: List, progress_bars: bool ) -> Tuple[List, int]: diff --git a/meshwell/polysurface.py b/meshwell/polysurface.py index cc4a97e..181a69f 100644 --- a/meshwell/polysurface.py +++ b/meshwell/polysurface.py @@ -58,7 +58,7 @@ def __init__( self.mesh_order = mesh_order if isinstance(physical_name, str): - self.physical_name = [physical_name] + self.physical_name = (physical_name,) else: self.physical_name = physical_name self.mesh_bool = mesh_bool diff --git a/meshwell/prism.py b/meshwell/prism.py index c6d3f42..54af08e 100644 --- a/meshwell/prism.py +++ b/meshwell/prism.py @@ -84,7 +84,7 @@ def __init__( # Format physical name if isinstance(physical_name, str): - self.physical_name = [physical_name] + self.physical_name = (physical_name,) else: self.physical_name = physical_name self.mesh_bool = mesh_bool diff --git a/meshwell/utils.py b/meshwell/utils.py index b3b7d4a..e9e2bd4 100644 --- a/meshwell/utils.py +++ b/meshwell/utils.py @@ -3,9 +3,12 @@ from meshwell.config import PATH -def compare_meshes(meshfile: Path): +def compare_meshes(meshfile: Path, other_meshfile: Path | None = None): meshfile2 = meshfile - meshfile1 = PATH.references / (str(meshfile.with_suffix("")) + ".reference.msh") + if other_meshfile is None: + meshfile1 = PATH.references / (str(meshfile.with_suffix("")) + ".reference.msh") + else: + meshfile1 = other_meshfile with open(str(meshfile1)) as f: expected_lines = f.readlines() @@ -14,3 +17,32 @@ def compare_meshes(meshfile: Path): diff = list(unified_diff(expected_lines, actual_lines)) assert diff == [], f"Mesh {str(meshfile)} differs from its reference!" + + +def compare_mesh_headers(meshfile: Path, other_meshfile: Path | None = None): + meshfile2 = meshfile + if other_meshfile is None: + meshfile1 = PATH.references / (str(meshfile.with_suffix("")) + ".reference.msh") + else: + meshfile1 = other_meshfile + + with open(str(meshfile1)) as f: + expected_lines = [] + for line in f: + expected_lines.append(line) + if line.startswith("$Entities"): + # Get one more line after $Entities + expected_lines.append(next(f)) + break + + with open(str(meshfile2)) as f: + actual_lines = [] + for line in f: + actual_lines.append(line) + if line.startswith("$Entities"): + # Get one more line after $Entities + actual_lines.append(next(f)) + break + + diff = list(unified_diff(expected_lines, actual_lines)) + assert diff == [], f"Mesh headers in {str(meshfile)} differ from reference!" diff --git a/tests/generate_references.sh b/tests/generate_references.sh index f6fa81f..86dd85b 100644 --- a/tests/generate_references.sh +++ b/tests/generate_references.sh @@ -6,7 +6,7 @@ source .venv/bin/activate # Install reference version of the package in editable mode sudo apt-get install libglu1-mesa -uv pip install git+https://github.com/simbilod/meshwell.git@ee10c5ab5da9c1311f5925907e08e494157c702d[dev] +uv pip install git+https://github.com/simbilod/meshwell.git@af140b69f2d563beed2c5389d27724d77a3429bb[dev] # Execute all Python files in the current directory python generate_references.py --references-path ./references/ diff --git a/tests/test_cad_load.py b/tests/test_cad_load.py new file mode 100644 index 0000000..68f101b --- /dev/null +++ b/tests/test_cad_load.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import pytest +import shapely +from pathlib import Path +from meshwell.model import Model +from meshwell.prism import Prism +from meshwell.gmsh_entity import GMSH_entity +from meshwell.resolution import ConstantInField + +from meshwell.utils import compare_mesh_headers + + +def test_load_cad_model(): + # Create initial model with some geometry + initial_model = Model(n_threads=1) + + # Create a prism from polygons + polygon1 = shapely.Polygon( + [[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]], + holes=([[0.5, 0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5], [0.5, 0.5]],), + ) + polygon2 = shapely.Polygon([[-1, -1], [-2, -1], [-2, -2], [-1, -2], [-1, -1]]) + polygon = shapely.MultiPolygon([polygon1, polygon2]) + + buffers = {0.0: 0.0, 1.0: -0.1} + + prism = Prism( + polygons=polygon, + buffers=buffers, + model=initial_model, + physical_name="prism_entity", + mesh_order=1, + resolutions=[ + ConstantInField(resolution=0.5, apply_to="volumes"), + ], + ) + + # Create a sphere + sphere = GMSH_entity( + gmsh_function=initial_model.occ.addSphere, + gmsh_function_kwargs={"xc": 0, "yc": 0, "zc": 0, "radius": 1}, + dimension=3, + model=initial_model, + physical_name="sphere_entity", + mesh_order=2, + resolutions=[ + ConstantInField(resolution=0.3, apply_to="volumes"), + ], + ) + + entities_list = [prism, sphere] + + # Mesh directly for baseline + initial_model.mesh(entities_list=entities_list, filename="test.msh") + + # Generate initial CAD and save to file + initial_model.cad(entities_list=entities_list, filename="test.xao") + + # Create new model to test loading + load_identical_model = Model("test_load_model_identical", n_threads=1) + + load_identical_model.mesh( + entities_list=entities_list, + cad_filename="test.xao", + from_cad=True, + filename="test_identical.msh", + ) + + compare_mesh_headers( + meshfile=Path("test.msh"), other_meshfile=Path("test_identical.msh") + ) + + # Create new model to test loading w/ modified entities + new_model = Model("test_load_model_modified", n_threads=1) + + # Create new entities with different mesh parameters + new_prism = Prism( + polygons=polygon, + buffers=buffers, + model=new_model, + physical_name="prism_entity", + mesh_order=1, + resolutions=[ + ConstantInField(resolution=0.2, apply_to="volumes"), # Different resolution + ], + ) + + new_sphere = GMSH_entity( + gmsh_function=new_model.occ.addSphere, + gmsh_function_kwargs={"xc": 0, "yc": 0, "zc": 0, "radius": 1}, + dimension=3, + model=new_model, + physical_name="sphere_entity", + mesh_order=2, + resolutions=[ + ConstantInField( + resolution=0.15, apply_to="volumes" + ), # Different resolution + ], + ) + + new_entities_list = [new_prism, new_sphere] + + # Create new model to test loading + load_modified_model = Model("test_load_model_modified", n_threads=1) + + load_modified_model.mesh( + entities_list=new_entities_list, + cad_filename="test.xao", + from_cad=True, + filename="test_modified.msh", + ) + + compare_mesh_headers( + meshfile=Path("test.msh"), other_meshfile=Path("test_modified.msh") + ) + + # Test loading CAD model + modified_entities, max_dim = new_model._load_cad_model( + filename="test.xao", entities_list=new_entities_list + ) + + # Verify results + assert len(modified_entities) == 2 + assert max_dim == 3 + + # Check entities maintained correct properties + for entity in modified_entities: + if entity.physical_name == "prism_entity": + assert entity.resolutions[0].resolution == 0.2 # New resolution + assert entity.mesh_order == 1 + elif entity.physical_name == "sphere_entity": + assert entity.resolutions[0].resolution == 0.15 # New resolution + assert entity.mesh_order == 2 + + +def test_load_cad_model_with_additive_entities(tmp_path): + """Test that loading fails when additive entities are present""" + model = Model(n_threads=1) + + # Create base geometry + sphere = GMSH_entity( + gmsh_function=model.occ.addSphere, + gmsh_function_kwargs={"xc": 0, "yc": 0, "zc": 0, "radius": 1}, + dimension=3, + model=model, + physical_name="base_sphere", + mesh_order=1, + ) + + # Create additive geometry + additive_sphere = GMSH_entity( + gmsh_function=model.occ.addSphere, + gmsh_function_kwargs={"xc": 0.5, "yc": 0, "zc": 0, "radius": 0.5}, + dimension=3, + model=model, + physical_name="additive_sphere", + mesh_order=1, + additive=True, # Make this an additive entity + ) + + xao_file = tmp_path / "test_additive.xao" + entities_list = [sphere, additive_sphere] + + # Should raise ValueError due to additive entities + with pytest.raises( + ValueError, + match="Meshing from a loaded CAD file currently does not support additive entities", + ): + model._load_cad_model(filename=xao_file, entities_list=entities_list) + + +if __name__ == "__main__": + test_load_cad_model() diff --git a/tests/test_mesh_order.py b/tests/test_mesh_order.py index 8000dc2..beffbb1 100644 --- a/tests/test_mesh_order.py +++ b/tests/test_mesh_order.py @@ -23,19 +23,19 @@ def test_mesh_order(): ), ] - assert entities[0].physical_name == ["meshdefault1"] - assert entities[1].physical_name == ["mesh10"] - assert entities[2].physical_name == ["mesh2"] - assert entities[3].physical_name == ["meshdefault2"] - assert entities[4].physical_name == ["mesh3p5"] + assert entities[0].physical_name == ("meshdefault1",) + assert entities[1].physical_name == ("mesh10",) + assert entities[2].physical_name == ("mesh2",) + assert entities[3].physical_name == ("meshdefault2",) + assert entities[4].physical_name == ("mesh3p5",) ordered_entities = order_entities(entities) - assert ordered_entities[0].physical_name == ["mesh2"] - assert ordered_entities[1].physical_name == ["mesh3p5"] - assert ordered_entities[2].physical_name == ["mesh10"] - assert ordered_entities[3].physical_name == ["meshdefault1"] - assert ordered_entities[4].physical_name == ["meshdefault2"] + assert ordered_entities[0].physical_name == ("mesh2",) + assert ordered_entities[1].physical_name == ("mesh3p5",) + assert ordered_entities[2].physical_name == ("mesh10",) + assert ordered_entities[3].physical_name == ("meshdefault1",) + assert ordered_entities[4].physical_name == ("meshdefault2",) assert ordered_entities[3].mesh_order == 11 assert ordered_entities[4].mesh_order == 12 diff --git a/tests/test_prism.py b/tests/test_prism.py index f8209fe..1193088 100644 --- a/tests/test_prism.py +++ b/tests/test_prism.py @@ -18,6 +18,7 @@ def test_prism(): buffers = {0.0: 0.0, 0.3: 0.1, 1.0: -0.2} model = Model(n_threads=1) + model._initialize_model() prism_obj = Prism(polygons=polygon, buffers=buffers, model=model) prism_obj.instanciate() @@ -39,6 +40,7 @@ def test_prism_extruded(): buffers = {-1.0: 0.0, 1.0: 0.0} model = Model(n_threads=1) + model._initialize_model() prism_obj = Prism(polygons=polygon, buffers=buffers, model=model) dim, tags = prism_obj.instanciate()[0]