{ "cells": [ { "cell_type": "markdown", "id": "d2656f40", "metadata": {}, "source": [ "# Managing collections of point clouds\n", "\n", "> **Note** — `cloud_of_clouds` automatically falls back to its model-only version when wxPython is unavailable.\n", "> You can also import the model explicitly: `from wolfhece.PyVertex._model import cloud_of_clouds`.\n", "> See the [Model/GUI architecture](architecture_model_gui.ipynb) tutorial for details.\n", "\n", "## What is a cloud_of_clouds?\n", "\n", "`cloud_of_clouds` is a **container** that groups multiple `cloud_vertices` instances into a single collection.\n", "It mirrors the `Zones → zone → vector` hierarchy, but for point clouds:\n", "\n", "```\n", "cloud_of_clouds ←→ Zones (collection)\n", " └── cloud_vertices ←→ zone (individual)\n", " └── wolfvertex ←→ vector (single point)\n", "```\n", "\n", "Typical use cases:\n", "\n", "- Group survey points by category (left bank, right bank, river bed…)\n", "- Keep spatial data organized while computing global bounds or statistics\n", "- Save and load multi-cloud datasets as a single JSON file\n", "- Merge all clouds into a flat cloud when needed\n", "\n", "## Prerequisites\n", "\n", "This tutorial assumes you are familiar with `cloud_vertices` (see [cloudpoints](cloudpoints.ipynb))." ] }, { "cell_type": "code", "execution_count": 17, "id": "6100ab48", "metadata": {}, "outputs": [], "source": [ "from wolfhece.PyVertex import cloud_vertices, cloud_of_clouds\n", "import numpy as np\n", "import tempfile, os" ] }, { "cell_type": "markdown", "id": "5f1c5e24", "metadata": {}, "source": [ "## Creating a cloud_of_clouds\n", "\n", "You can create an empty collection and add clouds later, or pass clouds at construction time." ] }, { "cell_type": "code", "execution_count": 18, "id": "75c39bf2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Collection \"survey_2024\" has 2 clouds and 7 vertices\n", "Cloud names: ['left_bank', 'right_bank']\n" ] } ], "source": [ "# Method 1: create an empty collection, then add clouds\n", "coc = cloud_of_clouds(idx='survey_2024')\n", "\n", "# Add a pre-existing cloud\n", "left_bank = cloud_vertices(idx='left_bank')\n", "left_bank.init_from_nparray(np.array([\n", " [100., 200., 50.],\n", " [110., 210., 51.],\n", " [120., 220., 49.],\n", "]))\n", "coc.add_cloud(left_bank)\n", "\n", "# Or create a cloud directly inside the collection\n", "right_bank = coc.create_cloud(idx='right_bank')\n", "right_bank.init_from_nparray(np.array([\n", " [105., 250., 48.],\n", " [115., 260., 47.],\n", " [125., 270., 46.],\n", " [135., 280., 45.],\n", "]))\n", "\n", "print(f'Collection \"{coc.idx}\" has {coc.nbclouds} clouds and {coc.nbvertices} vertices')\n", "print(f'Cloud names: {coc.cloud_names}')" ] }, { "cell_type": "code", "execution_count": 19, "id": "6d08dc11", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "coc2: 3 clouds, 10 vertices\n" ] } ], "source": [ "# Method 2: pass clouds at construction time\n", "bed = cloud_vertices(idx='bed')\n", "bed.init_from_nparray(np.array([\n", " [102., 230., 40.],\n", " [112., 240., 39.],\n", " [122., 250., 38.],\n", "]))\n", "\n", "coc2 = cloud_of_clouds(idx='survey_quick', clouds=[left_bank, right_bank, bed])\n", "print(f'coc2: {coc2.nbclouds} clouds, {coc2.nbvertices} vertices')" ] }, { "cell_type": "markdown", "id": "dcf02f91", "metadata": {}, "source": [ "## Accessing clouds\n", "\n", "`cloud_of_clouds` supports indexing by **integer position** or **name**, as well as standard Python\n", "iteration, `len()`, and `in` checks." ] }, { "cell_type": "code", "execution_count": 20, "id": "eff74a2b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "First cloud: left_bank — 3 vertices\n", "right_bank: 4 vertices\n", "\"left_bank\" in coc? True\n", "\"unknown\" in coc? False\n", " left_bank: 3 pts\n", " right_bank: 4 pts\n" ] } ], "source": [ "# By integer index\n", "print('First cloud:', coc[0].idx, '—', coc[0].nbvertices, 'vertices')\n", "\n", "# By name\n", "print('right_bank:', coc['right_bank'].nbvertices, 'vertices')\n", "\n", "# Containment check\n", "print('\"left_bank\" in coc?', 'left_bank' in coc)\n", "print('\"unknown\" in coc?', 'unknown' in coc)\n", "\n", "# Iterate\n", "for cloud in coc:\n", " print(f' {cloud.idx}: {cloud.nbvertices} pts')" ] }, { "cell_type": "markdown", "id": "f6dfd753", "metadata": {}, "source": [ "## Removing a cloud" ] }, { "cell_type": "code", "execution_count": 21, "id": "d9538115", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Removed: right_bank\n", "Remaining: ['left_bank']\n" ] } ], "source": [ "# Remove by name (returns the removed cloud, or None)\n", "removed = coc.remove_cloud('right_bank')\n", "print(f'Removed: {removed.idx}' if removed else 'Not found')\n", "print(f'Remaining: {coc.cloud_names}')\n", "\n", "# Re-add it for the rest of the tutorial\n", "coc.add_cloud(removed)" ] }, { "cell_type": "markdown", "id": "53127c2a", "metadata": {}, "source": [ "## Global bounds\n", "\n", "The collection computes the global bounding box across all its clouds." ] }, { "cell_type": "code", "execution_count": 22, "id": "43ce8b75", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "X bounds: (100.0, 135.0)\n", "Y bounds: (200.0, 280.0)\n", "Z bounds: (45.0, 51.0)\n" ] } ], "source": [ "coc.find_minmax()\n", "\n", "print(f'X bounds: {coc.xbounds}')\n", "print(f'Y bounds: {coc.ybounds}')\n", "print(f'Z bounds: {coc.zbounds}')" ] }, { "cell_type": "markdown", "id": "84ec8d91", "metadata": {}, "source": [ "## Adding values to clouds\n", "\n", "Extra value columns (beyond X, Y, Z) can be assigned to each cloud.\n", "`add_values` accepts either:\n", "\n", "- a **dict** `{cloud_name: array}` — values are assigned to matching clouds by name\n", "- a flat **ndarray** of length `nbvertices` — distributed in order across clouds" ] }, { "cell_type": "code", "execution_count": 23, "id": "eab19921", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " left_bank: [2.1 1.8 2.5]\n", " right_bank: [3. 2.7 3.2 2.9]\n" ] } ], "source": [ "# Assign depths via dict\n", "coc.add_values('depth', {\n", " 'left_bank': np.array([2.1, 1.8, 2.5]),\n", " 'right_bank': np.array([3.0, 2.7, 3.2, 2.9]),\n", "})\n", "\n", "# Retrieve values\n", "depths = coc.get_values('depth')\n", "for name, vals in depths.items():\n", " print(f' {name}: {vals}')" ] }, { "cell_type": "code", "execution_count": 24, "id": "8c00cbe5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " left_bank: [0.5 0.6 0.7]\n", " right_bank: [1.1 1.2 1.3 1.4]\n" ] } ], "source": [ "# Or assign a flat array (distributed across clouds in order)\n", "velocities = np.array([0.5, 0.6, 0.7, # left_bank (3 pts)\n", " 1.1, 1.2, 1.3, 1.4]) # right_bank (4 pts)\n", "coc.add_values('velocity', velocities)\n", "\n", "vels = coc.get_values('velocity')\n", "for name, vals in vels.items():\n", " print(f' {name}: {vals}')" ] }, { "cell_type": "markdown", "id": "2597ddff", "metadata": {}, "source": [ "## Getting all coordinates\n", "\n", "`get_all_xyz()` concatenates the XYZ coordinates from every cloud into a single `(N, 3)` array." ] }, { "cell_type": "code", "execution_count": 25, "id": "2a26e3a7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Shape: (7, 3)\n", "[[100. 200. 50.]\n", " [110. 210. 51.]\n", " [120. 220. 49.]\n", " [105. 250. 48.]\n", " [115. 260. 47.]\n", " [125. 270. 46.]\n", " [135. 280. 45.]]\n" ] } ], "source": [ "all_xyz = coc.get_all_xyz()\n", "print(f'Shape: {all_xyz.shape}')\n", "print(all_xyz)" ] }, { "cell_type": "markdown", "id": "df5397a6", "metadata": {}, "source": [ "## Iterating over all vertices\n", "\n", "Two iteration helpers cross all clouds:\n", "\n", "- `iter_all_vertices()` — yields each `wolfvertex` object\n", "- `iter_all_rows()` — yields `(cloud_idx, row_id, row_dict)` with full row data" ] }, { "cell_type": "code", "execution_count": 26, "id": "eb0a4e0b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " [left_bank] #0: (100.0, 200.0, 50.0)\n", " [left_bank] #1: (110.0, 210.0, 51.0)\n" ] } ], "source": [ "# Quick example with iter_all_rows\n", "for cloud_name, row_id, row in coc.iter_all_rows():\n", " v = row['vertex']\n", " print(f' [{cloud_name}] #{row_id}: ({v.x:.1f}, {v.y:.1f}, {v.z:.1f})')\n", " if row_id >= 1: # limit output\n", " break" ] }, { "cell_type": "markdown", "id": "ad05c27b", "metadata": {}, "source": [ "## Nearest neighbor query\n", "\n", "`find_nearest(xyz, nb=1)` searches across **all** clouds and returns the globally closest match.\n", "It returns a 4-tuple: `(distance, wolfvertex, row_dict, cloud_idx)`." ] }, { "cell_type": "code", "execution_count": 27, "id": "d88c8b9a", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "WARNING:root:xyz is a list of floats -- converting to a list of lists\n", "WARNING:root:xyz is a list of floats -- converting to a list of lists\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Nearest to [108.0, 225.0, 0.0]:\n", " Cloud: left_bank\n", " Vertex: (120.0, 220.0, 49.0)\n", " Distance: 50.70\n" ] } ], "source": [ "query = [108., 225., 0.]\n", "dist, vert, row, cloud_idx = coc.find_nearest(query)\n", "\n", "print(f'Nearest to {query}:')\n", "print(f' Cloud: {cloud_idx}')\n", "print(f' Vertex: ({vert.x:.1f}, {vert.y:.1f}, {vert.z:.1f})')\n", "print(f' Distance: {dist:.2f}')" ] }, { "cell_type": "markdown", "id": "394cb77c", "metadata": {}, "source": [ "## Merging all clouds into one\n", "\n", "`merge()` creates a **new** `cloud_vertices` that contains every vertex from every cloud.\n", "A `__source__` column is automatically added to track which cloud each vertex came from." ] }, { "cell_type": "code", "execution_count": 12, "id": "b7a5d052", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Merged cloud: 7 vertices\n", "Header: ['vertex', 'depth', 'velocity', '__source__']\n" ] } ], "source": [ "merged = coc.merge(idx='all_points')\n", "\n", "print(f'Merged cloud: {merged.nbvertices} vertices')\n", "print(f'Header: {merged.header}')" ] }, { "cell_type": "markdown", "id": "a5b0398c", "metadata": {}, "source": [ "## Display properties\n", "\n", "Display properties (color, width, alpha, …) can be set on the whole collection.\n", "The call is propagated to every child cloud." ] }, { "cell_type": "code", "execution_count": 28, "id": "7b31507e", "metadata": {}, "outputs": [], "source": [ "coc.set_alpha(180)\n", "coc.set_width(3)\n", "coc.set_legend_from_idx() # use cloud.idx as legend text\n", "\n", "# Legends\n", "coc.set_legend_visible(True)\n", "coc.set_legend_fontsize(12)" ] }, { "cell_type": "markdown", "id": "5ba38dfa", "metadata": {}, "source": [ "## Saving and loading (JSON)\n", "\n", "The collection can be serialized to a JSON file with `save_json()` and reloaded with `load_json()`.\n", "The file format stores all clouds, their vertices, properties, and value columns." ] }, { "cell_type": "code", "execution_count": 29, "id": "95713baa", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Saved to C:\\Users\\pierre\\AppData\\Local\\Temp\\tmpowk5e4cv\\survey.json\n", "Loaded: \"survey_2024\" — 2 clouds, 7 vertices\n", "Cloud names: ['left_bank', 'right_bank']\n" ] } ], "source": [ "# Save to a temporary file\n", "tmp_dir = tempfile.mkdtemp()\n", "json_path = os.path.join(tmp_dir, 'survey.json')\n", "\n", "coc.save_json(json_path)\n", "print(f'Saved to {json_path}')\n", "\n", "# Reload\n", "loaded = cloud_of_clouds.load_json(json_path)\n", "print(f'Loaded: \"{loaded.idx}\" — {loaded.nbclouds} clouds, {loaded.nbvertices} vertices')\n", "print(f'Cloud names: {loaded.cloud_names}')" ] }, { "cell_type": "markdown", "id": "da9f27b5", "metadata": {}, "source": [ "### Loading a single cloud_vertices JSON\n", "\n", "`load_json` also accepts files saved by `cloud_vertices.save_json()` (format `\"cloud_vertices\"`).\n", "In that case, the single cloud is wrapped in a `cloud_of_clouds` with one entry." ] }, { "cell_type": "code", "execution_count": 15, "id": "514adba2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded single as collection: 1 cloud, 3 vertices\n" ] } ], "source": [ "# Save a single cloud\n", "single_path = os.path.join(tmp_dir, 'single.json')\n", "left_bank.save_json(single_path)\n", "\n", "# Load it as a cloud_of_clouds (automatic wrapping)\n", "coc_from_single = cloud_of_clouds.load_json(single_path)\n", "print(f'Loaded single as collection: {coc_from_single.nbclouds} cloud, {coc_from_single.nbvertices} vertices')" ] }, { "cell_type": "markdown", "id": "f965a6d2", "metadata": {}, "source": [ "## Duplicating a collection\n", "\n", "`duplicate()` (or its alias `copy()`) creates a deep copy via JSON round-trip.\n", "The copy is fully independent — modifying it does not affect the original." ] }, { "cell_type": "code", "execution_count": 16, "id": "559398c1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Original: 7 vertices, Copy: 7 vertices\n", "Copy name: \"survey_copy\"\n" ] } ], "source": [ "coc_copy = coc.duplicate(idx='survey_copy')\n", "print(f'Original: {coc.nbvertices} vertices, Copy: {coc_copy.nbvertices} vertices')\n", "print(f'Copy name: \"{coc_copy.idx}\"')" ] }, { "cell_type": "markdown", "id": "6bc1c1ef", "metadata": {}, "source": [ "## Summary\n", "\n", "| Operation | Method |\n", "|-----------|--------|\n", "| Create empty | `cloud_of_clouds(idx='...')` |\n", "| Add existing cloud | `coc.add_cloud(cloud)` |\n", "| Create cloud in-place | `coc.create_cloud(idx='...')` |\n", "| Remove a cloud | `coc.remove_cloud('name')` or `coc.remove_cloud(0)` |\n", "| Access a cloud | `coc['name']` or `coc[0]` |\n", "| Number of clouds | `coc.nbclouds` or `len(coc)` |\n", "| Total vertices | `coc.nbvertices` |\n", "| Global bounds | `coc.xbounds`, `coc.ybounds`, `coc.zbounds` |\n", "| All XYZ | `coc.get_all_xyz()` → `(N, 3)` array |\n", "| Add values | `coc.add_values('key', dict_or_array)` |\n", "| Get values | `coc.get_values('key')` → `dict[str, ndarray]` |\n", "| Nearest neighbor | `coc.find_nearest(xyz)` → `(dist, vertex, row, cloud_name)` |\n", "| Merge all | `coc.merge()` → single `cloud_vertices` with `__source__` column |\n", "| Save | `coc.save_json('file.json')` |\n", "| Load | `cloud_of_clouds.load_json('file.json')` |\n", "| Deep copy | `coc.duplicate()` or `coc.copy()` |\n", "| Set display props | `coc.set_color(n)`, `coc.set_width(n)`, `coc.set_alpha(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 }