Model / GUI architecture in wolfhece
This notebook is intended for advanced users who want to understand the Model/GUI separation implemented in wolfhece.
Why this separation?
Historically, the main classes (WolfArray, vector, zone, Zones, cloud_vertices, Triangulation) mixed data logic and graphical rendering (wxPython, OpenGL). This caused several issues:
No headless usage — impossible to run on a compute server, in CI, or in Docker
Heavy dependencies — importing a simple 2-D array pulled in wxPython + OpenGL
Unit testing required mocking GUI components
Fragile inheritance — subclassing
WolfArrayforced inheritingElement_To_Drawas well
The solution: each class is now split into:
A Model (
*Model) containing all business logic, I/O and computationsA GUI class (historical name kept) inheriting from the Model +
Element_To_Draw
The public API has not changed: existing code using WolfArray continues to work.
Class hierarchy overview
wolf_array
header_wolf
└── WolfArrayModel (_base.py) ← data + computations
├── WolfArray (_base_gui.py) ← + Element_To_Draw + OpenGL
└── WolfArrayMBModel (_mb_model.py) ← multi-block data
└── WolfArrayMB (_mb.py) ← + WolfArray + multi-block GUI
PyVertexvectors
vectorpropertiesModel → vectorproperties (visual properties)
vectorModel → vector (polylines / polygons)
zoneModel → zone (vector collection)
ZonesModel → Zones (+ wx.Frame) (zone collection)
TriangulationModel → Triangulation (triangle mesh)
GridModel → Grid (regular grid)
PyVertex
wolfvertex (3-D point — no split)
cloud_vertices_model → cloud_vertices (point cloud)
cloudproperties_model → cloudproperties (visual properties)
cloud_of_clouds_model → cloud_of_clouds (cloud collection)
In every case: Model on the left → GUI on the right. The GUI class inherits from the Model.
1. WolfArrayModel vs WolfArray
WolfArrayModel provides all computational features:
Arithmetic (
+,-,*,/,**)Masking, crop, rebin, convolve, gradient, Laplacian
Inpainting (eikonal)
I/O (reading/writing Wolf files)
Reprojection
Without any dependency on wxPython or OpenGL.
[1]:
from wolfhece.wolf_array import WolfArrayModel, WolfArray, header_wolf
from wolfhece.drawing_obj import Element_To_Draw
import numpy as np
# Create a header: 100×80 pixel grid, 1 m resolution, origin at (0, 0)
h = header_wolf()
h.nbx, h.nby = 100, 80
h.dx, h.dy = 1.0, 1.0
h.origx, h.origy = 0.0, 0.0
# --- Model: no GUI dependency ---
model = WolfArrayModel(srcheader=h)
model.array.data[:] = np.random.rand(h.nbx, h.nby)
print(f"Type : {type(model).__name__}")
print(f"Shape : {model.array.shape}")
print(f"Inherits Element_To_Draw? {isinstance(model, Element_To_Draw)}")
Type : WolfArrayModel
Shape : (100, 80)
Inherits Element_To_Draw? False
[2]:
# --- GUI: same API + OpenGL rendering ---
gui = WolfArray(srcheader=h)
gui.array.data[:] = model.array.data
print(f"Type : {type(gui).__name__}")
print(f"Inherits WolfArrayModel? {isinstance(gui, WolfArrayModel)}")
print(f"Inherits Element_To_Draw? {isinstance(gui, Element_To_Draw)}")
print(f"\nisinstance(gui, WolfArrayModel) => {isinstance(gui, WolfArrayModel)}")
print(f"isinstance(model, WolfArray) => {isinstance(model, WolfArray)}")
Type : WolfArray
Inherits WolfArrayModel? True
Inherits Element_To_Draw? True
isinstance(gui, WolfArrayModel) => True
isinstance(model, WolfArray) => False
Type preservation in subclasses
Methods of WolfArrayModel that return new instances (.crop(), .rebin(), .inpaint(), …) use type(self)(...) rather than WolfArrayModel(...). This guarantees that:
Calling
model.crop(...)on aWolfArrayModelreturns aWolfArrayModelCalling
gui.crop(...)on aWolfArrayreturns aWolfArrayIf you create a subclass
MyArray(WolfArrayModel),.crop()returns aMyArray
[3]:
# Type preservation demo
cropped_model = model.crop(i_start=10, j_start=10, nbx=40, nby=30)
cropped_gui = gui.crop(i_start=10, j_start=10, nbx=40, nby=30)
print(f"model.crop() -> {type(cropped_model).__name__}") # WolfArrayModel
print(f"gui.crop() -> {type(cropped_gui).__name__}") # WolfArray
# Custom subclass
class MyArray(WolfArrayModel):
pass
custom = MyArray(srcheader=h)
custom.array.data[:] = 42.0
cropped_custom = custom.crop(i_start=0, j_start=0, nbx=20, nby=20)
print(f"MyArray.crop() -> {type(cropped_custom).__name__}") # MyArray
model.crop() -> WolfArrayModel
gui.crop() -> WolfArray
MyArray.crop() -> MyArray
2. vectorModel / zoneModel / ZonesModel
The same principle applies to vector classes.
Need |
Class to use |
Module |
|---|---|---|
Pure geometry, computations, I/O |
|
|
Rendering in the viewer / wx interaction |
|
|
The GUI classes override factory methods to ensure type consistency:
# In zoneModel (model):
def _make_vector(self, ...) -> vectorModel: ...
# In zone (GUI), the override:
def _make_vector(self, ...) -> vector: ... # returns the GUI version
So when a GUI zone creates an internal vector, it is always a GUI vector capable of rendering itself.
[4]:
from wolfhece.PyVertexvectors import (
# Models
vectorModel, zoneModel, ZonesModel,
# GUI
vector, zone, Zones,
# Point (no model/GUI split)
wolfvertex,
)
# --- Pure model usage ---
vm = vectorModel(name='contour_model')
vm.add_vertex(wolfvertex(0., 0.))
vm.add_vertex(wolfvertex(100., 0.))
vm.add_vertex(wolfvertex(100., 50.))
vm.add_vertex(wolfvertex(0., 50.))
vm.close_force() # close the polygon
vm.update_lengths() # compute lengths
print(f"Length : {vm.length2D:.1f} m")
print(f"Area : {vm.area:.1f} m2")
print(f"Type : {type(vm).__name__}")
# --- Same thing with the GUI version ---
vg = vector(name='contour_gui')
vg.add_vertex(wolfvertex(0., 0.))
vg.add_vertex(wolfvertex(100., 0.))
vg.add_vertex(wolfvertex(100., 50.))
vg.add_vertex(wolfvertex(0., 50.))
vg.close_force()
vg.update_lengths()
print(f"\nGUI - Length : {vg.length2D:.1f} m")
print(f"GUI - isinstance(vectorModel)? {isinstance(vg, vectorModel)}")
Length : 300.0 m
Area : 5000.0 m2
Type : vectorModel
GUI - Length : 300.0 m
GUI - isinstance(vectorModel)? True
3. cloud_vertices — graceful fallback
PyVertex implements an additional pattern: automatic fallback.
# In PyVertex/__init__.py:
try:
from ._gui import cloud_vertices, cloudproperties, ...
except ImportError:
# No wxPython/OpenGL -> model-only versions
cloud_vertices = cloud_vertices_model
cloudproperties = cloudproperties_model
This means from wolfhece.PyVertex import cloud_vertices always works:
With wxPython -> you get the GUI class (OpenGL rendering)
Without wxPython -> you get the model class (computations only)
To force model-only usage (e.g. in a compute script):
[5]:
# Explicit model import — always available, even without wxPython
from wolfhece.PyVertex._model import cloud_vertices as cloud_vertices_model
from wolfhece.PyVertex import wolfvertex
import numpy as np
cloud = cloud_vertices_model()
pts = np.column_stack([
np.random.uniform(0, 100, 500),
np.random.uniform(0, 100, 500),
np.random.uniform(0, 10, 500),
])
cloud.init_from_nparray(pts)
print(f"Points : {len(cloud.myvertices)}")
print(f"Type : {type(cloud).__name__}")
# Compare with the default import (GUI if available)
from wolfhece.PyVertex import cloud_vertices
print(f"\nDefault cloud_vertices : {cloud_vertices.__module__}")
print(f"Is subclass of model? {issubclass(cloud_vertices, cloud_vertices_model)}")
Points : 500
Type : cloud_vertices
Default cloud_vertices : wolfhece.PyVertex._gui
Is subclass of model? True
4. TriangulationModel
The triangle mesh follows exactly the same pattern:
[6]:
from wolfhece.PyVertexvectors import TriangulationModel, Triangulation
print(f"TriangulationModel MRO : {[b.__name__ for b in TriangulationModel.__mro__]}")
print(f"Triangulation MRO : {[b.__name__ for b in Triangulation.__mro__]}")
print(f"\nTriangulation inherits TriangulationModel? {issubclass(Triangulation, TriangulationModel)}")
TriangulationModel MRO : ['TriangulationModel', 'object']
Triangulation MRO : ['Triangulation', 'TriangulationModel', 'Element_To_Draw', 'object']
Triangulation inherits TriangulationModel? True
5. Element_To_Draw — the integration mixin
Element_To_Draw is the base class that allows an object to be displayed in the WolfMapViewer.
It provides:
mapviewer— reference to the parent viewerplotted— visibility flagxmin, ymin, xmax, ymax— spatial extentidx— unique identifier
Element_To_Draw (drawing_obj.py)
├── WolfArray
├── Triangulation
├── Zones
├── cloud_vertices (gui)
└── cloud_of_clouds (gui)
Model classes do NOT inherit from ``Element_To_Draw``. This is the key to the separation: no GUI dependency in the model layer.
6. When to use what?
Context |
Classes to use |
|---|---|
Compute script, CI, HPC server |
|
Interactive notebook (matplotlib) |
|
wxPython application / viewer |
|
Subclassing for a project |
|
Simple rule: if you do not need OpenGL, use the *Model classes.
7. Import summary
# --- Pure models (no GUI dependency) ---
from wolfhece.wolf_array import WolfArrayModel, WolfArrayMBModel, header_wolf
from wolfhece.PyVertexvectors import vectorModel, zoneModel, ZonesModel, TriangulationModel
from wolfhece.PyVertex._model import cloud_vertices as cloud_vertices_model
from wolfhece.PyVertex import wolfvertex # no split (always model)
# --- GUI classes (inherit from models + Element_To_Draw) ---
from wolfhece.wolf_array import WolfArray, WolfArrayMB
from wolfhece.PyVertexvectors import vector, zone, Zones, Triangulation
from wolfhece.PyVertex import cloud_vertices # GUI if available, model fallback otherwise