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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (ParaView plugin) support direct CGNS-tree/VTK conversion, including VTK-to-CGNS export, CGNS hierarchy and tag preservation, and bulk connectivity conversion for uniform fixed-size unstructured grids. The self-contained plugin bundle can be written to a caller-provided directory.
- (storage/writer) add optional `sample_callback` to `save_to_disk`, invoked once per sample right after it is written to disk as `sample_callback(split_name, index, sample_path)`. This lets callers process samples one by one once written instead of waiting for the whole dataset. Currently supported for the `cgns` backend, including parallel writing (`num_proc > 1`), where the callback runs inside the worker processes and must be picklable and process-safe.
- (python env) add python 3.14 support
- (dataset-viewer) add an **Open in ParaView** button for local datasets: it serves the selected dataset through a loopback-only `plaid-serve` data API on the first available port in 8081–8090 and launches ParaView with the PLAID plugin, keeping the server alive until ParaView exits. The ParaView executable is read from `PARAVIEW_EXEC` (default `paraview`).
- (docs) generate the viewer screenshots from a synthetic dataset (`docs/source/images/generate_viewer_images.py`) and embed an interactive skrub report, built with the documentation, in the viewer page.

### Changed

- (plaidcheck/problem_definition) support multiple subsplits per problem-definition split.
- (dataset-viewer) keep the camera viewpoint when changing samples; it is only reset on the first load or by **Reset camera**.
- (dataset-viewer) keep the current physical time when changing samples, using the closest available time step of the new sample.
- (plaid-serve) `--ParaViewRun` now starts the data API before launching ParaView, and passes a generated script that creates and displays a **Plaid Explorer** source automatically.
- (ParaView plugin) rename the server port property from `Port` to `PortNB` (`SetPort` to `SetPortNB`); update ParaView state files and scripts that set it.

### Fixed

Expand Down
4 changes: 4 additions & 0 deletions docs/source/concepts/serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ location in the JSON body using either `dataset` or `uri`.
| `--dataset PATH` | Default local PLAID dataset directory used by dataset endpoints. |
| `--ParaViewRun` | Launch ParaView with the PLAID plugin, set `PLAID_PORT` to the selected port, and stop the server when ParaView exits. The ParaView executable is read from `PARAVIEW_EXEC`. |

When `--ParaViewRun` is used, the launcher passes a generated script to
ParaView to create and display a **Plaid Explorer** source. The script is kept
available until the launcher exits.

## Request conventions

Dataset endpoints use `POST` with a JSON object body. The server resolves the
Expand Down
22 changes: 22 additions & 0 deletions docs/source/concepts/viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ already be filled in. The root controls and dataset selector are hidden by

Pick the **Dataset** you want to inspect.

For a **local** dataset, click **Open in ParaView** to launch ParaView with the
PLAID plugin connected to a loopback-only `plaid-serve` data API. The viewer
chooses the first available API port from **8081–8090**, keeping it close to the
default viewer port (8080), and keeps the server running until that ParaView
instance exits (or the viewer shuts down). If all ports in this range are in use
or denied, the launch reports an error instead of using a high-numbered port.
Your firewall must allow the selected API port. ParaView must be installed
and available as `paraview` on your `PATH`, or you must set `PARAVIEW_EXEC` to
the ParaView executable **before starting `plaid-viewer`**. For example, when
running from WSL with ParaView installed on Windows:

```bash
export PARAVIEW_EXEC=/mnt/c/Users/User/Apps/ParaView-6.2.0-Windows-Python3.12-msvc2017-AMD64/bin/paraview.exe
uv run plaid-viewer
```

Replace the example path with the location of your ParaView installation.
The launch script adds and displays a **Plaid Explorer** source in ParaView
automatically so you can inspect and compare samples.
Hub-streamed datasets are not supported by `plaid-serve` and cannot be opened
with this button.

The list depends on the selected source tab: local datasets are shown on the
**Local** tab, while streamed Hub datasets are shown on the **Hub** tab.

Expand Down
38 changes: 19 additions & 19 deletions src/plaid/cli/paraview_plugin/PlaidParaViewPlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ def __init__(
inputType=inputType,
outputType=outputType,
)
self.host: str = plaid_explorer_server_default_host
self.port: int = plaid_explorer_server_default_port
self.host_name: str = plaid_explorer_server_default_host
self.port_nb: int = plaid_explorer_server_default_port
self.server_ok = False

def _CleanCache(self):
Expand All @@ -329,18 +329,18 @@ def _CleanCache(self):
def SetHost(self, value):
"""Set the server host address to connect to for fetching dataset information and samples."""
value = str(value)
if self.host != value:
self.host = value
if self.host_name != value:
self.host_name = value
self._CleanCache()

@smproperty.intvector(
name="Port", default_values=str(plaid_explorer_server_default_port)
name="PortNB", default_values=str(plaid_explorer_server_default_port)
)
def SetPort(self, value):
def SetPortNB(self, value):
"""Set the server port to connect to for fetching dataset information and samples."""
value = int(value)
if self.port != value:
self.port = value
if self.port_nb != value:
self.port_nb = value
self._CleanCache()

def isServerOK(self):
Expand All @@ -349,7 +349,7 @@ def isServerOK(self):
try:
with request.urlopen(
request.Request(
url=f"http://{self.host}:{self.port}/health",
url=f"http://{self.host_name}:{self.port_nb}/health",
headers={"Content-Type": "application/json"},
method="POST",
),
Expand Down Expand Up @@ -384,7 +384,7 @@ def _request_json(
data = json.dumps(payload).encode("utf-8") if payload is not None else None

req = request.Request(
url=f"http://{self.host}:{self.port}{endpoint}",
url=f"http://{self.host_name}:{self.port_nb}{endpoint}",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
Expand Down Expand Up @@ -428,11 +428,11 @@ def SetHost(self, value):
return super().SetHost(value)

@smproperty.intvector(
name="Port", default_values=str(plaid_explorer_server_default_port)
name="PortNB", default_values=str(plaid_explorer_server_default_port)
)
def SetPort(self, value):
def SetPortNB(self, value):
"""Set the server port to connect to for fetching dataset information and samples."""
return super().SetPort(value)
return super().SetPortNB(value)

@smproperty.stringvector(
name="SelectSplit", default_values="", immediate_update="1"
Expand Down Expand Up @@ -553,7 +553,7 @@ def __init__(self):
self._timestep_values_cache: list[float] | None = None
self._sample_cache = None
self.host: str = plaid_process_server_default_host
self.port: int = plaid_process_server_default_port
self.port_nb: int = plaid_process_server_default_port
self.process_op: str = ""
self.ensure_3D_points = False

Expand Down Expand Up @@ -617,13 +617,13 @@ def SetHost(self, value):
self._PropertyChanged()

@smproperty.intvector(
name="Port", default_values=int(plaid_process_server_default_port)
name="PortNB", default_values=int(plaid_process_server_default_port)
)
def SetPort(self, value):
def SetPortNB(self, value):
"""Set the server port to connect to for processing data."""
value = int(value)
if self.port != value:
self.port = value
if self.port_nb != value:
self.port_nb = value
self._PropertyChanged()

def _request_json(
Expand All @@ -646,7 +646,7 @@ def _request_json(
data = json.dumps(payload).encode("utf-8") if payload is not None else None

req = request.Request(
url=f"http://{self.host}:{self.port}{endpoint}",
url=f"http://{self.host}:{self.port_nb}{endpoint}",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
Expand Down
84 changes: 75 additions & 9 deletions src/plaid/cli/paraview_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Utilities to launch ParaView with the PLAID plugin configured."""

import atexit
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Optional

paraview_exec = "paraview"

Expand Down Expand Up @@ -70,18 +72,82 @@ def convert_wsl_to_win(wsl_path: str) -> str:
return result.stdout.strip()


def run_paraview_with_plugin():
"""Launches ParaView with environment variables set to load the plugin."""
def run_paraview_with_plugin_client_server(
port: int = 8000, options: Optional[dict[str, str]] = None
) -> subprocess.Popen:
"""Launch ParaView with the plugin and a generated explorer script.

Args:
port: Port of the PLAID data API.
options: Optional source settings, including ``tree_name``.

Returns:
The ParaView process.
"""
my_env = os.environ.copy()

my_env["PV_PLUGIN_PATH"] = str(get_ParaView_plugin_path_one_file())
my_env["PARAVIEW_LOG_PLUGIN_VERBOSITY"] = "ON"
# my_env["PARAVIEW_LOG_PLUGIN_VERBOSITY"] = "ON" # uncomment this for debug
my_env["PLAID_PORT"] = str(port)

current_pv_path = os.environ.get("PARAVIEW_EXEC", paraview_exec)
if current_pv_path.startswith("/mnt/") and os:
# we are in a wsl and the paraview exec is windows, we need to convert the wsl path
# my_env["PV_PLUGIN_PATH"] = convert_wsl_to_win(str(get_ParaView_plugin_path()))
my_env["WSLENV"] = "PV_PLUGIN_PATH/p:PARAVIEW_LOG_PLUGIN_VERBOSITY/p"

process = subprocess.Popen([current_pv_path], env=my_env)
if current_pv_path.startswith("/mnt/"):
my_env["WSLENV"] = (
my_env.get("WSLENV", "") + ":" if my_env.get("WSLENV") else ""
) + "PV_PLUGIN_PATH/p:PARAVIEW_LOG_PLUGIN_VERBOSITY:PLAID_PORT:SCRIPT_PATH"

if options is None:
options = {}
tree_name = options.get("tree_name", "PlaidExplorer1")

script = f"""

#### import the simple module from the paraview
from paraview.simple import *
#### disable automatic camera reset on 'Show'
paraview.simple._DisableFirstRenderCameraReset()

# create a new 'Plaid Explorer'
plaidExplorer1 = PlaidExplorer(registrationName={tree_name!r})
plaidExplorer1.UpdatePipeline()
# get animation scene
animationScene1 = GetAnimationScene()

# update animation scene based on data timesteps
animationScene1.UpdateAnimationUsingDataTimeSteps()

# get active view
renderView1 = GetActiveViewOrCreate('RenderView')
# show data in view
plaidExplorer1Display = Show(plaidExplorer1, renderView1)

# trace defaults for the display properties.
plaidExplorer1Display.Representation = 'Surface'

# reset view to fit data
renderView1.ResetCamera(False, 0.9)

# update the view to ensure updated data information
renderView1.Update()
Show(plaidExplorer1)
Render()
Show(plaidExplorer1)
Render()
"""
script_dir = tempfile.TemporaryDirectory()
file_path = os.path.join(script_dir.name, "script.py")
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(script)
my_env["SCRIPT_PATH"] = file_path
process = subprocess.Popen(
[current_pv_path, "--script", file_path],
env=my_env,
stdin=subprocess.DEVNULL,
)
except BaseException:
script_dir.cleanup()
raise
# ParaView may not read the script immediately after Popen returns.
atexit.register(script_dir.cleanup)
return process
18 changes: 8 additions & 10 deletions src/plaid/cli/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import argparse
import json
import logging
import os
import threading
from dataclasses import dataclass, field
from http import HTTPStatus
Expand Down Expand Up @@ -500,22 +499,21 @@ def main(argv: list[str] | None = None) -> int:
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
if args.ParaViewRun:
os.environ["PLAID_PORT"] = str(args.port)
from .paraview_plugin import run_paraview_with_plugin

process = run_paraview_with_plugin()
else:
process = None

context = ServeContext(default_dataset_uri=args.dataset)
server = _ServeHTTPServer((str(args.host), int(args.port)), context)
log.info("PLAID data API listening on http://%s:%s", args.host, args.port)

if process is None:
if not args.ParaViewRun:
log.info("Running PLAID data API until interrupted")
server.serve_forever()
else:
from .paraview_plugin import run_paraview_with_plugin_client_server

try:
process = run_paraview_with_plugin_client_server(port=server.server_port)
except BaseException:
server.server_close()
raise
log.info("Running PLAID data API for ParaView")
_run_server_until_process_exits(server, process)

Expand Down
Loading
Loading