diff --git a/doc/changelog.d/4265.fixed.md b/doc/changelog.d/4265.fixed.md new file mode 100644 index 00000000000..40d959e7507 --- /dev/null +++ b/doc/changelog.d/4265.fixed.md @@ -0,0 +1,2 @@ +- Fixed `launch_fluent()` ordering so `case_file_name`/`case_data_file_name` are processed before `journal_file_names` when both are provided. +- In lightweight mode, deferred journal replay now completes before the background sync step begins. \ No newline at end of file diff --git a/doc/changelog.d/4990.miscellaneous.md b/doc/changelog.d/4990.miscellaneous.md new file mode 100644 index 00000000000..e61b5c8ce96 --- /dev/null +++ b/doc/changelog.d/4990.miscellaneous.md @@ -0,0 +1 @@ +Couple of issues in launch fluent diff --git a/src/ansys/fluent/core/launcher/launcher_utils.py b/src/ansys/fluent/core/launcher/launcher_utils.py index c4cc1e112d9..602199a8f20 100644 --- a/src/ansys/fluent/core/launcher/launcher_utils.py +++ b/src/ansys/fluent/core/launcher/launcher_utils.py @@ -210,12 +210,16 @@ def _confirm_watchdog_start(start_watchdog, cleanup_on_exit, fluent_connection): def _build_journal_argument( - topy: None | bool | str, journal_file_names: None | str | list[str] + topy: None | bool | str, + journal_file_names: None | str | list[str], + include_journal_file_names: bool = True, ) -> str: """Build Fluent commandline journal argument.""" def _impl( - topy: None | bool | str, journal_file_names: None | str | list[str] + topy: None | bool | str, + journal_file_names: None | str | list[str], + include_journal_file_names: bool, ) -> str: if journal_file_names and not isinstance(journal_file_names, (str, list)): raise TypeError( @@ -228,7 +232,7 @@ def _impl( fluent_jou_arg = "" if isinstance(journal_file_names, str): journal_file_names = [journal_file_names] - if journal_file_names: + if journal_file_names and include_journal_file_names: fluent_jou_arg += "".join( [f' -i "{journal}"' for journal in journal_file_names] ) @@ -239,4 +243,4 @@ def _impl( fluent_jou_arg += " -topy" return fluent_jou_arg - return _impl(topy, journal_file_names) + return _impl(topy, journal_file_names, include_journal_file_names) diff --git a/src/ansys/fluent/core/launcher/standalone_launcher.py b/src/ansys/fluent/core/launcher/standalone_launcher.py index 798f426eeb2..3b4b6d32801 100644 --- a/src/ansys/fluent/core/launcher/standalone_launcher.py +++ b/src/ansys/fluent/core/launcher/standalone_launcher.py @@ -41,6 +41,7 @@ from pathlib import Path import subprocess from typing import TYPE_CHECKING, Any, TypedDict +import warnings from typing_extensions import Unpack @@ -67,6 +68,7 @@ _get_server_info_file_names, ) import ansys.fluent.core.launcher.watchdog as watchdog +from ansys.fluent.core.pyfluent_warnings import PyFluentUserWarning from ansys.fluent.core.utils.fluent_version import FluentVersion if TYPE_CHECKING: @@ -188,6 +190,8 @@ def __init__( lightweight_mode : bool, optional If True, runs in lightweight mode where mesh settings are read into a background solver session, replacing it once complete. This parameter is only applicable when `case_file_name` is provided; defaults to False. + When combined with `journal_file_names`, a warning is issued and `lightweight_mode` is set to False, as + journal processing cannot be reliably ordered with mesh-only initialization. py : bool, optional If True, runs Fluent in Python mode. Defaults to None. gpu : bool, optional @@ -214,6 +218,10 @@ def __init__( ----- In job scheduler environments (e.g., SLURM, LSF, PBS), resources and compute nodes are allocated, and core counts are queried from these environments before being passed to Fluent. + + File processing order: Case files are processed before case-data files, which are processed before + journal files. In lightweight mode, journal files are read before the background session is synchronized + with the foreground session. """ import ansys.fluent.core as pyfluent @@ -224,6 +232,15 @@ def __init__( self.argvals["ui_mode"] = UIMode(kwargs.get("ui_mode")) if self.argvals.get("lightweight_mode") is None: self.argvals["lightweight_mode"] = False + + if self.argvals["lightweight_mode"] and self.argvals.get("journal_file_names"): + warnings.warn( + "'lightweight_mode' is not supported together with " + "'journal_file_names' and will be ignored.", + PyFluentUserWarning, + ) + self.argvals["lightweight_mode"] = False + fluent_version = _get_standalone_launch_fluent_version(self.argvals) if ( @@ -255,11 +272,14 @@ def __init__( self._kwargs = _get_subprocess_kwargs_for_fluent( self.argvals.get("env") or {}, self.argvals ) - if self.argvals.get("cwd"): - self._kwargs.update(cwd=self.argvals.get("cwd")) - self._launch_string += _build_journal_argument( - self.argvals.get("topy", []), self.argvals.get("journal_file_names") - ) + if self.argvals["cwd"]: + self._kwargs.update(cwd=self.argvals["cwd"]) + + topy = self.argvals.get("topy", []) + if topy: + self._launch_string += _build_journal_argument( + topy, self.argvals.get("journal_file_names") + ) if is_windows(): self._launch_cmd = self._launch_string @@ -344,35 +364,8 @@ def __call__( values = _get_server_info(self._server_info_file_name) if len(values) == 3: ip, port, password = values - watchdog.launch( - os.getpid(), - port, - password, - ip, - inside_container=False, - ) - # PyFluent is now connected: disable the idle-timeout guard. - self._disable_idle_timeout_guard(session) - if self.argvals.get("case_file_name"): - if FluentMode.is_meshing(self.argvals.get("mode")): - session.tui.file.read_case(self.argvals.get("case_file_name")) - elif self.argvals.get("lightweight_mode"): - session.read_case_lightweight(self.argvals.get("case_file_name")) - else: - session.settings.file.read( - file_type="case", - file_name=self.argvals.get("case_file_name"), - ) - if self.argvals.get("case_data_file_name"): - if not FluentMode.is_meshing(self.argvals.get("mode")): - session.settings.file.read( - file_type="case-data", - file_name=self.argvals.get("case_data_file_name"), - ) - else: - raise RuntimeError( - "Case and data file cannot be read in meshing mode." - ) + watchdog.launch(os.getpid(), port, password, ip) + self._process_case_data_and_journals(session) return session except Exception as ex: @@ -382,3 +375,48 @@ def __call__( server_info_file = Path(self._server_info_file_name) if server_info_file.exists(): server_info_file.unlink() + + @staticmethod + def _get_journal_file_names( + journal_file_names: None | str | list[str], + ) -> list[str]: + if isinstance(journal_file_names, str): + return [journal_file_names] + return journal_file_names or [] + + def _process_case_data_and_journals(self, session) -> None: + if self.argvals["case_file_name"]: + if FluentMode.is_meshing(self.argvals["mode"]): + session.tui.file.read_case(self.argvals["case_file_name"]) + elif self.argvals["lightweight_mode"]: + session.read_case_lightweight( + self.argvals["case_file_name"], + start_sync=False, + ) + else: + session.settings.file.read( + file_type="case", + file_name=self.argvals["case_file_name"], + ) + if self.argvals["case_data_file_name"]: + if not FluentMode.is_meshing(self.argvals["mode"]): + session.settings.file.read( + file_type="case-data", + file_name=self.argvals["case_data_file_name"], + ) + else: + raise RuntimeError("Case and data file cannot be read in meshing mode.") + + if self.argvals.get("topy"): + for journal_file_name in self._get_journal_file_names( + self.argvals.get("journal_file_names") + ): + session.tui.file.write_journal(f"{journal_file_name}.topy") + else: + for journal_file_name in self._get_journal_file_names( + self.argvals.get("journal_file_names") + ): + session.execute_tui(f'/file/read-journal "{journal_file_name}"') + + if self.argvals.get("lightweight_mode"): + session.start_case_lightweight_sync() diff --git a/src/ansys/fluent/core/session_solver.py b/src/ansys/fluent/core/session_solver.py index 56fe17e9f8f..8360aeabe88 100644 --- a/src/ansys/fluent/core/session_solver.py +++ b/src/ansys/fluent/core/session_solver.py @@ -385,13 +385,21 @@ def _stop_bg_sessions(self): if thread.is_alive(): thread.join() - def read_case_lightweight(self, file_name: str): + def start_case_lightweight_sync(self): + """Start pending lightweight background sync sessions.""" + for thread in self._bg_session_threads: + if thread.ident is None: + thread.start() + + def read_case_lightweight(self, file_name: str, start_sync: bool = True): """Read a case file using light IO mode. Parameters ---------- file_name : str Case file name + start_sync : bool, optional + Whether to immediately start lightweight background sync. """ self.settings.file.read( @@ -405,6 +413,8 @@ def read_case_lightweight(self, file_name: str): target=self._start_bg_session_and_sync, args=(launcher_args,) ) ) + if start_sync: + self.start_case_lightweight_sync() def get_state(self) -> StateT: """Get the state of the object.""" diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 8b2c6ef009b..ce9b72df90a 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -57,6 +57,7 @@ _build_fluent_launch_args_string, get_fluent_exe_path, ) +from ansys.fluent.core.launcher.standalone_launcher import StandaloneLauncher from ansys.fluent.core.utils.fluent_version import FluentVersion import ansys.platform.instancemanagement as pypim @@ -473,6 +474,82 @@ def test_build_journal_argument(topy, journal_file_names, result, raises): assert _build_journal_argument(topy, journal_file_names) == result +def test_build_journal_argument_without_journal_files_but_with_topy(): + assert ( + _build_journal_argument("a.py", ["a.jou"], include_journal_file_names=False) + == ' -topy="a.py"' + ) + + +def test_lightweight_case_journal_read_is_completed_before_sync_step(): + launcher = object.__new__(StandaloneLauncher) + launcher.argvals = { + "case_file_name": "a.cas.h5", + "case_data_file_name": None, + "mode": FluentMode.SOLVER, + "lightweight_mode": True, + "journal_file_names": ["a.jou", "b.jou"], + } + launcher._defer_journal_file_read = True + + calls = [] + + class _DummySession: + def read_case_lightweight(self, file_name, start_sync=True): + calls.append(("read_case_lightweight", file_name, start_sync)) + + def execute_tui(self, command): + calls.append(("execute_tui", command)) + + def start_case_lightweight_sync(self): + calls.append(("start_case_lightweight_sync",)) + + launcher._process_case_data_and_journals(_DummySession()) + + assert calls == [ + ("read_case_lightweight", "a.cas.h5", False), + ("execute_tui", '/file/read-journal "a.jou"'), + ("execute_tui", '/file/read-journal "b.jou"'), + ("start_case_lightweight_sync",), + ] + + +def test_case_and_case_data_are_processed_before_journal_files(): + launcher = object.__new__(StandaloneLauncher) + launcher.argvals = { + "case_file_name": "a.cas.h5", + "case_data_file_name": "a.cas.h5", + "mode": FluentMode.SOLVER, + "lightweight_mode": False, + "journal_file_names": ["a.jou", "b.jou"], + } + launcher._defer_journal_file_read = True + + calls = [] + + class _DummyFile: + def read(self, **kwargs): + calls.append(("read", kwargs)) + + class _DummySettings: + file = _DummyFile() + + class _DummySession: + settings = _DummySettings() + + def execute_tui(self, command): + calls.append(("execute_tui", command)) + + launcher._process_case_data_and_journals(_DummySession()) + + assert calls == [ + ("read", {"file_type": "case", "file_name": "a.cas.h5"}), + ("read", {"file_type": "case-data", "file_name": "a.cas.h5"}), + ("execute_tui", '/file/read-journal "a.jou"'), + ("execute_tui", '/file/read-journal "b.jou"'), + ] + + def test_show_gui_raises_warning(): with pytest.warns(PyFluentDeprecationWarning): grpc_kwds = get_grpc_launcher_args_for_gh_runs()