From a432685e1fa786e8aa8f5756752f8865dfe6ce61 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Wed, 20 May 2026 15:26:51 -0400 Subject: [PATCH 1/2] exports are more granular --- src/festim_gui/pages/exports_page.py | 411 +++++++++++++++++++++------ 1 file changed, 318 insertions(+), 93 deletions(-) diff --git a/src/festim_gui/pages/exports_page.py b/src/festim_gui/pages/exports_page.py index 77ea144..509e20d 100644 --- a/src/festim_gui/pages/exports_page.py +++ b/src/festim_gui/pages/exports_page.py @@ -2,28 +2,92 @@ from trame.ui.html import DivLayout from trame.widgets import vuetify3 as v3 +from festim_gui.components import RepeatedItemControls from festim_gui.pages.page import Page -from festim_gui.utils import as_bool +from festim_gui.utils import build_initial_rows, resolve_template_row DEFAULTS = { "field_exports_var": "concentration_field_exports", "derived_exports_var": "derived_quantities", - "enable_vtx_species_exports": True, "vtx_filename_template": "out/vol_{subdomain.id}.bp", - "vtx_field_expr": "problem.species", - "enable_surface_flux_exports": True, - "surface_flux_field_var": "H", } +VTX_EXPORT_DEFAULTS = { + "var": "vtx_export_{i}", + "field_expr": "problem.species", + "subdomain_var": "volume_1", +} +VTX_EXPORTS = [ + { + "var": "vtx_export_1", + "field_expr": "problem.species", + "subdomain_var": "volume_1", + } +] + +SURFACE_QUANTITY_TYPES = [ + "SurfaceFlux", + "TotalSurface", + "AverageSurface", + "MinimumSurface", + "MaximumSurface", +] +SURFACE_QUANTITY_DEFAULTS = { + "var": "surface_quantity_{i}", + "quantity_class": "SurfaceFlux", + "field_expr": "H", + "surface_var": "surface_1", +} +SURFACE_QUANTITIES = [ + { + "var": "surface_quantity_1", + "quantity_class": "SurfaceFlux", + "field_expr": "H", + "surface_var": "surface_1", + } +] + +VOLUME_QUANTITY_TYPES = [ + "TotalVolume", + "AverageVolume", + "MinimumVolume", + "MaximumVolume", +] +VOLUME_QUANTITY_DEFAULTS = { + "var": "volume_quantity_{i}", + "quantity_class": "TotalVolume", + "field_expr": "H", + "volume_var": "volume_1", +} +VOLUME_QUANTITIES = [ + { + "var": "volume_quantity_1", + "quantity_class": "TotalVolume", + "field_expr": "H", + "volume_var": "volume_1", + } +] + class ExportsPageState(StateDataModel): field_exports_var = Sync(str, DEFAULTS["field_exports_var"]) derived_exports_var = Sync(str, DEFAULTS["derived_exports_var"]) - enable_vtx_species_exports = Sync(bool, DEFAULTS["enable_vtx_species_exports"]) vtx_filename_template = Sync(str, DEFAULTS["vtx_filename_template"]) - vtx_field_expr = Sync(str, DEFAULTS["vtx_field_expr"]) - enable_surface_flux_exports = Sync(bool, DEFAULTS["enable_surface_flux_exports"]) - surface_flux_field_var = Sync(str, DEFAULTS["surface_flux_field_var"]) + vtx_export_rows = Sync( + list, + lambda: build_initial_rows(VTX_EXPORT_DEFAULTS, VTX_EXPORTS), + client_deep_reactive=True, + ) + surface_quantity_rows = Sync( + list, + lambda: build_initial_rows(SURFACE_QUANTITY_DEFAULTS, SURFACE_QUANTITIES), + client_deep_reactive=True, + ) + volume_quantity_rows = Sync( + list, + lambda: build_initial_rows(VOLUME_QUANTITY_DEFAULTS, VOLUME_QUANTITIES), + client_deep_reactive=True, + ) class ExportsPage(Page): @@ -38,49 +102,74 @@ def __init__(self, server): [ "field_exports_var", "derived_exports_var", - "enable_vtx_species_exports", "vtx_filename_template", - "vtx_field_expr", - "enable_surface_flux_exports", - "surface_flux_field_var", + "vtx_export_rows", + "surface_quantity_rows", + "volume_quantity_rows", ], self.notify_script_change, sync=True, ) self.build_ui() + def add_vtx_export(self, *_args, **_kwargs): + rows = list(self.config.vtx_export_rows) + rows.append(resolve_template_row(VTX_EXPORT_DEFAULTS, len(rows))) + self.config.vtx_export_rows = rows + + def remove_vtx_export(self, *_args, **_kwargs): + rows = list(self.config.vtx_export_rows) + if not rows: + return + rows.pop() + self.config.vtx_export_rows = rows + + def add_surface_quantity(self, *_args, **_kwargs): + rows = list(self.config.surface_quantity_rows) + rows.append(resolve_template_row(SURFACE_QUANTITY_DEFAULTS, len(rows))) + self.config.surface_quantity_rows = rows + + def remove_surface_quantity(self, *_args, **_kwargs): + rows = list(self.config.surface_quantity_rows) + if not rows: + return + rows.pop() + self.config.surface_quantity_rows = rows + + def add_volume_quantity(self, *_args, **_kwargs): + rows = list(self.config.volume_quantity_rows) + rows.append(resolve_template_row(VOLUME_QUANTITY_DEFAULTS, len(rows))) + self.config.volume_quantity_rows = rows + + def remove_volume_quantity(self, *_args, **_kwargs): + rows = list(self.config.volume_quantity_rows) + if not rows: + return + rows.pop() + self.config.volume_quantity_rows = rows + def build_ui(self) -> None: with DivLayout(self.server, template_name=self.id): with self.config.provide_as("exports_config"): with v3.VCard(variant="outlined"): with v3.VCardText(classes="d-flex flex-column ga-3"): - v3.VSwitch( - v_model="exports_config.enable_vtx_species_exports", - label="Enable VTX species exports", - color="primary", - hide_details=True, + v3.VTextField( + v_model="exports_config.field_exports_var", + label="Field export list variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + v3.VTextField( + v_model="exports_config.derived_exports_var", + label="Derived export list variable", + variant="outlined", + density="compact", update_modelValue=self.notify_script_change, ) - with v3.VRow(classes="ga-0"): - with v3.VCol(cols="6"): - v3.VTextField( - v_model="exports_config.field_exports_var", - label="Field export list variable", - variant="outlined", - density="compact", - update_modelValue=self.notify_script_change, - ) - with v3.VCol(cols="6"): - v3.VTextField( - v_model="exports_config.vtx_field_expr", - label="field expression", - variant="outlined", - density="compact", - update_modelValue=self.notify_script_change, - ) v3.VTextField( v_model="exports_config.vtx_filename_template", - label="VTX filename template (f-string body)", + label="VTX filename template", variant="outlined", density="compact", update_modelValue=self.notify_script_change, @@ -88,31 +177,153 @@ def build_ui(self) -> None: v3.VDivider(classes="my-1") - v3.VSwitch( - v_model="exports_config.enable_surface_flux_exports", - label="Enable surface flux exports", - color="primary", - hide_details=True, - update_modelValue=self.notify_script_change, + v3.VLabel("VTX Species Exports", classes="text-caption") + RepeatedItemControls( + on_add=self.add_vtx_export, + on_remove=self.remove_vtx_export, ) - with v3.VRow(classes="ga-0"): - with v3.VCol(cols="6"): - v3.VTextField( - v_model="exports_config.derived_exports_var", - label="Derived export list variable", - variant="outlined", - density="compact", - update_modelValue=self.notify_script_change, + with v3.VCard( + variant="tonal", + v_for="(vtx_row, idx) in exports_config.vtx_export_rows", + key=("idx",), + ): + with v3.VCardText(classes="d-flex flex-column ga-2"): + v3.VLabel( + "VTX export {{ idx + 1 }}", classes="text-caption" ) - with v3.VCol(cols="6"): + with v3.VRow(classes="ga-0"): + with v3.VCol(cols="6"): + v3.VTextField( + v_model="vtx_row.var", + label="Variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VCol(cols="6"): + v3.VTextField( + v_model="vtx_row.subdomain_var", + label="Volume subdomain variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) v3.VTextField( - v_model="exports_config.surface_flux_field_var", - label="Surface flux field variable", + v_model="vtx_row.field_expr", + label="Field expression", variant="outlined", density="compact", update_modelValue=self.notify_script_change, ) + v3.VDivider(classes="my-1") + + v3.VLabel( + "Derived Quantities - Surface", classes="text-caption" + ) + RepeatedItemControls( + on_add=self.add_surface_quantity, + on_remove=self.remove_surface_quantity, + ) + with v3.VCard( + variant="tonal", + v_for="(surface_row, idx) in exports_config.surface_quantity_rows", + key=("idx",), + ): + with v3.VCardText(classes="d-flex flex-column ga-2"): + v3.VLabel( + "Surface quantity {{ idx + 1 }}", + classes="text-caption", + ) + with v3.VRow(classes="ga-0"): + with v3.VCol(cols="6"): + v3.VTextField( + v_model="surface_row.var", + label="Variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VCol(cols="6"): + v3.VSelect( + v_model="surface_row.quantity_class", + items=(SURFACE_QUANTITY_TYPES,), + label="Quantity type", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VRow(classes="ga-0"): + with v3.VCol(cols="6"): + v3.VTextField( + v_model="surface_row.field_expr", + label="Field expression", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VCol(cols="6"): + v3.VTextField( + v_model="surface_row.surface_var", + label="Surface variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + + v3.VDivider(classes="my-1") + + v3.VLabel("Derived Quantities - Volume", classes="text-caption") + RepeatedItemControls( + on_add=self.add_volume_quantity, + on_remove=self.remove_volume_quantity, + ) + with v3.VCard( + variant="tonal", + v_for="(volume_row, idx) in exports_config.volume_quantity_rows", + key=("idx",), + ): + with v3.VCardText(classes="d-flex flex-column ga-2"): + v3.VLabel( + "Volume quantity {{ idx + 1 }}", + classes="text-caption", + ) + with v3.VRow(classes="ga-0"): + with v3.VCol(cols="6"): + v3.VTextField( + v_model="volume_row.var", + label="Variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VCol(cols="6"): + v3.VSelect( + v_model="volume_row.quantity_class", + items=(VOLUME_QUANTITY_TYPES,), + label="Quantity type", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VRow(classes="ga-0"): + with v3.VCol(cols="6"): + v3.VTextField( + v_model="volume_row.field_expr", + label="Field expression", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + with v3.VCol(cols="6"): + v3.VTextField( + v_model="volume_row.volume_var", + label="Volume variable", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) + @property def page_problem(self): return self.ctx.page_problem @@ -129,59 +340,73 @@ def script_lines(self) -> list[str]: self.config.vtx_filename_template.strip() or DEFAULTS["vtx_filename_template"] ) - vtx_field_expr = ( - self.config.vtx_field_expr.strip() or DEFAULTS["vtx_field_expr"] - ) - surface_flux_field_var = ( - self.config.surface_flux_field_var.strip() - or DEFAULTS["surface_flux_field_var"] - ) - - include_vtx = as_bool( - self.config.enable_vtx_species_exports, - DEFAULTS["enable_vtx_species_exports"], - ) - include_surface_flux = as_bool( - self.config.enable_surface_flux_exports, - DEFAULTS["enable_surface_flux_exports"], - ) lines = ["# 9. Exports"] - if include_vtx: + vtx_var_names = [] + for idx, row in enumerate(self.config.vtx_export_rows): + defaults = resolve_template_row(VTX_EXPORT_DEFAULTS, idx) + var_name = str(row.get("var", defaults["var"])) + field_expr = str(row.get("field_expr", defaults["field_expr"])) + subdomain_var = str(row.get("subdomain_var", defaults["subdomain_var"])) + vtx_var_names.append(var_name) lines.extend( [ - f"{field_exports_var} = [", - " F.VTXSpeciesExport(", - f' filename=f"{vtx_filename_template}",', - f" field={vtx_field_expr},", - " subdomain=subdomain,", - " )", - f" for subdomain in {problem_var}.volume_subdomains", - "]", - "", + f"{var_name} = F.VTXSpeciesExport(", + f' filename=f"{vtx_filename_template}",', + f" field={field_expr},", + f" subdomain={subdomain_var},", + ")", ] ) - if include_surface_flux: + if vtx_var_names: + lines.append(f"{field_exports_var} = [{', '.join(vtx_var_names)}]") + else: + lines.append(f"{field_exports_var} = []") + + lines.append("") + + derived_var_names = [] + for idx, row in enumerate(self.config.surface_quantity_rows): + defaults = resolve_template_row(SURFACE_QUANTITY_DEFAULTS, idx) + var_name = str(row.get("var", defaults["var"])) + quantity_class = str(row.get("quantity_class", defaults["quantity_class"])) + field_expr = str(row.get("field_expr", defaults["field_expr"])) + surface_var = str(row.get("surface_var", defaults["surface_var"])) + derived_var_names.append(var_name) lines.extend( [ - f"{derived_exports_var} = [", - f" F.SurfaceFlux(field={surface_flux_field_var}, surface=surf)", - f" for surf in {problem_var}.surface_subdomains", - "]", - "", + f"{var_name} = F.{quantity_class}(", + f" field={field_expr},", + f" surface={surface_var},", + ")", ] ) - if include_vtx and include_surface_flux: - lines.append( - f"{problem_var}.exports = {field_exports_var} + {derived_exports_var}" + for idx, row in enumerate(self.config.volume_quantity_rows): + defaults = resolve_template_row(VOLUME_QUANTITY_DEFAULTS, idx) + var_name = str(row.get("var", defaults["var"])) + quantity_class = str(row.get("quantity_class", defaults["quantity_class"])) + field_expr = str(row.get("field_expr", defaults["field_expr"])) + volume_var = str(row.get("volume_var", defaults["volume_var"])) + derived_var_names.append(var_name) + lines.extend( + [ + f"{var_name} = F.{quantity_class}(", + f" field={field_expr},", + f" volume={volume_var},", + ")", + ] ) - elif include_vtx: - lines.append(f"{problem_var}.exports = {field_exports_var}") - elif include_surface_flux: - lines.append(f"{problem_var}.exports = {derived_exports_var}") + + if derived_var_names: + lines.append(f"{derived_exports_var} = [{', '.join(derived_var_names)}]") else: - lines.append(f"{problem_var}.exports = []") + lines.append(f"{derived_exports_var} = []") + + lines.append("") + lines.append( + f"{problem_var}.exports = {field_exports_var} + {derived_exports_var}" + ) return lines From 74f4e0a16ab7779fa6c846ad656cea433d032417 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Wed, 20 May 2026 15:38:39 -0400 Subject: [PATCH 2/2] one filename per export --- src/festim_gui/pages/exports_page.py | 30 +++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/festim_gui/pages/exports_page.py b/src/festim_gui/pages/exports_page.py index 509e20d..73fa4e6 100644 --- a/src/festim_gui/pages/exports_page.py +++ b/src/festim_gui/pages/exports_page.py @@ -9,17 +9,18 @@ DEFAULTS = { "field_exports_var": "concentration_field_exports", "derived_exports_var": "derived_quantities", - "vtx_filename_template": "out/vol_{subdomain.id}.bp", } VTX_EXPORT_DEFAULTS = { "var": "vtx_export_{i}", + "filename": "out/field_export.bp", "field_expr": "problem.species", "subdomain_var": "volume_1", } VTX_EXPORTS = [ { "var": "vtx_export_1", + "filename": "out/field_export.bp", "field_expr": "problem.species", "subdomain_var": "volume_1", } @@ -72,7 +73,6 @@ class ExportsPageState(StateDataModel): field_exports_var = Sync(str, DEFAULTS["field_exports_var"]) derived_exports_var = Sync(str, DEFAULTS["derived_exports_var"]) - vtx_filename_template = Sync(str, DEFAULTS["vtx_filename_template"]) vtx_export_rows = Sync( list, lambda: build_initial_rows(VTX_EXPORT_DEFAULTS, VTX_EXPORTS), @@ -102,7 +102,6 @@ def __init__(self, server): [ "field_exports_var", "derived_exports_var", - "vtx_filename_template", "vtx_export_rows", "surface_quantity_rows", "volume_quantity_rows", @@ -114,7 +113,9 @@ def __init__(self, server): def add_vtx_export(self, *_args, **_kwargs): rows = list(self.config.vtx_export_rows) - rows.append(resolve_template_row(VTX_EXPORT_DEFAULTS, len(rows))) + row = resolve_template_row(VTX_EXPORT_DEFAULTS, len(rows)) + row["filename"] = f"out/field_export_{len(rows) + 1}.bp" + rows.append(row) self.config.vtx_export_rows = rows def remove_vtx_export(self, *_args, **_kwargs): @@ -167,13 +168,6 @@ def build_ui(self) -> None: density="compact", update_modelValue=self.notify_script_change, ) - v3.VTextField( - v_model="exports_config.vtx_filename_template", - label="VTX filename template", - variant="outlined", - density="compact", - update_modelValue=self.notify_script_change, - ) v3.VDivider(classes="my-1") @@ -208,6 +202,13 @@ def build_ui(self) -> None: density="compact", update_modelValue=self.notify_script_change, ) + v3.VTextField( + v_model="vtx_row.filename", + label="Filename", + variant="outlined", + density="compact", + update_modelValue=self.notify_script_change, + ) v3.VTextField( v_model="vtx_row.field_expr", label="Field expression", @@ -336,23 +337,20 @@ def script_lines(self) -> list[str]: derived_exports_var = ( self.config.derived_exports_var.strip() or DEFAULTS["derived_exports_var"] ) - vtx_filename_template = ( - self.config.vtx_filename_template.strip() - or DEFAULTS["vtx_filename_template"] - ) lines = ["# 9. Exports"] vtx_var_names = [] for idx, row in enumerate(self.config.vtx_export_rows): defaults = resolve_template_row(VTX_EXPORT_DEFAULTS, idx) var_name = str(row.get("var", defaults["var"])) + filename = str(row.get("filename", defaults["filename"])) field_expr = str(row.get("field_expr", defaults["field_expr"])) subdomain_var = str(row.get("subdomain_var", defaults["subdomain_var"])) vtx_var_names.append(var_name) lines.extend( [ f"{var_name} = F.VTXSpeciesExport(", - f' filename=f"{vtx_filename_template}",', + f' filename="{filename}",', f" field={field_expr},", f" subdomain={subdomain_var},", ")",