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