Skip to content

Commit 7174769

Browse files
committed
feat: add toggle between two-sim and multi-sim modes
1 parent eb9453e commit 7174769

6 files changed

Lines changed: 472 additions & 66 deletions

File tree

src/e3sm_compareview/app.py

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
build_simulation_configs,
1818
comparison_signature_for,
1919
label_signature_for,
20+
normalize_comparison_mode,
21+
normalize_two_sim_target,
2022
)
2123
from e3sm_compareview.components import doc, drawers, file_browser, toolbars
2224
from e3sm_compareview.pipeline import EAMVisSource
@@ -65,7 +67,10 @@ def __init__(self, server=None):
6567
# Simulation comparison selection
6668
"simulation_configs": [],
6769
"control_simulation_file": "",
68-
"comparison_mode": "diff",
70+
"two_sim_test_simulation_file": "",
71+
"comparison_mode": "multi-sim",
72+
"comparison_type": "diff",
73+
"selected_columns": ["ctrl", "test", "diff", "comp1", "comp2"],
6974
"dragged_simulation_path": "",
7075
}
7176
)
@@ -257,9 +262,24 @@ def selected_variable_names(self):
257262
@property
258263
def active_simulation_configs(self):
259264
return active_simulation_configs(
260-
self.state.simulation_configs, self.state.control_simulation_file
265+
self.state.simulation_configs,
266+
self.state.control_simulation_file,
267+
self.state.comparison_mode,
268+
self.state.two_sim_test_simulation_file,
261269
)
262270

271+
def _ensure_two_sim_target(self):
272+
if self.state.comparison_mode != "two-sim":
273+
return
274+
275+
target_path = normalize_two_sim_target(
276+
self.state.simulation_configs,
277+
self.state.control_simulation_file,
278+
self.state.two_sim_test_simulation_file,
279+
)
280+
if target_path != self.state.two_sim_test_simulation_file:
281+
self.state.two_sim_test_simulation_file = target_path
282+
263283
def _selected_variables_to_show(self):
264284
vars_to_show = self.selected_variables
265285
return vars_to_show if any(vars_to_show.values()) else None
@@ -338,7 +358,10 @@ def download_state(self):
338358
}
339359
state_content["comparisons"] = {
340360
"control": self.state.control_simulation_file,
361+
"target": self.state.two_sim_test_simulation_file,
341362
"mode": self.state.comparison_mode,
363+
"type": self.state.comparison_type,
364+
"columns": self.state.selected_columns,
342365
"simulations": self.state.simulation_configs,
343366
}
344367
state_content["variables-selection"] = self.state.variables_selected
@@ -364,7 +387,10 @@ def download_state(self):
364387
for view_type, var_names in active_variables.items():
365388
for var_name in var_names:
366389
for view_spec in self.source.get_view_specs(
367-
var_name, self.state.comparison_mode
390+
var_name,
391+
self.state.comparison_mode,
392+
self.state.comparison_type,
393+
self.state.selected_columns,
368394
):
369395
config = self.view_manager.get_view(view_spec, view_type).config
370396
views_to_export.append(
@@ -430,7 +456,28 @@ async def _import_state(self, state_content):
430456
self.state.control_simulation_file = comparisons.get(
431457
"control", self.state.control_simulation_file
432458
)
433-
self.state.comparison_mode = comparisons.get("mode", "diff")
459+
self.state.two_sim_test_simulation_file = comparisons.get(
460+
"target", self.state.two_sim_test_simulation_file
461+
)
462+
raw_mode = comparisons.get("mode")
463+
if raw_mode in ("two-sim", "multi-sim"):
464+
self.state.comparison_mode = raw_mode
465+
else:
466+
self.state.comparison_mode = comparisons.get(
467+
"strategy", self.state.comparison_mode
468+
)
469+
470+
raw_type = comparisons.get("type")
471+
if raw_type in ("diff", "comp1", "comp2"):
472+
self.state.comparison_type = raw_type
473+
else:
474+
legacy_type = comparisons.get("mode")
475+
if legacy_type in ("diff", "comp1", "comp2"):
476+
self.state.comparison_type = legacy_type
477+
self.state.selected_columns = comparisons.get(
478+
"columns", self.state.selected_columns
479+
)
480+
self._ensure_two_sim_target()
434481

435482
# Load variables
436483
self.state.variables_selected = state_content["variables-selection"]
@@ -477,6 +524,7 @@ async def data_loading_open(self, simulation_files, connectivity):
477524
)
478525
self.state.simulation_configs = simulation_configs
479526
self.state.control_simulation_file = control_file
527+
self._ensure_two_sim_target()
480528

481529
await asyncio.sleep(0.1)
482530
# Use the selected simulations from the UI state.
@@ -584,21 +632,52 @@ async def _data_load_variables(self):
584632
def _on_layout_change(self, **_):
585633
self._rebuild_active_layout()
586634

635+
@change("comparison_type")
636+
def _on_comparison_type_change(self, **_):
637+
if (
638+
self.state.comparison_mode != "multi-sim"
639+
or not self.state.variables_loaded
640+
):
641+
return
642+
643+
self._rebuild_active_layout(update_color=True)
644+
587645
@change("comparison_mode")
588-
def _on_comparison_mode_change(self, **_):
589-
if not self.state.variables_loaded:
646+
def _on_comparison_mode_change(self, comparison_mode, **_):
647+
normalized = normalize_comparison_mode(comparison_mode)
648+
if normalized != comparison_mode:
649+
self.state.comparison_mode = normalized
590650
return
591651

652+
self.state.variables_selected = []
653+
self.state.variables_loaded = False
654+
655+
@change("selected_columns")
656+
def _on_selected_columns_change(self, **_):
657+
if (
658+
self.state.comparison_mode != "two-sim"
659+
or not self.state.variables_loaded
660+
):
661+
return
592662
self._rebuild_active_layout(update_color=True)
593663

594-
@change("simulation_configs", "control_simulation_file")
664+
@change(
665+
"simulation_configs",
666+
"control_simulation_file",
667+
"comparison_mode",
668+
"two_sim_test_simulation_file",
669+
)
595670
def _on_simulation_selection_change(self, simulation_configs, **_):
596671
if simulation_configs:
597672
valid_paths = {entry["path"] for entry in simulation_configs}
598673
if self.state.control_simulation_file not in valid_paths:
599674
self.state.control_simulation_file = simulation_configs[0]["path"]
675+
self._ensure_two_sim_target()
600676
comparison_signature = comparison_signature_for(
601-
simulation_configs, self.state.control_simulation_file
677+
simulation_configs,
678+
self.state.control_simulation_file,
679+
self.state.comparison_mode,
680+
self.state.two_sim_test_simulation_file,
602681
)
603682
label_signature = label_signature_for(simulation_configs)
604683

@@ -610,6 +689,7 @@ def _on_simulation_selection_change(self, simulation_configs, **_):
610689

611690
if comparison_changed:
612691
self._refresh_source_simulations()
692+
self.view_manager.reset_view_orders(self.selected_variables)
613693
if self.state.variables_loaded and self._rebuild_active_layout(update_color=True):
614694
return
615695
self.state.variables_loaded = False

src/e3sm_compareview/comparison.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import re
22
from pathlib import Path
33

4-
COMPARISON_MODES = ("diff", "comp1", "comp2")
4+
COMPARISON_TYPES = ("diff", "comp1", "comp2")
5+
COMPARISON_MODES = ("two-sim", "multi-sim")
6+
7+
8+
def normalize_comparison_mode(mode):
9+
if mode in COMPARISON_MODES:
10+
return mode
11+
return "multi-sim"
512

613

714
def default_simulation_label(file_path):
@@ -71,9 +78,33 @@ def build_simulation_configs(simulation_files, existing_configs, control_file):
7178
return configs, control_file
7279

7380

74-
def comparison_signature_for(configs, control_file):
81+
def normalize_two_sim_target(configs, control_file, two_sim_target_file):
82+
configs = configs or []
83+
if not configs:
84+
return ""
85+
86+
valid_paths = [entry.get("path") for entry in configs if entry.get("path")]
87+
if not valid_paths:
88+
return ""
89+
90+
if two_sim_target_file in valid_paths and two_sim_target_file != control_file:
91+
return two_sim_target_file
92+
93+
for path in valid_paths:
94+
if path != control_file:
95+
return path
96+
97+
return control_file
98+
99+
100+
def comparison_signature_for(
101+
configs, control_file, comparison_mode="multi-sim", two_sim_target_file=""
102+
):
103+
comparison_mode = normalize_comparison_mode(comparison_mode)
75104
return (
105+
comparison_mode,
76106
control_file,
107+
normalize_two_sim_target(configs, control_file, two_sim_target_file),
77108
tuple(
78109
(entry.get("path"), bool(entry.get("include", True)))
79110
for entry in (configs or [])
@@ -87,7 +118,10 @@ def label_signature_for(configs):
87118
)
88119

89120

90-
def active_simulation_configs(configs, control_file):
121+
def active_simulation_configs(
122+
configs, control_file, comparison_mode="multi-sim", two_sim_target_file=""
123+
):
124+
comparison_mode = normalize_comparison_mode(comparison_mode)
91125
configs = configs or []
92126
if not configs:
93127
return []
@@ -96,11 +130,35 @@ def active_simulation_configs(configs, control_file):
96130
control_file = control_file or configs[0]["path"]
97131
control_config = configs_by_path.get(control_file, configs[0])
98132
control_index = next(
99-
(index for index, entry in enumerate(configs) if entry["path"] == control_config["path"]),
133+
(
134+
index
135+
for index, entry in enumerate(configs)
136+
if entry["path"] == control_config["path"]
137+
),
100138
0,
101139
)
102140

103141
active = [{**control_config, "role": "control", "source_index": control_index}]
142+
143+
if comparison_mode == "two-sim":
144+
target_path = normalize_two_sim_target(
145+
configs, control_config["path"], two_sim_target_file
146+
)
147+
target_config = configs_by_path.get(target_path)
148+
if target_config and target_config["path"] != control_config["path"]:
149+
target_index = next(
150+
(
151+
index
152+
for index, entry in enumerate(configs)
153+
if entry["path"] == target_config["path"]
154+
),
155+
0,
156+
)
157+
active.append(
158+
{**target_config, "role": "comparison", "source_index": target_index}
159+
)
160+
return active
161+
104162
active.extend(
105163
{**entry, "role": "comparison", "source_index": index}
106164
for index, entry in enumerate(configs)

src/e3sm_compareview/components/simulation_selection.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ def __init__(self):
7070
"{{ simulation_configs.length }} loaded",
7171
classes="text-caption mr-4",
7272
)
73+
with html.Div(classes="px-3 py-2 border-b-thin"):
74+
with v3.VBtnToggle(
75+
v_model=("comparison_mode", "multi-sim"),
76+
mandatory=True,
77+
density="compact",
78+
divided=True,
79+
):
80+
v3.VBtn("Two Sim", value="two-sim", classes="text-none")
81+
v3.VBtn("Multi Sim", value="multi-sim", classes="text-none")
7382

7483
with html.Div(v_if="simulation_configs.length === 0", classes="pa-4"):
7584
html.Div(
@@ -113,7 +122,7 @@ def __init__(self):
113122
update_modelValue="""
114123
simulation_configs = simulation_configs.map((sim) =>
115124
sim.path === entry.path ? ({ ...sim, label: $event }) : sim
116-
)
125+
);
117126
""",
118127
label="Label",
119128
density="compact",
@@ -132,30 +141,68 @@ def __init__(self):
132141
"control_simulation_file === entry.path ? 'primary' : 'default'",
133142
),
134143
classes="text-none w-100",
135-
style="min-width: 100px;",
144+
style="min-width: 112px;",
136145
size="small",
137-
click="control_simulation_file = entry.path",
146+
click=(
147+
self._on_control_selected,
148+
"[entry.path]",
149+
),
138150
)
139151
with v3.VCol(cols=6, md=3):
140-
v3.VCheckbox(
141-
model_value=(
142-
"control_simulation_file === entry.path ? true : entry.include",
143-
),
144-
update_modelValue="""
152+
with v3.Template(v_if="comparison_mode === 'multi-sim'"):
153+
v3.VCheckbox(
154+
model_value=(
155+
"control_simulation_file === entry.path ? true : entry.include",
156+
),
157+
update_modelValue="""
145158
simulation_configs = simulation_configs.map((sim) =>
146159
sim.path === entry.path ? ({ ...sim, include: !!$event }) : sim
147-
)
160+
);
148161
""",
149-
label="Include",
150-
density="compact",
151-
hide_details=True,
152-
disabled=(
153-
"control_simulation_file === entry.path",
154-
),
155-
)
162+
label="Include",
163+
density="compact",
164+
hide_details=True,
165+
disabled=(
166+
"control_simulation_file === entry.path",
167+
),
168+
)
169+
with v3.Template(v_else=True):
170+
v3.VBtn(
171+
text=(
172+
"two_sim_test_simulation_file === entry.path ? 'Test' : 'Set test'",
173+
),
174+
variant=(
175+
"two_sim_test_simulation_file === entry.path ? 'flat' : 'outlined'",
176+
),
177+
color=(
178+
"two_sim_test_simulation_file === entry.path ? 'primary' : 'default'",
179+
),
180+
classes="text-none w-100",
181+
style="min-width: 112px;",
182+
size="small",
183+
disabled=(
184+
"control_simulation_file === entry.path",
185+
),
186+
click="two_sim_test_simulation_file = entry.path",
187+
)
156188
html.Div(
157189
"{{ entry.path }}",
158190
classes="text-caption text-medium-emphasis mt-2",
159191
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left;",
160192
title=("entry.path",),
161193
)
194+
195+
def _on_control_selected(self, control_path, **_):
196+
self.state.control_simulation_file = control_path
197+
if self.state.comparison_mode != "two-sim":
198+
return
199+
if self.state.two_sim_test_simulation_file != control_path:
200+
return
201+
202+
fallback = control_path
203+
for sim in self.state.simulation_configs or []:
204+
sim_path = sim.get("path")
205+
if sim_path and sim_path != control_path:
206+
fallback = sim_path
207+
break
208+
self.state.two_sim_test_simulation_file = fallback

0 commit comments

Comments
 (0)