diff --git a/doc/changelog.d/4600.added.md b/doc/changelog.d/4600.added.md new file mode 100644 index 000000000000..2ab20b97f75d --- /dev/null +++ b/doc/changelog.d/4600.added.md @@ -0,0 +1 @@ +Update client side 'enhanced' meshing workflow to use server side 'meshing_workflow' root. diff --git a/src/ansys/fluent/core/meshing/meshing_workflow.py b/src/ansys/fluent/core/meshing/meshing_workflow.py index a35dddc9141d..aea466a7e230 100644 --- a/src/ansys/fluent/core/meshing/meshing_workflow.py +++ b/src/ansys/fluent/core/meshing/meshing_workflow.py @@ -31,7 +31,7 @@ from ansys.fluent.core._types import PathType from ansys.fluent.core.services.datamodel_se import PyMenuGeneric from ansys.fluent.core.utils.fluent_version import FluentVersion -from ansys.fluent.core.workflow import Workflow +from ansys.fluent.core.workflow_new import Workflow name_to_identifier_map = { "Watertight Geometry": "EnableCleanCAD", @@ -77,23 +77,23 @@ def __init__( self._meshing = meshing self._name = name self._identifier = identifier - self._unsubscribe_root_affected_callback() + # self._unsubscribe_root_affected_callback() if initialize: self._new_workflow(name=self._name) else: self._activate_dynamic_interface(dynamic_interface=True) self._initialized = True - def __getattribute__(self, item: str): - if ( - not item.startswith("_") - and super().__getattribute__("_initialized") - and not getattr(self._meshing.GlobalSettings, self._identifier)() - ): - raise RuntimeError( - f"'{self._name}' objects are inaccessible from other workflows." - ) - return super().__getattribute__(item) + # def __getattribute__(self, item: str): + # if ( + # not item.startswith("_") + # and super().__getattribute__("_initialized") + # and not getattr(self._meshing.GlobalSettings, self._identifier)() + # ): + # raise RuntimeError( + # f"'{self._name}' objects are inaccessible from other workflows." + # ) + # return super().__getattribute__(item) class WatertightMeshingWorkflow(MeshingWorkflow): diff --git a/src/ansys/fluent/core/session_base_meshing.py b/src/ansys/fluent/core/session_base_meshing.py index a13f415a598a..9485b35c0e45 100644 --- a/src/ansys/fluent/core/session_base_meshing.py +++ b/src/ansys/fluent/core/session_base_meshing.py @@ -140,7 +140,7 @@ def meshing_workflow(self): def watertight_workflow(self, initialize: bool = True): """Datamodel root of workflow.""" self._current_workflow = WorkflowMode.WATERTIGHT_MESHING_MODE.value( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, self.get_fluent_version(), initialize, @@ -150,7 +150,7 @@ def watertight_workflow(self, initialize: bool = True): def fault_tolerant_workflow(self, initialize: bool = True): """Datamodel root of workflow.""" self._current_workflow = WorkflowMode.FAULT_TOLERANT_MESHING_MODE.value( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, self.PartManagement, self.PMFileManagement, @@ -162,7 +162,7 @@ def fault_tolerant_workflow(self, initialize: bool = True): def two_dimensional_meshing_workflow(self, initialize: bool = True): """Data model root of the workflow.""" self._current_workflow = WorkflowMode.TWO_DIMENSIONAL_MESHING_MODE.value( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, self.get_fluent_version(), initialize, @@ -172,7 +172,7 @@ def two_dimensional_meshing_workflow(self, initialize: bool = True): def topology_based_meshing_workflow(self, initialize: bool = True): """Datamodel root of workflow.""" self._current_workflow = WorkflowMode.TOPOLOGY_BASED_MESHING_MODE.value( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, self.get_fluent_version(), initialize, @@ -182,7 +182,7 @@ def topology_based_meshing_workflow(self, initialize: bool = True): def load_workflow(self, file_path: PathType): """Datamodel root of workflow.""" self._current_workflow = LoadWorkflow( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, os.fspath(file_path), self.get_fluent_version(), @@ -192,7 +192,7 @@ def load_workflow(self, file_path: PathType): def create_workflow(self, initialize: bool = True): """Datamodel root of the workflow.""" self._current_workflow = CreateWorkflow( - _make_datamodel_module(self, "workflow"), + _make_datamodel_module(self, "meshing_workflow"), self.meshing, self.get_fluent_version(), initialize, diff --git a/src/ansys/fluent/core/workflow_new.py b/src/ansys/fluent/core/workflow_new.py new file mode 100644 index 000000000000..44ed2e421e12 --- /dev/null +++ b/src/ansys/fluent/core/workflow_new.py @@ -0,0 +1,606 @@ +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Workflow module that wraps and extends the core functionality.""" + +from __future__ import annotations + +from collections import OrderedDict +import re + +from ansys.fluent.core.services.datamodel_se import PyMenu +from ansys.fluent.core.utils.fluent_version import FluentVersion + + +def _convert_task_list_to_display_names(workflow_root, task_list): + _display_names = [] + for _task_name in task_list: + name_obj = PyMenu( + service=workflow_root.service, + rules=workflow_root.rules, + path=[("task_object", _task_name), ("_name_", "")], + ) + _display_names.append(name_obj.get_remote_state()) + return _display_names + + +def _get_child_task_by_task_id(workflow_root, task_id): + return PyMenu( + service=workflow_root.service, + rules=workflow_root.rules, + path=[("task_object", task_id), ("_name_", "")], + ).get_remote_state() + + +def camel_to_snake_case(camel_case_str: str) -> str: + """Convert camel case input string to snake case output string.""" + if not camel_case_str.islower(): + _snake_case_str = ( + re.sub( + "((?<=[a-z])[A-Z0-9]|(?!^)[A-Z](?=[a-z0-9]))", + r"_\1", + camel_case_str, + ) + .lower() + .replace("__", "_") + ) + else: + _snake_case_str = camel_case_str + return _snake_case_str + + +camel_to_snake_case.cache = {} + + +class Workflow: + """Wraps a workflow object, adding methods to discover more about the relationships + between task objects.""" + + def __init__( + self, + workflow: PyMenu, + command_source: PyMenu, + fluent_version: FluentVersion, + ) -> None: + """Initialize WorkflowWrapper. + + Parameters + ---------- + workflow : PyMenu + The workflow object. + command_source : PyMenu + The application root for commanding. + """ + self._workflow = workflow + self._command_source = command_source + self._fluent_version = fluent_version + self._task_dict = {} + self._compound_child_dict = {} + + def tasks(self) -> list: + """Get the ordered task list held by the workflow.""" + self._task_dict = {} + _state = self._workflow.task_object() + for task in sorted(_state): + name = task.split(":")[0] + display_name = task.split(":")[-1] + task_obj = getattr(self._workflow.task_object, name)[display_name] + if task_obj.task_type() == "Compound Child": + if name not in self._compound_child_dict: + self._compound_child_dict[name] = { + name + "_child_1": task_obj, + } + else: + _name_list = [] + for key, value in self._compound_child_dict[name].items(): + _name_list.append(value._name_()) + if task_obj._name_() not in _name_list: + child_key = ( + int(sorted(self._compound_child_dict[name])[-1][-1]) + 1 + ) + self._compound_child_dict[name][ + name + f"_child_{child_key}" + ] = task_obj + else: + if name not in self._task_dict: + self._task_dict[name] = task_obj + else: + self._task_dict[name + f"_{task_obj.name().split()[-1]}"] = task_obj + + for key, value in self._compound_child_dict.items(): + for task_name, task_obj in value.items(): + self._task_dict[task_name] = task_obj + + return list(self._task_dict.values()) + + def _workflow_state(self): + return self._workflow() + + def _new_workflow(self, name: str): + self._workflow.general.initialize_workflow(workflow_type=name) + + def _load_workflow(self, file_path: str): + self._workflow.general.load_workflow(file_path=file_path) + + def _create_workflow(self): + self._workflow.general.create_new_workflow() + + def save_workflow(self, file_path: str): + """Save the current workflow to the location provided.""" + self._workflow.general.save_workflow(file_path=file_path) + + def load_state(self, list_of_roots: list): + """Load the state of the workflow.""" + self._workflow.general.load_state(list_of_roots=list_of_roots) + + def task_names(self): + """Get the list of the Python names for the available tasks.""" + names = [] + for name in self._workflow.task_object(): + names.append(name.split(":")[0]) + return names + + def children(self): + ordered_names = _convert_task_list_to_display_names( + self._workflow, + self._workflow.general.workflow.task_list(), + ) + name_to_task = { + task_obj.name(): TaskObject( + task_obj, task_obj.__class__.__name__.lstrip("_"), self._workflow, self + ) + for task_obj in self.tasks() + } + + sorted_list = [] + for name in ordered_names: + if name not in name_to_task: + continue + sorted_list.append(name_to_task[name]) + return sorted_list + + def first_child(self): + task_list = self._workflow.general.workflow.task_list() + if task_list: + first_name = _get_child_task_by_task_id(self._workflow, task_list[0]) + else: + return None + for task_obj in self.tasks(): + if task_obj.name() == first_name: + return TaskObject( + task_obj, task_obj.__class__.__name__.lstrip("_"), self._workflow, self + ) + + def last_child(self): + task_list = self._workflow.general.workflow.task_list() + if task_list: + last_name = _get_child_task_by_task_id(self._workflow, task_list[1]) + else: + return None + for task_obj in self.tasks(): + if task_obj.name() == last_name: + return TaskObject( + task_obj, task_obj.__class__.__name__.lstrip("_"), self._workflow, self + ) + + def task_list(self): + """.""" + return _convert_task_list_to_display_names( + self._workflow, self._workflow.general.workflow.task_list() + ) + + def ordered_tasks(self): + ordered_names = _convert_task_list_to_display_names( + self._workflow, + self._workflow.general.workflow.task_list(), + ) + name_to_task = { + task_obj.name(): TaskObject( + task_obj, task_obj.__class__.__name__.lstrip("_"), self._workflow, self + ) + for task_obj in self.tasks() + } + + sorted_dict = OrderedDict() + + for name in ordered_names: + if name not in name_to_task: + continue + task_obj = name_to_task[name] + sorted_dict[name] = task_obj + + return sorted_dict + + def delete_tasks(self, list_of_tasks: list[str]): + """Delete the provided list of tasks. + + Parameters + ---------- + list_of_tasks: list[str] + List of task items. + + Returns + ------- + None + + Raises + ------ + TypeError + If 'task' does not match a task name, no tasks are deleted. + """ + items_to_be_deleted = [] + for item in list_of_tasks: + if not isinstance(item, TaskObject): + if isinstance(item, str): + items_to_be_deleted.append(item) + else: + raise TypeError( + "'list_of_tasks' only takes list of 'TaskObject' types." + ) + else: + items_to_be_deleted.append(item.name()) + + self._workflow.general.delete_tasks(list_of_tasks=items_to_be_deleted) + + def __getattr__(self, item): + if item not in self._task_dict: + self.tasks() + if item in self._task_dict: + return TaskObject(self._task_dict[item], item, self._workflow, self) + return getattr(self._workflow, item) + + def __call__(self): + return self._workflow_state() + + def __delattr__(self, item): + if item not in self._task_dict: + self.tasks() + if item in self._task_dict: + getattr(self, item).delete() + del self._task_dict[item] + else: + raise LookupError(f"'{item}' is not a valid task name.'") + + +class TaskObject: + """TaskObject""" + + def __init__(self, task_object, base_name, workflow, parent): + """__init__ method of TaskObject class.""" + super().__setattr__("_task_object", task_object) + super().__setattr__("_name", base_name) + super().__setattr__("_workflow", workflow) + super().__setattr__("_parent", parent) + self._cache = {} + + def get_next_possible_tasks(self): + """.""" + task_obj = super().__getattribute__("_task_object") + ret_list = [] + for item in task_obj.get_next_possible_tasks(): + snake_case_name = camel_to_snake_case(item) + if snake_case_name != item: + self._cache[snake_case_name] = item + ret_list.append(snake_case_name) + return ret_list + + def insert_next_task(self, task_name): + """.""" + task_obj = super().__getattribute__("_task_object") + # This is just a precaution in case this method is directly called from the task level. + self.get_next_possible_tasks() + command_name = self._cache.get(task_name) or task_name + task_obj.insert_next_task(command_name=command_name) + + @property + def insertable_tasks(self): + """Tasks that can be inserted after the current task.""" + return self._NextTask(self) + + class _NextTask: + # Comment the code for better explanation. + def __init__(self, base_task): + """Initialize an ``_NextTask`` instance.""" + self._base_task = base_task + self._insertable_tasks = [] + for item in self._base_task.get_next_possible_tasks(): + insertable_task = type("Insert", (self._Insert,), {})( + self._base_task, item + ) + setattr(self, item, insertable_task) + self._insertable_tasks.append(insertable_task) + + def __call__(self): + return self._insertable_tasks + + class _Insert: + def __init__(self, base_task, name): + """Initialize an ``_Insert`` instance.""" + self._base_task = base_task + self._name = name + + def insert(self): + """Insert a task in the workflow.""" + return self._base_task.insert_next_task(task_name=self._name) + + def __repr__(self): + return f"" + + def __getattr__(self, item): + task_obj = super().__getattribute__("_task_object") + args = task_obj.arguments + if item in args(): + return getattr(args, item) + return getattr(task_obj, item) + + def __setattr__(self, key, value): + task_obj = super().__getattribute__("_task_object") + args = task_obj.arguments + if hasattr(args, key): + setattr(args, key, value) + else: + super().__setattr__(key, value) + + def __call__(self): + task_obj = super().__getattribute__("_task_object") + return task_obj.execute() + + def __getitem__(self, key): + task_obj = super().__getattribute__("_task_object") + name = super().__getattribute__("_name") + workflow = super().__getattribute__("_workflow") + parent = super().__getattribute__("_parent") + name_1 = name + name_2 = re.sub(r"\s+\d+$", "", task_obj.name().strip()) + f" {key}" + try: + task_obj = getattr(workflow.task_object, name_1)[name_2] + if task_obj.task_type == "Compound Child": + temp_parent = self + else: + temp_parent = parent + return TaskObject(task_obj, name_1, workflow, temp_parent) + except LookupError: + task_obj = getattr(workflow.task_object, name_1)[key] + if task_obj.task_type == "Compound Child": + temp_parent = self + else: + temp_parent = parent + try: + return TaskObject( + getattr(workflow.task_object, name_1)[key], + name_1, + workflow, + temp_parent, + ) + except LookupError as ex2: + raise LookupError( + f"Neither '{name_2}' nor '{key}' not found in task object '{name_1}'." + ) from ex2 + + def __delitem__(self, key): + self[key].delete() + + def task_list(self): + """.""" + task_obj = super().__getattribute__("_task_object") + # This is just a precaution in case this method is directly called from the task level. + task_list = task_obj.task_list() + if task_list: + return _convert_task_list_to_display_names( + super().__getattribute__("_workflow"), task_list + ) + else: + return [] + + def children(self): + if not self.task_list(): + return [] + + workflow = super().__getattribute__("_workflow") + type_to_name = { + item.split(":")[0]: item.split(":")[-1] for item in workflow.task_object() + } + name_to_task = { + val: TaskObject( + getattr(workflow.task_object, key)[val], key, workflow, self + ) + for key, val in type_to_name.items() + } + sorted_list = [] + for name in self.task_list(): + if name not in name_to_task: + continue + sorted_list.append(name_to_task[name]) + return sorted_list + + def first_child(self): + task_list = self.task_list() + if task_list: + first_name = task_list[0] + else: + return None + workflow = super().__getattribute__("_workflow") + + type_to_name = { + item.split(":")[0]: item.split(":")[-1] for item in workflow.task_object() + } + for key, val in type_to_name.items(): + if val == first_name: + return TaskObject( + getattr(workflow.task_object, key)[val], key, workflow, self + ) + + def last_child(self): + task_list = self.task_list() + if task_list: + last_name = task_list[-1] + else: + return None + workflow = super().__getattribute__("_workflow") + + type_to_name = { + item.split(":")[0]: item.split(":")[-1] for item in workflow.task_object() + } + for key, val in type_to_name.items(): + if val == last_name: + return TaskObject( + getattr(workflow.task_object, key)[val], key, workflow, self + ) + + @staticmethod + def _get_next_key(input_dict, current_key): + keys = list(input_dict) + idx = keys.index(current_key) + if idx == len(keys) - 1: + raise IndexError("Reached the end.") + return keys[idx + 1] + + @staticmethod + def _get_previous_key(input_dict, current_key): + keys = list(input_dict) + idx = keys.index(current_key) + if idx == 0: + raise IndexError("In the beginning.") + return keys[idx - 1] + + def has_parent(self): + try: + super().__getattribute__("_parent") + return True + except AttributeError: + return False + + def parent(self): + parent = super().__getattribute__("_parent") + return parent + + def has_next(self) -> bool: + parent = super().__getattribute__("_parent") + task_dict = parent.ordered_tasks() + try: + self._get_next_key(task_dict, self.name()) + return True + except IndexError: + return False + + def next(self): + parent = super().__getattribute__("_parent") + task_dict = parent.ordered_tasks() + next_key = self._get_next_key(task_dict, self.name()) + return task_dict[next_key] + + def has_previous(self) -> bool: + parent = super().__getattribute__("_parent") + task_dict = parent.ordered_tasks() + try: + self._get_previous_key(task_dict, self.name()) + return True + except IndexError: + return False + + def previous(self): + parent = super().__getattribute__("_parent") + task_dict = parent.ordered_tasks() + previous_key = self._get_previous_key(task_dict, self.name()) + return task_dict[previous_key] + + def ordered_tasks(self): + sorted_dict = OrderedDict() + if not self.task_list(): + return sorted_dict + workflow = super().__getattribute__("_workflow") + + type_to_name = { + item.split(":")[0]: item.split(":")[-1] for item in workflow.task_object() + } + + name_to_task = { + val: TaskObject( + getattr(workflow.task_object, key)[val], key, workflow, self + ) + for key, val in type_to_name.items() + } + + for name in self.task_list(): + if name not in name_to_task: + continue + task_obj = name_to_task[name] + sorted_dict[name] = task_obj + + return sorted_dict + + def get_sorted_tasks(self): + workflow = super().__getattribute__("_workflow") + sorted_dict = OrderedDict() + ordered_names = _convert_task_list_to_display_names( + workflow, + workflow.general.workflow.task_list(), + ) + type_to_name = { + item.split(":")[0]: item.split(":")[-1] for item in workflow.task_object() + } + + name_to_task = { + val: TaskObject( + getattr(workflow.task_object, key)[val], key, workflow, self + ) + for key, val in type_to_name.items() + } + + for name in ordered_names: + if name not in name_to_task: + continue + task_obj = name_to_task[name] + sorted_dict[name] = task_obj + + sub_task_names = task_obj.task_list() + if sub_task_names: + for sub_task_name in sub_task_names: + sorted_dict[sub_task_name] = name_to_task[sub_task_name] + + return sorted_dict + + def get_upstream_tasks(self): + upstream_tasks = OrderedDict() + for name, task_obj in self.get_sorted_tasks().items(): + if name == self.name(): + break + upstream_tasks[name] = task_obj + return upstream_tasks + + def get_downstream_tasks(self): + name_found = False + downstream_tasks = OrderedDict() + for name, task_obj in self.get_sorted_tasks().items(): + if name_found: + downstream_tasks[name] = task_obj + if name == self.name(): + name_found = True + return downstream_tasks + + def delete(self): + """.""" + workflow = super().__getattribute__("_workflow") + workflow.general.delete_tasks(list_of_tasks=[self.name()]) + + def __repr__(self): + return self.name() diff --git a/tests/test_server_meshing_workflow.py b/tests/test_server_meshing_workflow.py index cf1e537de2a2..77d94bd1c33b 100644 --- a/tests/test_server_meshing_workflow.py +++ b/tests/test_server_meshing_workflow.py @@ -23,6 +23,7 @@ import pytest from ansys.fluent.core import examples +from ansys.fluent.core.services.datamodel_se import PyMenu @pytest.mark.fluent_version(">=26.1") @@ -786,3 +787,99 @@ def test_arguments_and_parameters_in_new_meshing_workflow(new_meshing_session): watertight.task_object.import_geometry["Import Geometry"].state() == "Forced-up-to-date" ) + + +@pytest.mark.codegen_required +@pytest.mark.fluent_version(">=26.1") +def test_get_task_by_id(new_meshing_session): + # This test is only intended for developer level testing + meshing_session = new_meshing_session + meshing_session.meshing_workflow.general.initialize_workflow( + workflow_type="Watertight Geometry" + ) + service = meshing_session.meshing_workflow.service + rules = meshing_session.meshing_workflow.rules + + path = [("task_object", "TaskObject1"), ("_name_", "")] + assert ( + PyMenu(service=service, rules=rules, path=path).get_remote_state() + == "Import Geometry" + ) + + path = [("task_object", "TaskObject1"), ("CommandName", "")] + assert ( + PyMenu(service=service, rules=rules, path=path).get_remote_state() + == "ImportGeometry" + ) + + path = [("task_object", "TaskObject5"), ("_name_", "")] + assert ( + PyMenu(service=service, rules=rules, path=path).get_remote_state() + == "Apply Share Topology" + ) + + path = [("task_object", "TaskObject1")] + assert PyMenu(service=service, rules=rules, path=path).get_remote_state() == { + "_name_": "Import Geometry", + "arguments": {}, + "warnings": None, + "command_name": "ImportGeometry", + "errors": None, + "task_type": "Simple", + "object_path": "", + "state": "Out-of-date", + "check_point": "default-off", + } + + +@pytest.mark.fluent_version(">=26.1") +def test_insert_delete_and_rename_task(new_meshing_session): + meshing_session = new_meshing_session + meshing_session.meshing_workflow.general.initialize_workflow( + workflow_type="Watertight Geometry" + ) + + # Insert new task + assert len(meshing_session.meshing_workflow.task_object()) == 11 + meshing_session.meshing_workflow.task_object.import_geometry[ + "Import Geometry" + ].insert_next_task(command_name="ImportBodyOfInfluenceGeometry") + assert len(meshing_session.meshing_workflow.task_object()) == 12 + assert meshing_session.meshing_workflow.task_object.import_boi_geometry[ + "Import Body of Influence Geometry" + ].arguments() == { + "type": "CAD", + "geometry_file_name": None, + "cad_import_options": {}, + } + + # Delete + assert len(meshing_session.meshing_workflow.task_object()) == 12 + assert ( + "create_volume_mesh_wtm:Generate the Volume Mesh" + in meshing_session.meshing_workflow.task_object() + ) + meshing_session.meshing_workflow.general.delete_tasks( + list_of_tasks=["Generate the Volume Mesh"] + ) + assert len(meshing_session.meshing_workflow.task_object()) == 11 + assert ( + "create_volume_mesh_wtm:Generate the Volume Mesh" + not in meshing_session.meshing_workflow.task_object() + ) + + # Rename + assert ( + "add_boundary_layers:Add Boundary Layers" + in meshing_session.meshing_workflow.task_object() + ) + meshing_session.meshing_workflow.task_object.add_boundary_layers[ + "Add Boundary Layers" + ].rename(new_name="Add BL") + assert ( + "add_boundary_layers:Add Boundary Layers" + not in meshing_session.meshing_workflow.task_object() + ) + assert ( + "add_boundary_layers:Add BL" in meshing_session.meshing_workflow.task_object() + )