Skip to content
Open
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
20 changes: 17 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ authors = [
{name = "Kitware Inc."},
]
dependencies = [
"trame>=3.12",
"trame>=3.13.2",
"trame-vuetify",
"trame-rca",
"trame-rca[turbo]>=2.6",
"trame-dockview",
"trame-dataclass >=2.1.3",
"pyproj>=3.6.1",
"netCDF4>=1.6.5",
"vtk>=9.6",
]
requires-python = ">=3.12"
readme = "README.rst"
Expand Down Expand Up @@ -94,7 +98,17 @@ isort.required-imports = []
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20", "PLC0415"]
"noxfile.py" = ["T20"]
"src/**" = ["SIM117"]
"src/**" = ["SIM117", "T"]
"src/e3sm_siteview/plugins/**" = [
"PLW",
"ARG",
"RET",
"PLC",
"SIM",
"EM",
"PGH",
"T",
]

[tool.semantic_release]
version_toml = [
Expand Down
37 changes: 37 additions & 0 deletions src/e3sm_siteview/analysis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import importlib.util
from pathlib import Path

REGISTRY = {}


def create_analysis(type, server, reader):
klass = REGISTRY.get(type)
if klass:
return klass(server, reader)
return None


def register_analysis(type, klass):
REGISTRY[type] = klass


def create_id_generator():
count = 0
while True:
count += 1
yield f"analysis_{count}"


def load_all_analysis():
for module_path in Path(__file__).parent.glob("*.py"):
if module_path.stem[0] == "_":
continue
name = module_path.stem
spec = importlib.util.spec_from_file_location(name, module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)


ANALYSIS_ID = create_id_generator()

__all__ = ["ANALYSIS_ID", "create_analysis", "load_all_analysis", "register_analysis"]
67 changes: 67 additions & 0 deletions src/e3sm_siteview/analysis/viz3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import vtkmodules.vtkRenderingOpenGL2 # noqa: F401
from trame.app import TrameComponent
from trame.ui.html import DivLayout
from trame.widgets import html
from trame.widgets import vtk as vtk_widgets

# VTK factory initialization
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa: F401
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkDataSetMapper,
vtkRenderer,
vtkRenderWindow,
vtkRenderWindowInteractor,
)

from e3sm_siteview.analysis import ANALYSIS_ID, register_analysis

NAME = "viz"


class Viz3D(TrameComponent):
def __init__(self, server, reader):
super().__init__(server)
self._id = next(ANALYSIS_ID)
self._reader = reader
self._setup_vtk(reader)
self._build_ui()

@property
def name(self):
return self._id

def _setup_vtk(self, reader):
mapper = vtkDataSetMapper()
actor = vtkActor(mapper=mapper)
reader >> mapper

renderer = vtkRenderer()
render_window = vtkRenderWindow()
render_window.AddRenderer(renderer)
render_window.OffScreenRenderingOn()

renderWindowInteractor = vtkRenderWindowInteractor()
renderWindowInteractor.SetRenderWindow(render_window)
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()

renderer.AddActor(actor)
renderer.ResetCamera()

self.actor = actor
self.mapper = mapper
self.renderer = renderer
self.render_window = render_window

def _build_ui(self):
with DivLayout(self.server, self.name, classes="h-100") as self.ui:
with html.Div(
style="position:absolute;top:0;left:0;width:100%;height:100%;"
):
self.view = vtk_widgets.VtkRemoteView(
self.render_window,
interactive_ratio=1,
)


register_analysis(NAME, Viz3D)
42 changes: 29 additions & 13 deletions src/e3sm_siteview/app.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
from pathlib import Path
from types import SimpleNamespace

from trame.app import TrameApp
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import dockview
from trame.widgets import vuetify3 as v3

from e3sm_siteview import components as sv_ui
from e3sm_siteview import pages
from e3sm_siteview.analysis import load_all_analysis
from e3sm_siteview.cli import configure_and_parse
from e3sm_siteview.viewer import E3SMAnalyser


class E3smSiteView(TrameApp):
def __init__(self, server=None):
super().__init__(server, client_type="vue3")

# Tab title
self.state.trame__title = "E3SM Site View"

# --hot-reload arg optional logic
if self.server.hot_reload:
self.server.controller.on_server_reload.add(self._build_ui)

self._build_ui()

def _build_ui(self, *_args, **_kwargs):
self.state.trame__title = "E3SM Site View"
with VAppLayout(self.server, fill_height=True) as self.ui:
with v3.VLayout():
sv_ui.View3DTools()
with v3.VMain():
dockview.DockView(ctx_name="views_container")
args = configure_and_parse(self.server.cli)
load_all_analysis()
self.ctx.viewers = {}

# debug
connectivity_file = Path(args.cf).resolve()
for data_file in args.df:
viewer = E3SMAnalyser(
self.server,
connectivity_file,
Path(data_file).resolve(),
)
self.ctx.viewers[viewer.name] = viewer

self.ctx.pages = SimpleNamespace(
fields=pages.FieldSelectionPage(self.server),
viz=pages.VisualizationPage(self.server),
site=pages.SiteSelectionPage(self.server),
)


def main(server=None, **kwargs):
Expand Down
20 changes: 20 additions & 0 deletions src/e3sm_siteview/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
def configure_and_parse(parser):
parser.add_argument(
"-cf",
help="the nc file with connnectivity information",
)
parser.add_argument(
"-df",
nargs="+",
help="the nc file with data/variables",
)
parser.add_argument(
"--perf",
dest="perf",
action="store_true",
help="Emit performance timing on stderr ([PERF] lines). Used to "
"diagnose where slider-tick cost is going — reader I/O, pipeline, "
"rendering, web layer, etc.",
)

return parser.parse_known_args()[0]
115 changes: 115 additions & 0 deletions src/e3sm_siteview/components/field_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from trame.widgets import html
from trame.widgets import vuetify3 as v3

from e3sm_siteview import constants, module


class FieldSelection(html.Div):
def __init__(self, next_fn, **_):
super().__init__(classes="step-pane ")
self.server.enable_module(module)

self.state.field_surface = [
{**f, "selected": False} for f in constants.FIELDS_METADATA.values()
]
self.state.field_midpoints = [
{**f, "selected": False} for f in constants.FIELDS_METADATA.values()
]

with self:
with html.Div(classes="px-6 pt-6 d-flex align-center ga-2"):
v3.VTextField(
v_model=("fields_filter", ""),
placeholder="Search fields (name or description)",
prepend_inner_icon="mdi-magnify",
variant="outlined",
density="comfortable",
clearable=True,
hide_details=True,
)
v3.VBtn(
"Load ({{ 5 }} + {{ 6 }})",
classes="text-none",
variant="flat",
color="primary",
size="large",
append_icon="mdi-chevron-right",
height=48,
click=next_fn,
)
with html.Div(classes="flex-fill px-6 py-3"):
with v3.VExpansionPanels(
v_model=("fields_open_panels", [0, 1]),
multiple=True,
variant="accordion",
):
for group in constants.FIELD_GROUPS:
with v3.VExpansionPanel():
with v3.VExpansionPanelTitle(height="48px", static=True):
v3.VIcon(
icon=group.icon, classes="mr-3", color=group.color
)
html.Span(group.label, classes="font-weight-medium")
v3.VChip(
"1 / 10", # FIXME
size="x-small",
variant="outlined",
color=group.color,
classes="ml-4",
)
v3.VSpacer()
v3.VBtn(
"Select all",
variant="text",
v_on_click_stop_prevent=(
self.updateSelection,
f"['{group.key}', true]",
),
classes="mr-2 text-none",
density="compact",
)
v3.VBtn(
"Clear",
variant="text",
v_on_click_stop_prevent=(
self.updateSelection,
f"['{group.key}', false]",
),
classes="mr-2 text-none",
density="compact",
)

with v3.VExpansionPanelText(classes="border-t"):
with v3.VRow(dense=True):
with v3.VCol(
v_for=f"field in {group.state_name}",
key="field.name",
cols=12,
sm=6,
md=4,
xl=3,
v_show="utils.e3sm.match(field, fields_filter)",
):
with v3.VCheckbox(
v_model="field.selected",
density="compact",
hide_details=True,
):
with v3.Template(v_slot_label=True):
with html.Div():
with html.Div(
"{{ field.name }}",
classes="font-weight-medium",
):
html.Span(
"({{ field.units }})",
classes="text-caption text-medium-emphasis",
)
html.Div(
"{{ field.description }}",
classes="text-caption text-medium-emphasis",
style="line-height:1.2;",
)

def updateSelection(self, group_key, select):
print("toggle selection for", group_key, select)
Loading