Repository: ansys/pyfluent

Issue:
Fluent session will not launch if AWP_ROOTXXX environment variable is for later release than installed Fluent release.
### 🔍 Before submitting the issue

- [x] I have searched among the existing issues
- [x] I am using a Python virtual environment

### 🐞 Description of the bug

If the AWP_ROOTXXX environment variable exists for a later release than the installed Fluent release, ``launch_fluent`` will fail with default arguments. e.g. AWP_ROOT252 will break launching for if the latest version of Fluent is 251.
This can break automated workflows.

See https://github.com/ansys/ansys-tools-path/issues/296 and https://github.com/ansys/pysherlock/issues/523 for similar issues.

### 📝 Steps to reproduce

Install Ansys Fluent version 2025 R1, with no later version of any Ansys product installed.

Run the following commands:
```
>>> import os
>>> from ansys.fluent.core import launch_fluent
>>> print(os.getenv('AWP_ROOT252'))  # Confirm no 2025 R2 installed
None
>>> solver = launch_fluent()  # Correctly launches
>>> solver.is_active()
True
>>> solver.exit()
>>> os.environ['AWP_ROOT252'] = 'C:\\Program Files\\ANSYS Inc\\v252'  # Mimic 2025 R2 installation
>>> solver = launch_fluent()  # Launch fails due to missing .exe
The system cannot find the file C:\Program Files\ANSYS Inc\v252\fluent\ntbin\win64\fluent.exe.
```

### 💻 Which operating system are you using?

Windows

### 📀 Which ANSYS version are you using?

_No response_

### 🐍 Which Python version are you using?

3.12

### 📦 Installed packages

```shell
ansys-api-fluent==0.3.35
ansys-api-platform-instancemanagement==1.1.3
ansys-api-tools-filetransfer==0.1.1
ansys-fluent-core @ git+https://github.com/ansys/pyfluent.git@26d2edd26a885a5514d02f186c6505c1420f7889
ansys-platform-instancemanagement==1.1.2
ansys-tools-filetransfer==0.1.1
ansys-units==0.4.0
certifi==2025.4.26
charset-normalizer==3.4.2
click==8.2.0
colorama==0.4.6
defusedxml==0.7.1
Deprecated==1.2.18
docker==7.1.0
googleapis-common-protos==1.70.0
grpcio==1.71.0
grpcio-health-checking==1.62.3
grpcio-status==1.62.3
idna==3.10
importlib_metadata==8.7.0
joblib==1.5.0
nltk==3.9.1
numpy==2.2.5
pandas==2.2.3
protobuf==4.25.7
pyansys-tools-report==0.8.2
python-dateutil==2.9.0.post0
pytz==2025.2
pywin32==310
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
scooby==0.10.1
setuptools==80.4.0
six==1.17.0
tqdm==4.67.1
tzdata==2025.2
urllib3==2.4.0
wrapt==1.17.2
zipp==3.21.0
```

Hints from issue comments:
@mkundu1: By default, PyFluent will look for the latest version of Ansys Fluent installed on the machine. The `launch_fluent` method supports a `product_version` parameter to specify the Ansys version: https://fluent.docs.pyansys.com/version/stable/api/launcher/launcher.html#ansys.fluent.core.launcher.launcher.launch_fluent@EDCarman: @mkundu1 the problem comes when the latest version of Fluent does not match the latest version of other tools.

e.g. if I have Fluent 2024 R2 installed and Mechanical 2025 R1 installed, it will fail because AWP_ROOT251 exists, but the 2025 R1 Fluent does not. It is not finding the latest version of Ansys Fluent installed on the machine, because it stops as soon as it finds AWP_ROOT251 and assumes the corresponding .exe exists. 
It should rather confirm whether the .exe exists. If it doesn't, it should move to check for AWP_ROOT242, 241 etc. until if finds an exe, or runs out of valid versions.

Using the ``product _version`` parameter may be problematic as it then hard-codes an Ansys version into the script. For example, I use ``product _version=242`` and it works. I later uninstall all 2024 R2 products and install 2025 R1 Fluent. The script will fail again, until I edit it.@mkundu1: @EDCarman Right, it should check wheter the exe exists, thanks. We'll fix it.

Solution patch:
diff --git a/doc/changelog.d/4024.added.md b/doc/changelog.d/4024.added.md
new file mode 100644
index 0000000000..74e4ed34f5
--- /dev/null
+++ b/doc/changelog.d/4024.added.md
@@ -0,0 +1 @@
+Check if fluent exe exists in AWP_ROOT path while determining the Fluent version to launch
\ No newline at end of file
diff --git a/doc/changelog.d/4025.documentation.md b/doc/changelog.d/4025.documentation.md
new file mode 100644
index 0000000000..273a84d893
--- /dev/null
+++ b/doc/changelog.d/4025.documentation.md
@@ -0,0 +1 @@
+use variable catalog
\ No newline at end of file
diff --git a/doc/source/user_guide/physical_variables.rst b/doc/source/user_guide/physical_variables.rst
new file mode 100644
index 0000000000..b8a169da5f
--- /dev/null
+++ b/doc/source/user_guide/physical_variables.rst
@@ -0,0 +1,78 @@
+﻿.. _user_guide_variables:
+
+===============================
+Working with physical variables
+===============================
+
+PyFluent integrates with the PyAnsys-units library, which provides a shared catalog of variable objects based on physical quantities like temperature, pressure, and velocity. These variable objects, or VariableDescriptors, can be used throughout PyFluent to reference fields and quantities in a clear, consistent, and reliable way.
+
+Instead of relying on raw strings like ``"temperature"`` or ``"SV_T"``, which may vary between Fluent interfaces or be hard to interpret, you can use named descriptors from the catalog such as ``VariableCatalog``.``TEMPERATURE``). This improves code readability, reduces the chance of errors, and makes it easier to work across different APIs.
+
+The same catalog is designed to work not just with PyFluent, but also with other PyAnsys libraries, offering a unified and expressive way to interact with physical quantities across products.
+
+Overview
+--------
+
+The key benefits of using the ``VariableCatalog`` include:
+
+- Code that is portable across Ansys products and more resistant to typos
+- Simplified refactoring when field names change in Fluent or across versions
+- Integrated dimensional consistency (via ``ansys-units``)
+- Autocompletion and discoverability of supported quantities
+
+Accessing field data
+---------------------
+
+Here’s how to use ``VariableCatalog`` to read and reduce field data using unit-aware quantity references:
+
+.. code-block:: python
+
+    from ansys.fluent.core import launch_fluent, examples
+    from ansys.units.variable_descriptor import VariableCatalog
+
+    solver = launch_fluent()
+    case_path = examples.download_file("mixing_elbow.cas.h5", "pyfluent/mixing_elbow")
+    solver.file.read(file_type="case", file_name=case_path)
+
+    solver.settings.solution.initialization.hybrid_initialize()
+
+    temperature = VariableCatalog.TEMPERATURE
+    locations = ["hot-inlet"]
+
+    # Access scalar field data
+    temp_data = solver.fields.field_data.get_scalar_field_data(
+        field_name=temperature, surfaces=locations
+    )
+    print(temp_data[locations[0]][0])  # value at hot-inlet
+
+    # Compute minimum of a physical quantity
+    temp_min = solver.fields.reduction.minimum(expression=temperature, locations=locations)
+    print(temp_min)
+
+    # Access solution variable data
+    sol_data = solver.fields.solution_variable_data.get_data(
+        solution_variable_name=temperature, zone_names=locations
+    )
+    print(sol_data[locations[0]][0])
+
+Using variables with report definitions
+---------------------------------------
+
+You can also use physical quantities in report definitions to improve clarity and maintainability:
+
+.. code-block:: python
+
+    surface_report = solver.settings.solution.report_definitions.surface["avg_temp"]
+    surface_report.report_type = "surface-areaavg"
+    surface_report.field = temperature  # Note: using VariableCatalog, not a string
+    surface_report.surface_names = locations
+
+    result = solver.solution.report_definitions.compute(report_defs=["avg_temp"])
+    print(result[0]["avg_temp"][0])
+
+Notes
+-----
+
+- All quantities used via ``VariableCatalog`` map to their correct Fluent field identifiers internally.
+- These objects can be validated against their expected dimensional types.
+- ``VariableCatalog`` is part of ``ansys-units``, which is automatically installed with PyFluent.
diff --git a/doc/source/user_guide/user_guide_contents.rst b/doc/source/user_guide/user_guide_contents.rst
index 2560a11ad1..b66f6ce83a 100644
--- a/doc/source/user_guide/user_guide_contents.rst
+++ b/doc/source/user_guide/user_guide_contents.rst
@@ -16,6 +16,7 @@ User guide
    monitors
    transfer_data
    units
+   physical_variables
    file_transfer
    offline/offline_contents
    convert_journal
diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt
index ea6eca2837..187d1a8190 100644
--- a/doc/styles/config/vocabularies/ANSYS/accept.txt
+++ b/doc/styles/config/vocabularies/ANSYS/accept.txt
@@ -82,3 +82,4 @@ venv
 create_workflow
 load_workflow
 codegen
+discoverability
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index e313ccd226..28d61be883 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,6 +47,7 @@ tests = [
     "pytest-cov==6.1.1",
     "pytest-mock==3.14.0",
     "pytest-xdist==3.6.1",
+    "pyfakefs==5.8.0"
 ]
 docs = [
     "Sphinx==8.1.3",
diff --git a/src/ansys/fluent/core/codegen/tuigen.py b/src/ansys/fluent/core/codegen/tuigen.py
index 4bdb2ebb09..63c9180cb9 100644
--- a/src/ansys/fluent/core/codegen/tuigen.py
+++ b/src/ansys/fluent/core/codegen/tuigen.py
@@ -58,7 +58,6 @@ from ansys.fluent.core.services.datamodel_tui import (
 )
 from ansys.fluent.core.utils.fix_doc import escape_wildcards
 from ansys.fluent.core.utils.fluent_version import (
-    AnsysVersionNotFound,
     FluentVersion,
     get_version_for_file_name,
 )
@@ -130,7 +129,7 @@ def _copy_tui_help_xml_file(version: str):
                 shutil.copy(str(xml_source), _XML_HELP_FILE)
             else:
                 logger.warning("fluent_gui_help.xml is not found.")
-        except AnsysVersionNotFound:
+        except FileNotFoundError:
             logger.warning("fluent_gui_help.xml is not found.")
 
 
diff --git a/src/ansys/fluent/core/launcher/launcher.py b/src/ansys/fluent/core/launcher/launcher.py
index 723054192e..f594ed1dad 100644
--- a/src/ansys/fluent/core/launcher/launcher.py
+++ b/src/ansys/fluent/core/launcher/launcher.py
@@ -184,6 +184,12 @@ def launch_fluent(
         Version of Ansys Fluent to launch. To use Fluent version 2025 R1, pass
         any of  ``FluentVersion.v251``, ``"25.1.0"``, ``"25.1"``, ``25.1``or ``251``.
         The default is ``None``, in which case the newest installed version is used.
+        PyFluent uses the ``AWP_ROOT<ver>`` environment variable to locate the Fluent
+        installation, where ``<ver>`` is the Ansys release number such as ``251``.
+        The ``AWP_ROOT<ver>`` environment variable is automatically configured on Windows
+        system when Fluent is installed. On Linux systems, ``AWP_ROOT<ver>`` must be
+        configured to point to the absolute path of an Ansys installation such as
+        ``/apps/ansys_inc/v251``.
     dimension : Dimension or int, optional
         Geometric dimensionality of the Fluent simulation. The default is ``None``,
         in which case ``Dimension.THREE`` is used. Options are either the values of the
diff --git a/src/ansys/fluent/core/launcher/process_launch_string.py b/src/ansys/fluent/core/launcher/process_launch_string.py
index 005e22f76f..6c2a48609b 100644
--- a/src/ansys/fluent/core/launcher/process_launch_string.py
+++ b/src/ansys/fluent/core/launcher/process_launch_string.py
@@ -150,10 +150,6 @@ def get_fluent_exe_path(**launch_argvals) -> Path:
         Fluent executable path
     """
 
-    def get_fluent_root(version: FluentVersion) -> Path:
-        awp_root = os.environ[version.awp_var]
-        return Path(awp_root) / "fluent"
-
     def get_exe_path(fluent_root: Path) -> Path:
         if launcher_utils.is_windows():
             return fluent_root / "ntbin" / "win64" / "fluent.exe"
@@ -171,7 +167,7 @@ def get_fluent_exe_path(**launch_argvals) -> Path:
     # 2. product_version parameter passed with launch_fluent
     product_version = launch_argvals.get("product_version")
     if product_version:
-        return get_exe_path(get_fluent_root(FluentVersion(product_version)))
+        return FluentVersion(product_version).get_fluent_exe_path()
 
     # (DEV) "PYFLUENT_FLUENT_ROOT" environment variable
     fluent_root = os.getenv("PYFLUENT_FLUENT_ROOT")
@@ -179,4 +175,4 @@ def get_fluent_exe_path(**launch_argvals) -> Path:
         return get_exe_path(Path(fluent_root))
 
     # 3. the latest ANSYS version from AWP_ROOT environment variables
-    return get_exe_path(get_fluent_root(FluentVersion.get_latest_installed()))
+    return FluentVersion.get_latest_installed().get_fluent_exe_path()
diff --git a/src/ansys/fluent/core/utils/fluent_version.py b/src/ansys/fluent/core/utils/fluent_version.py
index 249ef85107..d95425daac 100644
--- a/src/ansys/fluent/core/utils/fluent_version.py
+++ b/src/ansys/fluent/core/utils/fluent_version.py
@@ -25,6 +25,9 @@
 from enum import Enum
 from functools import total_ordering
 import os
+from pathlib import Path
+import platform
+from typing import Any
 
 import ansys.fluent.core as pyfluent
 
@@ -32,7 +35,19 @@ import ansys.fluent.core as pyfluent
 class AnsysVersionNotFound(RuntimeError):
     """Raised when Ansys version is not found."""
 
-    pass
+    def __init__(self, version: Any):
+        """Initialize VersionNotFound.
+
+        Parameters
+        ----------
+        version : str
+            Version that was not found.
+        """
+        super().__init__(
+            f"The specified version '{version}' is not supported."
+            + " Supported versions are: "
+            + ", ".join([member.value for member in FluentVersion][::-1])
+        )
 
 
 class ComparisonError(RuntimeError):
@@ -84,7 +99,7 @@ class FluentVersion(Enum):
     v222 = "22.2.0"
 
     @classmethod
-    def _missing_(cls, version):
+    def _missing_(cls, version: Any):
         if isinstance(version, (int, float, str)):
             version = str(version)
             if len(version) == 3:
@@ -93,11 +108,8 @@ class FluentVersion(Enum):
             for member in cls:
                 if version == member.value:
                     return member
-        raise AnsysVersionNotFound(
-            f"The specified version '{version[:-2]}' is not supported."
-            + " Supported versions are: "
-            + ", ".join([member.value for member in cls][::-1])
-        )
+
+        raise AnsysVersionNotFound(version[:-2])
 
     @classmethod
     def get_latest_installed(cls):
@@ -111,15 +123,33 @@ class FluentVersion(Enum):
 
         Raises
         ------
-        AnsysVersionNotFound
+        FileNotFoundError
             If an Ansys version cannot be found.
         """
         for member in cls:
-            if member.awp_var in os.environ:
+            if member.awp_var in os.environ and member.get_fluent_exe_path().exists():
                 return member
 
-        raise AnsysVersionNotFound(
-            "Verify the value of the 'AWP_ROOT' environment variable."
+        raise FileNotFoundError(
+            "Unable to locate a compatible Ansys Fluent installation. "
+            "Ensure that an environment variable like 'AWP_ROOT242' or 'AWP_ROOT251' "
+            "points to a supported Ansys version, and that Fluent is included in the installation."
+        )
+
+    def get_fluent_exe_path(self) -> Path:
+        """Get the path for the Fluent executable file.
+
+        Returns
+        -------
+        Path
+            Fluent executable path.
+        """
+        awp_root = os.environ[self.awp_var]
+        fluent_root = Path(awp_root) / "fluent"
+        return (
+            fluent_root / "ntbin" / "win64" / "fluent.exe"
+            if platform.system() == "Windows"
+            else fluent_root / "bin" / "fluent"
         )
 
     @classmethod


Test patch:
diff --git a/tests/test_fluent_version.py b/tests/test_fluent_version.py
index f6008253fb..8f12c1e567 100644
--- a/tests/test_fluent_version.py
+++ b/tests/test_fluent_version.py
@@ -47,8 +47,11 @@ def test_version_not_found():
         FluentVersion(22)
 
 
-def test_get_latest_installed(helpers):
+def test_get_latest_installed(helpers, fs):
     helpers.mock_awp_vars()
+    with pytest.raises(FileNotFoundError):
+        assert FluentVersion.get_latest_installed() == FluentVersion.current_release()
+    fs.create_file(FluentVersion.current_release().get_fluent_exe_path())
     assert FluentVersion.get_latest_installed() == FluentVersion.current_release()
 
 
diff --git a/tests/test_launcher.py b/tests/test_launcher.py
index b1ee55de62..336343d5d1 100644
--- a/tests/test_launcher.py
+++ b/tests/test_launcher.py
@@ -55,7 +55,7 @@ from ansys.fluent.core.launcher.pyfluent_enums import (
     LaunchMode,
     UIMode,
 )
-from ansys.fluent.core.utils.fluent_version import AnsysVersionNotFound, FluentVersion
+from ansys.fluent.core.utils.fluent_version import FluentVersion
 import ansys.platform.instancemanagement as pypim
 
 
@@ -256,49 +256,31 @@ def test_gpu_launch_arg_additional_arg():
 
 def test_get_fluent_exe_path_when_nothing_is_set(helpers):
     helpers.delete_all_awp_vars()
-    with pytest.raises(AnsysVersionNotFound):
+    with pytest.raises(FileNotFoundError):
         get_fluent_exe_path()
-    with pytest.raises(AnsysVersionNotFound):
+    with pytest.raises(FileNotFoundError):
         FluentVersion.get_latest_installed()
 
 
-def test_get_fluent_exe_path_from_awp_root_222(helpers):
-    helpers.mock_awp_vars(version="222")
-    if platform.system() == "Windows":
-        expected_path = Path("ansys_inc/v222/fluent") / "ntbin" / "win64" / "fluent.exe"
-    else:
-        expected_path = Path("ansys_inc/v222/fluent") / "bin" / "fluent"
-    assert FluentVersion.get_latest_installed() == FluentVersion.v222
-    assert get_fluent_exe_path() == expected_path
-
-
-def test_get_fluent_exe_path_from_awp_root_231(helpers):
-    helpers.mock_awp_vars(version="231")
-    if platform.system() == "Windows":
-        expected_path = Path("ansys_inc/v231/fluent") / "ntbin" / "win64" / "fluent.exe"
-    else:
-        expected_path = Path("ansys_inc/v231/fluent") / "bin" / "fluent"
-    assert FluentVersion.get_latest_installed() == FluentVersion.v231
-    assert get_fluent_exe_path() == expected_path
-
-
-def test_get_fluent_exe_path_from_awp_root_232(helpers):
-    helpers.mock_awp_vars(version="232")
-    if platform.system() == "Windows":
-        expected_path = Path("ansys_inc/v232/fluent") / "ntbin" / "win64" / "fluent.exe"
-    else:
-        expected_path = Path("ansys_inc/v232/fluent") / "bin" / "fluent"
-    assert FluentVersion.get_latest_installed() == FluentVersion.v232
-    assert get_fluent_exe_path() == expected_path
-
-
-def test_get_fluent_exe_path_from_awp_root_241(helpers):
-    helpers.mock_awp_vars(version="241")
+@pytest.mark.parametrize(
+    "fluent_version",
+    [version for version in FluentVersion],
+)
+def test_get_fluent_exe_path_from_awp_root(fluent_version, helpers, fs):
+    helpers.mock_awp_vars(version=str(fluent_version.number))
+    fs.create_file(fluent_version.get_fluent_exe_path())
     if platform.system() == "Windows":
-        expected_path = Path("ansys_inc/v241/fluent") / "ntbin" / "win64" / "fluent.exe"
+        expected_path = (
+            Path(f"ansys_inc/v{fluent_version.number}/fluent")
+            / "ntbin"
+            / "win64"
+            / "fluent.exe"
+        )
     else:
-        expected_path = Path("ansys_inc/v241/fluent") / "bin" / "fluent"
-    assert FluentVersion.get_latest_installed() == FluentVersion.v241
+        expected_path = (
+            Path(f"ansys_inc/v{fluent_version.number}/fluent") / "bin" / "fluent"
+        )
+    assert FluentVersion.get_latest_installed() == fluent_version
     assert get_fluent_exe_path() == expected_path