Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/loopstructural_visualisation/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
# loopstructural-visualisation

A LoopStructural interface for pyvista's Plotter class.
A LoopStructural interface for pyvista's Plotter class.

The viewer uses the current LoopStructural geometry and stratigraphic-column
APIs. Feature evaluation takes world coordinates; the viewer does not apply an
additional model-coordinate transform. Notebook integration supports both
PyVista's original Trame modules and the newer `trame-pyvista` package.

Compatibility checked on Windows with Python 3.13 and workspace LoopStructural
1.8.4:

| VTK | PyVista | Checks |
| --- | --- | --- |
| 9.7.1 | 0.49.1 | Core plotting, off-screen rendering, clipping, notebook UI |
| 9.6.2 | 0.48.4 | Core plotting, off-screen rendering, clipping |

CI also runs the plotting and notebook tests on Windows and Linux against the
VTK 9.6 and 9.7 release lines with the latest compatible PyVista.

From the workspace root, run the tests with:

```sh
uv sync --package loopstructuralvisualisation --extra tests --extra jupyter
uv run --no-sync pytest packages/loopstructural_visualisation/tests
```


To install `pip install loopstructuralvisualisation` or for a jupyter notebook environment (including the pyvista[jupyuter] dependencies) `pip install loopstructuralvisualisation[jupyter]`
Expand Down
2 changes: 1 addition & 1 deletion packages/loopstructural_visualisation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = ["numpy>=1.18", "pyvista>=0.42", "LoopStructural>=1.6.17"]
dependencies = ["numpy>=1.18", "matplotlib>=3.6", "pyvista>=0.44", "LoopStructural>=1.8.0"]

[project.optional-dependencies]
jupyter = ["pyvista[jupyter]"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class Loop2DView:
""" """

def __init__(self, model=None, bounding_box=np.zeros((2, 2)), nsteps=None, ax=None, **kwargs):
def __init__(self, model=None, bounding_box=None, nsteps=None, ax=None, **kwargs):
"""

Parameters
Expand All @@ -23,8 +23,9 @@ def __init__(self, model=None, bounding_box=np.zeros((2, 2)), nsteps=None, ax=No

self.xx = None
self.yy = None
self._model = None

self._bounding_box = bounding_box
self._bounding_box = np.zeros((2, 2)) if bounding_box is None else bounding_box
self._nsteps = nsteps
if self._nsteps is not None and self._bounding_box is not None:
self._update_grid()
Expand All @@ -50,9 +51,9 @@ def model(self):
@model.setter
def model(self, model):
if model is not None:
bb = np.array([model.origin[:2], model.maximum[:2]])
bb = np.array([model.bounding_box.origin[:2], model.bounding_box.maximum[:2]])
self.bounding_box = bb # model.bounding_box
self.nsteps = model.nsteps[:2]
self.nsteps = model.bounding_box.nsteps[:2]
self._model = model
self._update_grid()

Expand Down Expand Up @@ -171,7 +172,6 @@ def add_data(self, feature, val=True, grad=True, unfault=False, dip=True, **kwar
if unfault:
value_data = np.copy(feature.interpolator.get_value_constraints())

value_data[:, :3] = self.model.rescale(value_data[:, :3], inplace=False)
point_colour = kwargs.pop("point_colour", None)
if point_colour is None:
self.ax.scatter(
Expand All @@ -184,11 +184,10 @@ def add_data(self, feature, val=True, grad=True, unfault=False, dip=True, **kwar
)
if point_colour is not None:
self.ax.scatter(value_data[:, 0], value_data[:, 1], c=point_colour)
if grad:
if grad and ori_data:
symb_colour = kwargs.pop("symb_colour", "black")
symb_scale = kwargs.pop("symb_scale", 1.0)
gradient_data = np.hstack(ori_data)
gradient_data[:, :3] = self.model.rescale(gradient_data[:, :3], inplace=False)
gradient_data = np.vstack(ori_data).copy()
gradient_data[:, 3:5] /= np.linalg.norm(gradient_data[:, 3:5], axis=1)[:, None]
t = gradient_data[:, [4, 3]] * np.array([1, -1]).T
n = gradient_data[:, 3:5]
Expand All @@ -209,18 +208,19 @@ def add_data(self, feature, val=True, grad=True, unfault=False, dip=True, **kwar
def add_fault_ellipse(self, faults=None, **kwargs):
from matplotlib.patches import Ellipse

for f in self.model.stratigraphic_column["faults"].values():
center = self.model.rescale(f["FaultCenter"])
if faults is None:
faults = self.model.faults
for fault in faults:
center = fault.fault_centre
strike = fault.fault_strike_vector
e = Ellipse(
(center[0], center[1]),
f["HorizontalRadius"] * 2,
f["InfluenceDistance"] * 2,
360 - f["FaultDipDirection"],
facecolor="None",
edgecolor="k",
fault.fault_major_axis * 2,
fault.fault_minor_axis * 2,
angle=np.degrees(np.arctan2(strike[1], strike[0])),
**({'facecolor': 'none', 'edgecolor': 'k'} | kwargs),
)

self.ax.add_patch(e)
self.ax.add_patch(e)

def add_scalar_field(self, feature, z=0, **kwargs):
"""
Expand All @@ -240,9 +240,7 @@ def add_scalar_field(self, feature, z=0, **kwargs):
"""
zz = np.zeros(self.xx.shape)
zz[:] = z
v = feature.evaluate_value(
self.model.scale(np.array([self.xx, self.yy, zz]).T, inplace=False)
)
v = feature.evaluate_value(np.array([self.xx, self.yy, zz]).T)
return self.ax.imshow(
v.reshape(self.nsteps).T,
extent=[
Expand Down Expand Up @@ -271,11 +269,10 @@ def add_contour(self, feature, values, z=0, mask=None, **kwargs):
"""
zz = np.zeros(self.xx.shape)
zz[:] = z
v = feature.evaluate_value(
self.model.scale(np.array([self.xx, self.yy, zz]).T, inplace=False)
)
points = np.array([self.xx, self.yy, zz]).T
v = feature.evaluate_value(points)
if mask:
maskv = mask(self.model.scale(np.array([self.xx, self.yy, zz]).T, inplace=False))
maskv = mask(points)
v[~maskv] = np.nan
return self.ax.contour(
v.reshape(self.nsteps).T,
Expand All @@ -300,27 +297,14 @@ def add_model(self, z=0, cmap=None):
cmap : str/matplotlib colourmap, optional
specify a colour map, by default 'tab20'
"""
if cmap is None:
import matplotlib.colors as colors

colours = []
boundaries = []
data = []
for g in self.model.stratigraphic_column.keys():
if g == "faults":
continue
for v in self.model.stratigraphic_column[g].values():
data.append((v["id"], v["colour"]))
colours.append(v["colour"])
boundaries.append(v["id"]) # print(u,v)
cmap = colors.ListedColormap(colours)

zz = np.zeros_like(self.xx)
zz[:] = z # self.bounding_box[1,2]
pts = np.vstack([self.xx.flatten(), self.yy.flatten(), zz.flatten()])
if self.model is None:
logger.error("Mapview needs a model assigned to plot model on map")
return
if cmap is None:
cmap = self.model.stratigraphic_column.cmap()
vals = self.model.evaluate_model(pts.T, scale=True)
return self.ax.imshow(
vals.reshape(self.nsteps).T,
Expand Down
Loading
Loading