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:

  1. No headless usage — impossible to run on a compute server, in CI, or in Docker

  2. Heavy dependencies — importing a simple 2-D array pulled in wxPython + OpenGL

  3. Unit testing required mocking GUI components

  4. Fragile inheritance — subclassing WolfArray forced inheriting Element_To_Draw as well

The solution: each class is now split into:

  • A Model (*Model) containing all business logic, I/O and computations

  • A 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 a WolfArrayModel returns a WolfArrayModel

  • Calling gui.crop(...) on a WolfArray returns a WolfArray

  • If you create a subclass MyArray(WolfArrayModel), .crop() returns a MyArray

[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

vectorModel, zoneModel, ZonesModel

wolfhece.PyVertexvectors._models

Rendering in the viewer / wx interaction

vector, zone, Zones

wolfhece.PyVertexvectors

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 viewer

  • plotted — visibility flag

  • xmin, ymin, xmax, ymax — spatial extent

  • idx — 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

WolfArrayModel, vectorModel, zoneModel, ZonesModel, cloud_vertices_model

Interactive notebook (matplotlib)

WolfArrayModel or WolfArray — both have plot_matplotlib()

wxPython application / viewer

WolfArray, vector, zone, Zones, cloud_vertices

Subclassing for a project

WolfArrayModel (lighter, no GUI constraint)

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