gri-plot 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {gri_plot-0.2.3 → gri_plot-0.2.4}/PKG-INFO +6 -17
  2. {gri_plot-0.2.3 → gri_plot-0.2.4}/README.md +5 -15
  3. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/__init__.py +5 -26
  4. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/figure3d.py +3 -83
  5. gri_plot-0.2.4/gri_plot/observables/__init__.py +15 -0
  6. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/surfaces/__init__.py +3 -12
  7. gri_plot-0.2.4/gri_plot/surfaces/mesh.py +133 -0
  8. {gri_plot-0.2.3 → gri_plot-0.2.4}/pyproject.toml +9 -2
  9. gri_plot-0.2.4/report.xml +1 -0
  10. gri_plot-0.2.4/test/test_figure3d.py +79 -0
  11. gri_plot-0.2.4/test/test_frames.py +136 -0
  12. gri_plot-0.2.4/test/test_surfaces/test_mesh.py +51 -0
  13. {gri_plot-0.2.3 → gri_plot-0.2.4}/uv.lock +0 -175
  14. gri_plot-0.2.3/.coveragerc +0 -8
  15. gri_plot-0.2.3/gri_plot/observables/__init__.py +0 -21
  16. gri_plot-0.2.3/gri_plot/observables/fdoa.py +0 -256
  17. gri_plot-0.2.3/gri_plot/observables/tdoa.py +0 -203
  18. gri_plot-0.2.3/gri_plot/observables/terrain.py +0 -295
  19. gri_plot-0.2.3/gri_plot/surfaces/gradients/__init__.py +0 -39
  20. gri_plot-0.2.3/gri_plot/surfaces/gradients/axis.py +0 -76
  21. gri_plot-0.2.3/gri_plot/surfaces/gradients/line.py +0 -88
  22. gri_plot-0.2.3/gri_plot/surfaces/gradients/plane.py +0 -55
  23. gri_plot-0.2.3/gri_plot/surfaces/gradients/point.py +0 -42
  24. gri_plot-0.2.3/gri_plot/surfaces/intersection.py +0 -219
  25. gri_plot-0.2.3/gri_plot/surfaces/mesh.py +0 -605
  26. gri_plot-0.2.3/report.xml +0 -1
  27. gri_plot-0.2.3/test/test_figure3d.py +0 -287
  28. gri_plot-0.2.3/test/test_frames.py +0 -44
  29. gri_plot-0.2.3/test/test_observables/test_fdoa.py +0 -59
  30. gri_plot-0.2.3/test/test_observables/test_tdoa.py +0 -72
  31. gri_plot-0.2.3/test/test_observables/test_terrain.py +0 -45
  32. gri_plot-0.2.3/test/test_surfaces/test_intersection.py +0 -203
  33. gri_plot-0.2.3/test/test_surfaces/test_mesh.py +0 -239
  34. {gri_plot-0.2.3 → gri_plot-0.2.4}/.docs_other_projects.md +0 -0
  35. {gri_plot-0.2.3 → gri_plot-0.2.4}/.gitignore +0 -0
  36. {gri_plot-0.2.3 → gri_plot-0.2.4}/.gitlab-ci-deps.yml +0 -0
  37. {gri_plot-0.2.3 → gri_plot-0.2.4}/.gitlab-ci.yml +0 -0
  38. {gri_plot-0.2.3 → gri_plot-0.2.4}/.init_venv.sh +0 -0
  39. {gri_plot-0.2.3 → gri_plot-0.2.4}/.pre-commit-config.yaml +0 -0
  40. {gri_plot-0.2.3 → gri_plot-0.2.4}/.pre-commit-unskipped.py +0 -0
  41. {gri_plot-0.2.3 → gri_plot-0.2.4}/.python-version +0 -0
  42. {gri_plot-0.2.3 → gri_plot-0.2.4}/.ruff.toml +0 -0
  43. {gri_plot-0.2.3 → gri_plot-0.2.4}/.vscode/settings.json +0 -0
  44. {gri_plot-0.2.3 → gri_plot-0.2.4}/CONTRIBUTING.md +0 -0
  45. {gri_plot-0.2.3 → gri_plot-0.2.4}/LICENSE +0 -0
  46. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/figure_map.py +0 -0
  47. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/frames.py +0 -0
  48. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/observables/aoa.py +0 -0
  49. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/observables/los.py +0 -0
  50. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/observables/range_sphere.py +0 -0
  51. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/plot_ellipse.py +0 -0
  52. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/py.typed +0 -0
  53. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/scatter.py +0 -0
  54. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/scatter_map.py +0 -0
  55. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/__init__.py +0 -0
  56. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/cone.py +0 -0
  57. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/cylinder.py +0 -0
  58. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/ellipse.py +0 -0
  59. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/ellipsoid.py +0 -0
  60. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/meshgen.py +0 -0
  61. {gri_plot-0.2.3 → gri_plot-0.2.4}/gri_plot/shapes/sphere.py +0 -0
  62. {gri_plot-0.2.3 → gri_plot-0.2.4}/notes.txt +0 -0
  63. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/__init__.py +0 -0
  64. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_figure_map.py +0 -0
  65. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_observables/__init__.py +0 -0
  66. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_observables/test_aoa.py +0 -0
  67. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_observables/test_los.py +0 -0
  68. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_observables/test_range_sphere.py +0 -0
  69. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_plot_ellipse.py +0 -0
  70. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_plot_scattermap.py +0 -0
  71. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_scatter.py +0 -0
  72. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/__init__.py +0 -0
  73. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/test_cone.py +0 -0
  74. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/test_cylinder.py +0 -0
  75. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/test_ellipse.py +0 -0
  76. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/test_ellipsoid.py +0 -0
  77. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_shapes/test_sphere.py +0 -0
  78. {gri_plot-0.2.3 → gri_plot-0.2.4}/test/test_surfaces/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gri-plot
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: 3D geolocation visualization and 2D plotting utilities built on Plotly
5
5
  Project-URL: Homepage, https://geosolresearch.com
6
6
  Project-URL: Repository, https://gitlab.com/geosol-foss/python/gri-plot
@@ -20,7 +20,6 @@ Requires-Python: >=3.12
20
20
  Requires-Dist: gri-utils>=0.3.3
21
21
  Requires-Dist: numpy>=2.3.3
22
22
  Requires-Dist: plotly>=6.3.0
23
- Requires-Dist: scikit-image>=0.25.0
24
23
  Description-Content-Type: text/markdown
25
24
 
26
25
  [![GeoSol Research Logo](https://geosolresearch.com/logos/foss_logo.png "GeoSol Research")](https://geosolresearch.com)
@@ -31,7 +30,7 @@ Description-Content-Type: text/markdown
31
30
 
32
31
  ## Overview
33
32
 
34
- This library renders implicit surfaces (TDOA hyperboloids, FDOA iso-Doppler surfaces, AOA cones, range spheres, terrain) and geometric shapes (ellipsoids, spheres, cones, cylinders) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
33
+ This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
35
34
 
36
35
  ## Installation
37
36
 
@@ -86,14 +85,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
86
85
  - **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
87
86
  - **`add_points()`**: Add labeled scatter points
88
87
  - **`add_line()`**: Add line segments connecting points
89
- - **`add_intersection()`**: Highlight where multiple surfaces intersect
90
88
  - **`plot_surfaces()`**: Convenience function for quick multi-surface plots
91
89
  - Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
92
90
 
93
91
  ```python
94
92
  from gri_plot import Figure3D
95
93
  from gri_plot.shapes import Sphere
96
- from gri_plot.observables import TdoaSurface
97
94
  import numpy as np
98
95
 
99
96
  fig = Figure3D()
@@ -113,7 +110,7 @@ Coordinate frame handling for 3D display:
113
110
 
114
111
  ## Shapes
115
112
 
116
- Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation (for intersection calculations).
113
+ Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
117
114
 
118
115
  - **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
119
116
  - **Sphere**: From center + radius
@@ -123,31 +120,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
123
120
 
124
121
  ## Observables
125
122
 
126
- Geolocation-specific surfaces for visualizing measurement iso-surfaces. Some inherit from shapes (reusing parametric rendering), others use marching cubes for mesh extraction.
123
+ Geolocation-specific surfaces that inherit from parametric shapes:
127
124
 
128
- - **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
129
- - **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
130
125
  - **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
131
126
  - **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
132
127
  - **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
133
- - **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
134
128
 
135
129
  ## Surfaces
136
130
 
137
- Low-level infrastructure for implicit surface rendering:
131
+ Low-level infrastructure for shape rendering:
138
132
 
139
133
  - **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
140
- - **Mesh utilities**: `field_to_mesh()` (marching cubes with Newton-Raphson surface projection and adaptive refinement), `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
141
- - **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
142
- - **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
134
+ - **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
143
135
 
144
136
  ## Dependencies
145
137
 
146
138
  - **plotly**: Interactive 2D and 3D visualization
147
- - **matplotlib**: Static plot support
148
139
  - **numpy**: Array operations
149
- - **scipy**: Marching cubes mesh extraction
150
- - **scikit-image**: Additional mesh processing
151
140
  - **gri-utils**: Coordinate conversions and constants
152
141
 
153
142
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Overview
8
8
 
9
- This library renders implicit surfaces (TDOA hyperboloids, FDOA iso-Doppler surfaces, AOA cones, range spheres, terrain) and geometric shapes (ellipsoids, spheres, cones, cylinders) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
9
+ This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
10
10
 
11
11
  ## Installation
12
12
 
@@ -61,14 +61,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
61
61
  - **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
62
62
  - **`add_points()`**: Add labeled scatter points
63
63
  - **`add_line()`**: Add line segments connecting points
64
- - **`add_intersection()`**: Highlight where multiple surfaces intersect
65
64
  - **`plot_surfaces()`**: Convenience function for quick multi-surface plots
66
65
  - Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
67
66
 
68
67
  ```python
69
68
  from gri_plot import Figure3D
70
69
  from gri_plot.shapes import Sphere
71
- from gri_plot.observables import TdoaSurface
72
70
  import numpy as np
73
71
 
74
72
  fig = Figure3D()
@@ -88,7 +86,7 @@ Coordinate frame handling for 3D display:
88
86
 
89
87
  ## Shapes
90
88
 
91
- Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation (for intersection calculations).
89
+ Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
92
90
 
93
91
  - **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
94
92
  - **Sphere**: From center + radius
@@ -98,31 +96,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
98
96
 
99
97
  ## Observables
100
98
 
101
- Geolocation-specific surfaces for visualizing measurement iso-surfaces. Some inherit from shapes (reusing parametric rendering), others use marching cubes for mesh extraction.
99
+ Geolocation-specific surfaces that inherit from parametric shapes:
102
100
 
103
- - **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
104
- - **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
105
101
  - **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
106
102
  - **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
107
103
  - **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
108
- - **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
109
104
 
110
105
  ## Surfaces
111
106
 
112
- Low-level infrastructure for implicit surface rendering:
107
+ Low-level infrastructure for shape rendering:
113
108
 
114
109
  - **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
115
- - **Mesh utilities**: `field_to_mesh()` (marching cubes with Newton-Raphson surface projection and adaptive refinement), `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
116
- - **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
117
- - **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
110
+ - **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
118
111
 
119
112
  ## Dependencies
120
113
 
121
114
  - **plotly**: Interactive 2D and 3D visualization
122
- - **matplotlib**: Static plot support
123
115
  - **numpy**: Array operations
124
- - **scipy**: Marching cubes mesh extraction
125
- - **scikit-image**: Additional mesh processing
126
116
  - **gri-utils**: Coordinate conversions and constants
127
117
 
128
118
 
@@ -1,16 +1,16 @@
1
1
  """Plotting utilities for GeoSol Research.
2
2
 
3
3
  This package provides utilities for 2D and 3D visualization using Plotly,
4
- including scatter plots, geographic maps, and 3D surfaces for geolocation
5
- observables.
4
+ including scatter plots, geographic maps, and parametric 3D shapes for
5
+ geolocation observables.
6
6
 
7
7
  Main components:
8
8
  - scatter: 2D scatter plots with Plotly
9
9
  - scatter_map: Geographic scatter maps
10
10
  - Figure3D: 3D figure for combining surfaces
11
11
  - shapes: Geometric shapes (Ellipsoid, Sphere, Cone, Cylinder)
12
- - observables: Geolocation surfaces (TDOA, FDOA, AOA, etc.)
13
- - surfaces: Low-level surface utilities
12
+ - observables: Parametric geolocation surfaces (AOA, LOS, Range)
13
+ - surfaces: ImplicitShape ABC and mesh trace utilities
14
14
  - frames: Coordinate frame handling
15
15
  """
16
16
 
@@ -23,11 +23,8 @@ from .frames import Bounds, Frame, FrameTransformer
23
23
  # Observables
24
24
  from .observables import (
25
25
  AoaSurface,
26
- FdoaSurface,
27
26
  LosSurface,
28
27
  RangeSphere,
29
- TdoaSurface,
30
- TerrainSurface,
31
28
  )
32
29
  from .plot_ellipse import plot_ellipse
33
30
  from .scatter import scatter
@@ -45,15 +42,7 @@ from .surfaces import (
45
42
  expand_bounds,
46
43
  merge_bounds,
47
44
  )
48
- from .surfaces.gradients import (
49
- axis_value,
50
- distance_from_axis,
51
- distance_from_line,
52
- distance_from_line_segment,
53
- distance_from_plane,
54
- distance_from_point,
55
- )
56
- from .surfaces.mesh import field_to_mesh, grid_to_mesh, vertices_to_mesh3d
45
+ from .surfaces.mesh import grid_to_mesh, vertices_to_mesh3d
57
46
 
58
47
  __all__ = [
59
48
  "DEFAULT_COLORS",
@@ -63,7 +52,6 @@ __all__ = [
63
52
  "Cylinder",
64
53
  "Ellipse",
65
54
  "Ellipsoid",
66
- "FdoaSurface",
67
55
  "Figure3D",
68
56
  "FigureMap",
69
57
  "Frame",
@@ -73,19 +61,10 @@ __all__ = [
73
61
  "MapStyle",
74
62
  "RangeSphere",
75
63
  "Sphere",
76
- "TdoaSurface",
77
- "TerrainSurface",
78
- "axis_value",
79
64
  "cone_mesh",
80
65
  "cylinder_mesh",
81
- "distance_from_axis",
82
- "distance_from_line",
83
- "distance_from_line_segment",
84
- "distance_from_plane",
85
- "distance_from_point",
86
66
  "ellipsoid_mesh",
87
67
  "expand_bounds",
88
- "field_to_mesh",
89
68
  "grid_to_mesh",
90
69
  "merge_bounds",
91
70
  "plot_ellipse",
@@ -12,9 +12,7 @@ import numpy as np
12
12
  import plotly.graph_objects as go
13
13
 
14
14
  from .frames import Frame, FrameTransformer
15
- from .surfaces import ImplicitShape, expand_bounds, merge_bounds
16
- from .surfaces.intersection import IntersectionField
17
- from .surfaces.mesh import field_to_mesh, vertices_to_mesh3d
15
+ from .surfaces import ImplicitShape, merge_bounds
18
16
 
19
17
  if TYPE_CHECKING:
20
18
  from collections.abc import Sequence
@@ -140,7 +138,7 @@ class Figure3D:
140
138
 
141
139
  Args:
142
140
  surface: ImplicitShape object to visualize.
143
- resolution: Mesh resolution. If None, uses field_to_mesh default.
141
+ resolution: Mesh resolution. If None, uses the shape's default.
144
142
  color: Base color for single-color gradient. If None, uses full
145
143
  Viridis colorscale. For multiple surfaces, pick colors from
146
144
  DEFAULT_COLORS to distinguish them (e.g., color=DEFAULT_COLORS[0]).
@@ -304,84 +302,6 @@ class Figure3D:
304
302
  trace_type = type(trace)
305
303
  return trace_type(**trace_dict)
306
304
 
307
- def add_intersection( # noqa: PLR0913 - intersection has many visual options
308
- self,
309
- *surfaces: ImplicitShape,
310
- threshold: float = 1.5,
311
- steepness: float | None = None,
312
- resolution: int | None = None,
313
- color: str | None = None,
314
- opacity: float = 0.8,
315
- label: str | None = None,
316
- **kwargs,
317
- ) -> Figure3D:
318
- """Add an intersection visualization for multiple implicit surfaces.
319
-
320
- Highlights regions where multiple surfaces intersect by computing a
321
- combined "closeness" field. Single-surface regions are hidden (below
322
- threshold), while multi-surface intersections are progressively
323
- highlighted.
324
-
325
- Args:
326
- *surfaces: ImplicitSurface objects to intersect. Must be at least 2.
327
- threshold: Value subtracted from combined closeness. Default 1.5
328
- hides single-surface regions while showing 2+ intersections.
329
- steepness: Exponential falloff rate. If None, auto-computed based
330
- on number of surfaces to ensure good visual gradient.
331
- resolution: Mesh resolution. If None, uses field_to_mesh default.
332
- color: Base color for gradient. If None, uses next palette color.
333
- opacity: Surface opacity (0-1).
334
- label: Optional label for legends.
335
- **kwargs: Additional arguments passed to the Mesh3d trace.
336
-
337
- Returns:
338
- Self for method chaining.
339
-
340
- Raises:
341
- ValueError: If fewer than 2 surfaces provided.
342
- """
343
- intersection = IntersectionField(
344
- surfaces,
345
- threshold=threshold,
346
- steepness=steepness,
347
- )
348
-
349
- # Get bounds and add to tracking
350
- bounds_xyz = intersection.get_bounds_xyz()
351
- self._surface_bounds.append(bounds_xyz)
352
-
353
- # Determine color
354
- if color is None:
355
- color = self._next_color()
356
- colorscale = _color_to_gradient(color)
357
-
358
- # Generate mesh using marching cubes via field_to_mesh
359
- bounds = expand_bounds(bounds_xyz, 1.05)
360
- if resolution is None:
361
- vertices, faces = field_to_mesh(intersection.residual_fn, bounds)
362
- else:
363
- vertices, faces = field_to_mesh(
364
- intersection.residual_fn,
365
- bounds,
366
- resolution=resolution,
367
- )
368
-
369
- # Create Mesh3d trace
370
- trace = vertices_to_mesh3d(
371
- vertices,
372
- faces,
373
- colorscale=colorscale,
374
- opacity=opacity,
375
- name=label,
376
- **kwargs,
377
- )
378
-
379
- if self.display_frame != Frame.XYZ:
380
- trace = self._transform_trace(trace)
381
-
382
- self._traces.append(trace)
383
- return self
384
-
385
305
  def get_bounds_xyz(
386
306
  self,
387
307
  ) -> tuple[NDArray[np.floating], NDArray[np.floating]] | None:
@@ -457,7 +377,7 @@ def plot_surfaces(
457
377
  display_frame: Coordinate frame for visualization.
458
378
  origin_xyz: Origin for ENU frame.
459
379
  title: Figure title.
460
- resolution: Mesh resolution. If None, uses field_to_mesh default.
380
+ resolution: Mesh resolution. If None, uses the shape's default.
461
381
  **kwargs: Additional arguments passed to add_surface.
462
382
 
463
383
  Returns:
@@ -0,0 +1,15 @@
1
+ """Observable surface classes for geolocation visualization.
2
+
3
+ This module provides parametric surfaces for visualizing AOA cones,
4
+ line-of-sight rays, and range spheres.
5
+ """
6
+
7
+ from .aoa import AoaSurface
8
+ from .los import LosSurface
9
+ from .range_sphere import RangeSphere
10
+
11
+ __all__ = [
12
+ "AoaSurface",
13
+ "LosSurface",
14
+ "RangeSphere",
15
+ ]
@@ -21,26 +21,20 @@ class ImplicitShape(ABC):
21
21
  This ABC provides a unified interface for shapes that can be:
22
22
  - Evaluated via an implicit function (residual_fn)
23
23
  - Rendered as a Plotly Mesh3d trace
24
- - Used in intersection calculations
25
24
 
26
25
  Residual convention:
27
26
  - residual < 0: inside the shape
28
27
  - residual = 0: on the boundary
29
28
  - residual > 0: outside the shape
30
29
 
31
- The is_volume property determines intersection behavior:
32
- - True: interior (residual < 0) is the solution region
33
- - False: boundary (residual = 0) is the solution locus
30
+ The is_volume property describes whether the shape represents a solid
31
+ volume or a thin boundary surface.
34
32
  """
35
33
 
36
34
  @property
37
35
  @abstractmethod
38
36
  def is_volume(self) -> bool:
39
- """Whether this shape represents a volume (True) or surface (False).
40
-
41
- Volumes contribute to intersection everywhere inside (residual < 0).
42
- Surfaces contribute based on distance from boundary (|residual|).
43
- """
37
+ """Whether this shape represents a volume (True) or surface (False)."""
44
38
  ...
45
39
 
46
40
  @property
@@ -163,11 +157,8 @@ def expand_bounds(
163
157
  return center - half_extent * factor, center + half_extent * factor
164
158
 
165
159
 
166
- from .intersection import IntersectionField # noqa: E402
167
-
168
160
  __all__ = [
169
161
  "ImplicitShape",
170
- "IntersectionField",
171
162
  "expand_bounds",
172
163
  "merge_bounds",
173
164
  ]
@@ -0,0 +1,133 @@
1
+ """Core mesh utilities for 3D surface visualization.
2
+
3
+ Provides functions for creating Plotly Mesh3d traces from vertex/face arrays
4
+ and for building triangular meshes from parametric 2D grids.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import numpy as np
12
+ import plotly.graph_objects as go
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Callable
16
+
17
+ from numpy.typing import NDArray
18
+
19
+
20
+ def vertices_to_mesh3d( # noqa: PLR0913 - mesh rendering needs many options
21
+ vertices: NDArray[np.floating],
22
+ faces: NDArray[np.integer],
23
+ *,
24
+ intensity_fn: Callable[[NDArray[np.floating]], NDArray[np.floating]] | None = None,
25
+ color: str | None = None,
26
+ opacity: float = 0.7,
27
+ name: str | None = None,
28
+ colorscale: str | list = "Viridis",
29
+ colorbar_title: str | None = None,
30
+ showscale: bool = False,
31
+ **kwargs,
32
+ ) -> go.Mesh3d:
33
+ """Create a Plotly Mesh3d trace from vertices and faces.
34
+
35
+ This is the core rendering function. All shape and surface classes should
36
+ use this to generate their Plotly traces.
37
+
38
+ Args:
39
+ vertices: Vertex positions, shape (N, 3).
40
+ faces: Triangle indices, shape (M, 3).
41
+ intensity_fn: Optional function to compute vertex intensities for coloring.
42
+ Takes vertices (N, 3) and returns intensities (N,). If None and color
43
+ is None, defaults to distance from mesh centroid (works well in any
44
+ coordinate frame).
45
+ color: Uniform surface color. Mutually exclusive with intensity_fn.
46
+ opacity: Surface opacity (0-1).
47
+ name: Trace name for legend.
48
+ colorscale: Plotly colorscale name or custom colorscale list
49
+ (used with intensity_fn).
50
+ colorbar_title: Title for colorbar (used with intensity_fn).
51
+ showscale: Whether to show the colorbar.
52
+ **kwargs: Additional arguments for go.Mesh3d.
53
+
54
+ Returns:
55
+ Plotly Mesh3d trace.
56
+ """
57
+ vertices = np.asarray(vertices)
58
+ faces = np.asarray(faces)
59
+
60
+ trace_kwargs: dict = {
61
+ "x": vertices[:, 0],
62
+ "y": vertices[:, 1],
63
+ "z": vertices[:, 2],
64
+ "i": faces[:, 0],
65
+ "j": faces[:, 1],
66
+ "k": faces[:, 2],
67
+ "opacity": opacity,
68
+ }
69
+
70
+ if color is not None:
71
+ trace_kwargs["color"] = color
72
+ else:
73
+ if intensity_fn is not None:
74
+ intensity = intensity_fn(vertices)
75
+ else:
76
+ centroid = vertices.mean(axis=0)
77
+ intensity = np.linalg.norm(vertices - centroid, axis=-1)
78
+
79
+ trace_kwargs["intensity"] = intensity
80
+ trace_kwargs["colorscale"] = colorscale
81
+ trace_kwargs["showscale"] = showscale
82
+ if colorbar_title is not None:
83
+ trace_kwargs["colorbar"] = {"title": colorbar_title}
84
+
85
+ if name is not None:
86
+ trace_kwargs["name"] = name
87
+ trace_kwargs["showlegend"] = True
88
+
89
+ trace_kwargs.update(kwargs)
90
+ return go.Mesh3d(**trace_kwargs)
91
+
92
+
93
+ def grid_to_mesh(
94
+ x: NDArray[np.floating],
95
+ y: NDArray[np.floating],
96
+ z: NDArray[np.floating],
97
+ ) -> tuple[NDArray[np.floating], NDArray[np.integer]]:
98
+ """Convert a 2D grid of 3D points to vertices and triangular faces.
99
+
100
+ Used for parametric surfaces where x, y, z are 2D arrays defining
101
+ a surface patch.
102
+
103
+ Args:
104
+ x: X coordinates, 2D array of shape (M, N).
105
+ y: Y coordinates, 2D array of shape (M, N).
106
+ z: Z coordinates, 2D array of shape (M, N).
107
+
108
+ Returns:
109
+ Tuple of (vertices, faces) where:
110
+ - vertices: shape (M*N, 3)
111
+ - faces: shape ((M-1)*(N-1)*2, 3)
112
+ """
113
+ m, n = x.shape
114
+ vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
115
+
116
+ faces = []
117
+ for i in range(m - 1):
118
+ for j in range(n - 1):
119
+ idx00 = i * n + j
120
+ idx01 = i * n + (j + 1)
121
+ idx10 = (i + 1) * n + j
122
+ idx11 = (i + 1) * n + (j + 1)
123
+
124
+ faces.append([idx00, idx10, idx11])
125
+ faces.append([idx00, idx11, idx01])
126
+
127
+ return vertices, np.array(faces, dtype=np.int64)
128
+
129
+
130
+ __all__ = [
131
+ "grid_to_mesh",
132
+ "vertices_to_mesh3d",
133
+ ]
@@ -21,7 +21,6 @@ dependencies = [
21
21
  "gri-utils>=0.3.3",
22
22
  "numpy>=2.3.3",
23
23
  "plotly>=6.3.0",
24
- "scikit-image>=0.25.0",
25
24
  ]
26
25
 
27
26
  [tool.uv.sources]
@@ -63,5 +62,13 @@ dev = [
63
62
  "ruff>=0.13.1",
64
63
  ]
65
64
 
65
+ [tool.coverage.run]
66
+ source = ["gri_plot"]
67
+ branch = true
68
+
66
69
  [tool.coverage.report]
67
- fail_under = 70
70
+ fail_under = 50
71
+ skip_empty = true
72
+
73
+ [tool.coverage.html]
74
+ directory = ".cov_html"
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="24" tests="109" time="6.356" timestamp="2026-04-18T04:32:03.689023+00:00" hostname="runner-xs6vzpvoq-project-74598118-concurrent-0"><testcase classname="test.test_figure3d" name="test_enu_requires_origin" time="0.002" /><testcase classname="test.test_figure3d" name="test_build_returns_plotly_figure" time="0.988" /><testcase classname="test.test_figure3d" name="test_plot_surfaces_convenience" time="0.050" /><testcase classname="test.test_figure3d" name="test_visual_ellipsoid_enu" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_figure3d.py:39: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_figure3d" name="test_visual_multiple_spheres" time="0.000"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_figure3d.py:52: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_figure3d" name="test_visual_combined_surfaces" time="0.000"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_figure3d.py:64: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_figure_map" name="test_default_init" time="0.001" /><testcase classname="test.test_figure_map" name="test_blank_enu_requires_origin" time="0.001" /><testcase classname="test.test_figure_map" name="test_blank_enu_with_origin" time="0.001" /><testcase classname="test.test_figure_map" name="test_build_satellite_returns_figure" time="0.043" /><testcase classname="test.test_figure_map" name="test_build_blank_returns_figure" time="0.042" /><testcase classname="test.test_figure_map" name="test_build_with_title" time="0.036" /><testcase classname="test.test_figure_map" name="test_add_contour_returns_self" time="0.003" /><testcase classname="test.test_figure_map" name="test_add_contour_satellite_creates_scattermap" time="0.038" /><testcase classname="test.test_figure_map" name="test_add_contour_blank_creates_scatter" time="0.038" /><testcase classname="test.test_figure_map" name="test_add_contours_returns_self" time="0.002" /><testcase classname="test.test_figure_map" name="test_add_contours_creates_separate_traces" time="0.038" /><testcase classname="test.test_figure_map" name="test_add_contours_empty_list" time="0.037" /><testcase classname="test.test_figure_map" name="test_add_band_returns_self" time="0.009" /><testcase classname="test.test_figure_map" name="test_add_band_creates_filled_trace" time="0.044" /><testcase classname="test.test_figure_map" name="test_add_band_blank_mode" time="0.042" /><testcase classname="test.test_figure_map" name="test_add_band_empty_list" time="0.036" /><testcase classname="test.test_figure_map" name="test_add_filled_contour_returns_self" time="0.002" /><testcase classname="test.test_figure_map" name="test_add_filled_contour_creates_fill" time="0.037" /><testcase classname="test.test_figure_map" name="test_add_markers_returns_self" time="0.002" /><testcase classname="test.test_figure_map" name="test_add_markers_satellite" time="0.040" /><testcase classname="test.test_figure_map" name="test_add_markers_blank" time="0.038" /><testcase classname="test.test_figure_map" name="test_add_markers_single_point" time="0.039" /><testcase classname="test.test_figure_map" name="test_full_chaining" time="0.050" /><testcase classname="test.test_figure_map" name="test_auto_color_cycling" time="0.040" /><testcase classname="test.test_figure_map" name="test_visual_satellite_contours" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to inspect satellite map">/builds/geosol-foss/python/gri-plot/test/test_figure_map.py:308: Visual test - unskip to inspect satellite map</skipped></testcase><testcase classname="test.test_figure_map" name="test_visual_blank_enu" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to inspect blank ENU plot">/builds/geosol-foss/python/gri-plot/test/test_figure_map.py:321: Visual test - unskip to inspect blank ENU plot</skipped></testcase><testcase classname="test.test_frames" name="test_xyz_lla_roundtrip" time="0.001" /><testcase classname="test.test_frames" name="test_enu_requires_origin" time="0.001" /><testcase classname="test.test_frames" name="test_enu_roundtrip" time="0.001" /><testcase classname="test.test_frames" name="test_axis_labels" time="0.001" /><testcase classname="test.test_frames" name="test_axis_labels_enu" time="0.001" /><testcase classname="test.test_frames" name="test_xyz_to_display_is_identity" time="0.001" /><testcase classname="test.test_frames" name="test_lla_roundtrip_via_transformer" time="0.001" /><testcase classname="test.test_frames" name="test_transform_bounds_xyz_passthrough" time="0.001" /><testcase classname="test.test_frames" name="test_transform_bounds_enu_orders_corners" time="0.002" /><testcase classname="test.test_frames" name="test_transform_bounds_lla_samples_all_corners" time="0.001" /><testcase classname="test.test_observables.test_aoa" name="test_residual_on_axis_inside" time="0.002" /><testcase classname="test.test_observables.test_aoa" name="test_residual_perpendicular_outside" time="0.001" /><testcase classname="test.test_observables.test_aoa" name="test_invalid_max_range" time="0.001" /><testcase classname="test.test_observables.test_aoa" name="test_visual_aoa_cone" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_observables/test_aoa.py:40: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_observables.test_los" name="test_residual_inside_outside" time="0.001" /><testcase classname="test.test_observables.test_los" name="test_invalid_length" time="0.001" /><testcase classname="test.test_observables.test_los" name="test_visual_los_ray" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_observables/test_los.py:32: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_observables.test_range_sphere" name="test_residual_on_surface" time="0.001" /><testcase classname="test.test_observables.test_range_sphere" name="test_invalid_range" time="0.001" /><testcase classname="test.test_observables.test_range_sphere" name="test_visual_range_sphere" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_observables/test_range_sphere.py:25: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_plot_ellipse" name="test_returns_figure" time="0.047" /><testcase classname="test.test_plot_ellipse" name="test_requires_at_least_one_ellipse" time="0.002" /><testcase classname="test.test_plot_ellipse" name="test_multiple_ellipses_create_traces" time="0.044" /><testcase classname="test.test_plot_ellipse" name="test_show_axes_false_reduces_traces" time="0.075" /><testcase classname="test.test_plot_ellipse" name="test_visual_single_ellipse" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to view">/builds/geosol-foss/python/gri-plot/test/test_plot_ellipse.py:40: Visual test - unskip to view</skipped></testcase><testcase classname="test.test_plot_ellipse" name="test_visual_multiple_ellipses" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to view">/builds/geosol-foss/python/gri-plot/test/test_plot_ellipse.py:48: Visual test - unskip to view</skipped></testcase><testcase classname="test.test_plot_scattermap" name="test_map_plot_single" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_plot_scattermap.py:6: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_plot_scattermap" name="test_map_plot_mult" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_plot_scattermap.py:23: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_single" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:7: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_double" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:14: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_x_space" time="0.000"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:22: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_sequence" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:30: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_kwargs" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:38: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_trace_kwargs" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:46: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_scatter" name="test_trace_kwargs2" time="0.001"><skipped type="pytest.skip" message="manual only for demonstration and test">/builds/geosol-foss/python/gri-plot/test/test_scatter.py:54: manual only for demonstration and test</skipped></testcase><testcase classname="test.test_shapes.test_cone" name="test_residual_inside_outside" time="0.002" /><testcase classname="test.test_shapes.test_cone" name="test_invalid_parameters[0.0-100.0]" time="0.001" /><testcase classname="test.test_shapes.test_cone" name="test_invalid_parameters[1.5707963267948966-100.0]" time="0.001" /><testcase classname="test.test_shapes.test_cone" name="test_invalid_parameters[0.5-0.0]" time="0.001" /><testcase classname="test.test_shapes.test_cone" name="test_invalid_parameters[0.5--10.0]" time="0.001" /><testcase classname="test.test_shapes.test_cone" name="test_visual_cone" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_shapes/test_cone.py:41: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_shapes.test_cylinder" name="test_residual_inside_outside" time="0.001" /><testcase classname="test.test_shapes.test_cylinder" name="test_invalid_parameters[0.0-100.0]" time="0.001" /><testcase classname="test.test_shapes.test_cylinder" name="test_invalid_parameters[-10.0-100.0]" time="0.001" /><testcase classname="test.test_shapes.test_cylinder" name="test_invalid_parameters[25.0-0.0]" time="0.001" /><testcase classname="test.test_shapes.test_cylinder" name="test_invalid_parameters[25.0--10.0]" time="0.001" /><testcase classname="test.test_shapes.test_cylinder" name="test_visual_cylinder" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_shapes/test_cylinder.py:44: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_shapes.test_ellipse" name="test_properties_stored" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_center_defaults_to_origin" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_ori_deg_normalized[370.0-10.0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_ori_deg_normalized[-10.0-350.0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_ori_deg_normalized[0.0-0.0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_ori_deg_normalized[360.0-0.0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_ori_deg_normalized[720.0-0.0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_rejects_non_positive_axes[0-50]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_rejects_non_positive_axes[-10-50]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_rejects_non_positive_axes[100-0]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_rejects_non_positive_axes[100--5]" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_rejects_sma_less_than_smi" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_boundary_shape" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_boundary_extent_at_zero_orientation" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_boundary_respects_center" time="0.001" /><testcase classname="test.test_shapes.test_ellipse" name="test_repr" time="0.001" /><testcase classname="test.test_shapes.test_ellipsoid" name="test_residual_on_surface" time="0.001" /><testcase classname="test.test_shapes.test_ellipsoid" name="test_requires_covariance_or_semi_axes" time="0.001" /><testcase classname="test.test_shapes.test_ellipsoid" name="test_rejects_both_covariance_and_semi_axes" time="0.001" /><testcase classname="test.test_shapes.test_ellipsoid" name="test_visual_ellipsoid_from_covariance" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_shapes/test_ellipsoid.py:31: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_shapes.test_ellipsoid" name="test_visual_ellipsoid_from_semi_axes" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_shapes/test_ellipsoid.py:42: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_shapes.test_sphere" name="test_residual_fn[point_offset0-0]" time="0.001" /><testcase classname="test.test_shapes.test_sphere" name="test_residual_fn[point_offset1--1]" time="0.001" /><testcase classname="test.test_shapes.test_sphere" name="test_residual_fn[point_offset2-1]" time="0.002" /><testcase classname="test.test_shapes.test_sphere" name="test_invalid_radius" time="0.001" /><testcase classname="test.test_shapes.test_sphere" name="test_visual_sphere" time="0.001"><skipped type="pytest.skip" message="Visual test - unskip to generate plot">/builds/geosol-foss/python/gri-plot/test/test_shapes/test_sphere.py:37: Visual test - unskip to generate plot</skipped></testcase><testcase classname="test.test_surfaces.test_mesh" name="test_vertices_to_mesh3d_returns_mesh3d" time="0.002" /><testcase classname="test.test_surfaces.test_mesh" name="test_vertices_to_mesh3d_uniform_color" time="0.003" /><testcase classname="test.test_surfaces.test_mesh" name="test_vertices_to_mesh3d_intensity_fn_applied" time="0.002" /><testcase classname="test.test_surfaces.test_mesh" name="test_grid_to_mesh_produces_expected_shape" time="0.001" /></testsuite></testsuites>