Skip to content

Commit cdff823

Browse files
feat: Interactive settings UI. (#4374)
The interactive **Settings UI** provides an intuitive way to explore and modify settings objects exposed by PyFluent. It lets you navigate through the hierarchy, expand sections, and directly interact with individual parameters or commands. --------- Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent f041514 commit cdff823

File tree

8 files changed

+811
-0
lines changed

8 files changed

+811
-0
lines changed

doc/changelog.d/4374.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Interactive settings UI.

doc/source/user_guide/ui.rst

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
.. _ref_ui:
2+
3+
Interactive Settings UI
4+
=======================
5+
6+
The interactive **Settings UI** provides an intuitive way to explore and modify
7+
settings objects exposed by PyFluent. It lets you navigate through the hierarchy,
8+
expand sections, and directly interact with individual parameters or commands.
9+
10+
As you expand sections, the UI displays the **path** to each settings object.
11+
These paths can be reused in automation scripts with PyFluent. You can launch the UI
12+
with any settings object, from the root ``solver.settings`` to a specific branch.
13+
14+
The UI supports two modes:
15+
16+
* **Web mode** when running from a standalone Python script.
17+
* **Inline mode** inside a Jupyter Notebook.
18+
19+
Launching the UI
20+
----------------
21+
22+
Import the ``ui`` function and pass in a settings object:
23+
24+
.. code-block:: python
25+
26+
import ansys.fluent.core as pyfluent
27+
from ansys.fluent.core.ui import ui
28+
29+
solver = pyfluent.launch_fluent()
30+
31+
# Launch the UI for the full settings tree
32+
ui(solver.settings)
33+
34+
You can also launch the UI for a specific subset of settings:
35+
36+
.. code-block:: python
37+
38+
# Case file read operations
39+
ui(solver.settings.file.read_case)
40+
41+
# Physical models configuration
42+
ui(solver.settings.setup.models)
43+
44+
Features
45+
--------
46+
47+
* **Expandable sections**: Browse the hierarchy of settings
48+
objects by expanding and collapsing sections.
49+
* **Path display**: Each expanded section shows its fully qualified path
50+
for easy reuse in scripts.
51+
* **Interactive fields**: Parameters can be edited directly from the UI.
52+
* **Flexible scope**: Start at the root (``solver.settings``) or
53+
any branch of the settings tree.
54+
* **Multiple environments**:
55+
- Web mode for standalone Python scripts.
56+
- Inline mode inside Jupyter Notebooks.
57+
58+
Use Cases
59+
---------
60+
61+
The interactive Settings UI is particularly useful for:
62+
63+
* Exploring solver settings without memorizing full paths.
64+
* Rapidly editing parameters during solver setup.
65+
* Teaching and demonstrations, where hierarchical navigation and paths
66+
aid understanding.
67+
* Embedding interactive solver configuration directly in notebooks
68+
for reproducible workflows.
69+
70+
``ui`` bridges programmatic control with interactive exploration,
71+
letting you configure Fluent in the way that best fits your workflow.

doc/source/user_guide/user_guide_contents.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ User guide
2727
make_container_image
2828
legacy/legacy_contents
2929
beta_feature_access
30+
ui
3031

3132

3233
Welcome to the PyFluent user guide. This guide helps you understand how to use PyFluent to

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ include = ["src/ansys/fluent/core/generated/"]
4141

4242
[project.optional-dependencies]
4343
reader = ["h5py==3.14.0"]
44+
ui-jupyter = ["ipywidgets"]
45+
ui = ["panel"]
4446
tests = [
4547
"pytest==8.4.1",
4648
"pytest-cov==6.2.1",

src/ansys/fluent/core/ui/__init__.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Public exposure of ui UI for PyFluent."""
24+
25+
26+
def in_jupyter():
27+
"""Checks if the library is being used in a Jupyter environment."""
28+
try:
29+
from IPython import get_ipython
30+
31+
return "IPKernelApp" in get_ipython().config
32+
except (ImportError, AttributeError):
33+
return False
34+
35+
36+
if in_jupyter():
37+
from ansys.fluent.core.ui.jupyter_ui import (
38+
set_auto_refresh,
39+
settings_ui,
40+
)
41+
else:
42+
from ansys.fluent.core.ui.standalone_web_ui import ( # noqa: F401
43+
build_settings_view,
44+
set_auto_refresh,
45+
)
46+
47+
48+
def ui(settings_obj):
49+
"""PyFluent ui UI wrapper."""
50+
if in_jupyter():
51+
import IPython
52+
from IPython.display import display
53+
54+
if hasattr(IPython, "get_ipython") and "ZMQInteractiveShell" in str(
55+
type(IPython.get_ipython())
56+
):
57+
display(settings_ui(settings_obj))
58+
else:
59+
import panel as pn
60+
61+
pn.extension()
62+
view = build_settings_view(settings_obj)
63+
view.servable()
64+
pn.serve(view)
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Render ui UI in Jupyter notebook."""
24+
25+
from ansys.fluent.core.ui.utils import (
26+
_parse_path,
27+
_render_widget_from_props_generic,
28+
_safe_get_properties,
29+
)
30+
31+
try:
32+
import ipywidgets as widgets
33+
except ModuleNotFoundError as exc:
34+
raise ModuleNotFoundError(
35+
"Missing dependencies, use 'pip install ansys-fluent-core[ui-jupyter]' to install them."
36+
) from exc
37+
38+
from ansys.fluent.core.solver.flobject import (
39+
BaseCommand,
40+
Group,
41+
NamedObject,
42+
)
43+
44+
45+
def set_auto_refresh():
46+
"""Refreshes the UI w.r.t. server state for each command execution or parameter invocation."""
47+
raise NotImplementedError("This is yet to be implemented in jupyter environment.")
48+
49+
50+
def _render_widgets_from_props(settings_obj, label, props):
51+
"""Render widget using pre-fetched props instead of repeated calls."""
52+
return _render_widget_from_props_generic(settings_obj, label, props, widgets)
53+
54+
55+
def _param_ui(settings_obj, props):
56+
label = props["python_name"].replace("_", " ").capitalize()
57+
58+
def get_fn():
59+
try:
60+
return getattr(props["parent"], props["python_name"])
61+
except AttributeError:
62+
return props["parent"][props["obj_name"]]
63+
64+
def set_fn(v):
65+
return setattr(settings_obj.parent, props["python_name"], v)
66+
67+
widget = _render_widgets_from_props(get_fn(), label, props)
68+
output = widgets.Output()
69+
with output:
70+
output.clear_output()
71+
print(_parse_path(settings_obj))
72+
if hasattr(widget, "_is_list_text"):
73+
typ, parse_csv = widget._is_list_text
74+
75+
def commit_text_list(change):
76+
if change["name"] == "value":
77+
raw = change["new"]
78+
vals = (
79+
[typ(v.strip()) for v in raw.split(",") if v.strip()]
80+
if parse_csv
81+
else list(change["new"])
82+
)
83+
with output:
84+
output.clear_output()
85+
set_fn(vals)
86+
print(f"{_parse_path(settings_obj)} = {vals}")
87+
88+
widget.observe(commit_text_list)
89+
else:
90+
91+
def on_change(change):
92+
if change["name"] == "value":
93+
with output:
94+
output.clear_output()
95+
try:
96+
set_fn(change["new"])
97+
print(f"{_parse_path(settings_obj)} = {change['new']}")
98+
except Exception as e:
99+
print(f"Error setting {label}: {e}")
100+
101+
widget.observe(on_change)
102+
return widgets.VBox([widget, output])
103+
104+
105+
def _command_ui(func, props):
106+
"""
107+
Renders input widgets for function arguments based on .argument_names()
108+
and executes func(**kwargs) on button click.
109+
"""
110+
111+
# Get argument names from the function object
112+
if not hasattr(func, "argument_names"):
113+
return widgets.HTML("Command has no 'argument_names()'.")
114+
115+
arg_names = func.argument_names
116+
arg_widgets = {}
117+
controls = []
118+
for name in arg_names:
119+
child_obj = getattr(func, name)
120+
child_props = _safe_get_properties(child_obj)
121+
widget = _render_widgets_from_props(child_obj, name, child_props)
122+
arg_widgets[name] = widget
123+
controls.append(widget)
124+
125+
# Run button
126+
button = widgets.Button(
127+
description=f"Run {props['python_name']}", button_style="success"
128+
)
129+
output = widgets.Output()
130+
with output:
131+
output.clear_output()
132+
print(_parse_path(func))
133+
134+
def on_click(_):
135+
kwargs = {name: w.value for name, w in arg_widgets.items()}
136+
with output:
137+
output.clear_output()
138+
try:
139+
func(**kwargs)
140+
kwargs_str = "("
141+
for k, v in kwargs.items():
142+
if type(v) is str:
143+
if v != "":
144+
kwargs_str += f"{k}='{v}', "
145+
else:
146+
kwargs_str += f"{k}={v}, "
147+
print(f"{_parse_path(func)}" + kwargs_str.strip()[:-1] + ")")
148+
except Exception as e:
149+
print("Error:", e)
150+
151+
button.on_click(on_click)
152+
return widgets.VBox(controls + [button, output])
153+
154+
155+
def settings_ui(obj, indent=0):
156+
"""Render settings objects into ui graphics."""
157+
props = _safe_get_properties(obj)
158+
if isinstance(obj, (Group, NamedObject)):
159+
if isinstance(obj, Group):
160+
command_names = obj.get_active_command_names()
161+
child_names = obj.get_active_child_names() + command_names
162+
else:
163+
command_names = obj.command_names
164+
child_names = list(obj) + command_names
165+
accordions = []
166+
for child_name in child_names:
167+
168+
def lazy_loader(name=child_name, parent=obj, lvl=indent + 1):
169+
try:
170+
child_obj = getattr(parent, name)
171+
except AttributeError:
172+
child_obj = parent[name]
173+
return settings_ui(child_obj, lvl)
174+
175+
acc = widgets.Accordion(children=[widgets.HTML("Loading...")])
176+
if child_name in command_names:
177+
acc.set_title(0, f"⚡ {child_name}")
178+
else:
179+
acc.set_title(0, child_name)
180+
181+
def on_selected(change, loader=lazy_loader, accordion=acc):
182+
if change["name"] == "selected_index" and change["new"] == 0:
183+
if isinstance(accordion.children[0], widgets.HTML):
184+
accordion.children = [loader()]
185+
186+
acc.observe(on_selected, names="selected_index")
187+
accordions.append(acc)
188+
189+
return widgets.VBox(accordions)
190+
191+
else:
192+
if isinstance(obj, BaseCommand):
193+
return (
194+
widgets.VBox([_command_ui(obj, props)])
195+
if props["is_active"]
196+
else widgets.HTML("")
197+
)
198+
else:
199+
return (
200+
widgets.VBox([_param_ui(obj, props)])
201+
if props["is_active"]
202+
else widgets.HTML("")
203+
)

0 commit comments

Comments
 (0)