.. _bolt_example: Full-code customization tutorial: Bolt app ========================================== This tutorial shows how to create a full code customization of the web app. It is intended for advanced users for whom the default customization options are not sufficient. .. note:: This tutorial assumes that you have some knowledge of Python and the Python-based UI framework, Dash. For information on Dash, see the `Dash documentation `_. Code: You can access the :ref:`full code` at the end of this document or download it :download:`here `. Project: You can download the optiSLang project used in this tutorial :download:`here `. The simplified tutorial project consists of nodes that run Python code to generate values of allowable bolt forces and stresses for each given design point, where each design point has parameters for bolt class, bolt diameter, and safety factor. .. attention:: This project should be used only to demonstrate the customization of the UI. Do not use it for any real calculations. Customized UI: When you complete this tutorial, the customized UI will look like this: .. image:: /img/bolt_app_full_page.png :width: 100% :align: center :alt: Screenshot of the Bolt app In the final customized UI, you can see that: * There are two tables, one with the parameters and one with the responses, for each design point. * There are two bar charts, one with the stress response values and one with the force values. * On the right side, there is a markdown text with information about the calculations and a custom image. The following sections describe how to create this custom UI step by step, starting from the default example solution created from the supplied optiSLang project. Create the default solution ----------------------------- The first step is to create a new solution from the supplied optiSLang project ``Bolt_App.opf``: #. Open the project in optiSLang and save it as Ansys Web App. #. :ref:`Unpack the created .awa file `. #. Set up the development environment as described in :ref:`test_the_changes`. When you run this default solution, it looks like this: .. image:: /img/bolt_default.png :width: 100% :align: center :alt: Screenshot of the Bolt app default solution Add a custom image --------------------- Add a custom image to be used in the page. #. Add the image. a. Download ``bolt.png`` file from here :download:`here `. b. Add the image to the folder ``src\ansys\solutions\bolt_app\ui\assets\images``. #. Add the image to the ``Image`` class. a. Open the file ``src\ansys\solutions\bolt_app\ui\utils\images.py``. b. Add the following line, making sure that the filename is the same as the name of the file you added: .. vale off .. code-block:: python :emphasize-lines: 8 class Image(Enum): """Enumeration of images used in the solution. If you for example need to change the name of the project title, you can change it here. """ ABOUT = "about.png" ABOUT_PLACEHOLDERS = "about-placeholders.png" BOLT = "bolt.png" .. vale on .. vale Google.Will = NO This adds a new image to the ``Image`` class so it can be used in the layout. .. vale Google.Will = NO .. vale on .. tip:: If you want to use the image in the tree view, you can add it to the ``Icons`` class, as described in the :ref:`camera example`. Update the tree view ---------------------- Add the Bolt app to the tree view and remove the default monitoring pages. #. Import the app layout. a. Open the file ``src/ansys/solutions/bolt_app/ui/pages/page.py``. b. Add the following line: .. vale off .. code-block:: python :emphasize-lines: 2 from ansys.solutions.bolt_app.ui.components import event_listeners from ansys.solutions.bolt_app.ui.customization import bolt_app from ansys.solutions.bolt_app.ui.pages import monitoring_page, problem_setup_page .. vale on .. note:: Note that ``create ansys.solutions.bolt_app.ui.customization.bolt_app`` isn't yet created. This happens later, when you :ref:`create the app layout `. #. Add the app to the tree view. Add a new item to the ``get_treeview_items`` function so the item is displayed in the navigation tree. For a more detailed explanation, see the :ref:`camera tutorial `. .. vale off .. code-block:: diff def get_treeview_items(project: Bolt_AppSolution) -> list[dict]: """Get the treeview items for the navigation tree.""" problem_setup_step_item = AwcDashTreeViewItem( id="problem_setup_step", text="Problem Setup", icon=Icon.SETTINGS, ) tree_items = [problem_setup_step_item] - osl_project_tree = DataAccess(project).get_project_tree() - - if not osl_project_tree.is_uninitialized: - tree_items.append(AwcDashTreeViewItem.from_actor_tree_node(osl_project_tree)) + if DataAccess(project).get_osl_instance_started(): + bolt_app_item = AwcDashTreeViewItem( + id="bolt_app", + text="Bolt App", + icon=Icon.SYSTEM, + ) + tree_items.append(bolt_app_item) return AwcDashTreeView(tree_items=tree_items).create() .. vale on #. Display the app when clicked. Update the ``get_page_layout_by_clicked_tree_item`` function to display the Bolt app when the item is clicked in the navigation tree. Check if the ``clicked_tree_item_id`` is ``bolt_app``. If so, it should return the app layout. .. vale off .. code-block:: python :lineno-start: 85 :emphasize-lines: 3, 19-20 :linenos: if DataAccess(project).get_osl_instance_started(): bolt_app_item = AwcDashTreeViewItem( id="bolt_app", text="Bolt App", icon=Icon.SYSTEM, ) tree_items.append(bolt_app_item) return AwcDashTreeView(tree_items=tree_items).create() def get_page_layout_by_clicked_tree_item(clicked: str | None, project: Bolt_AppSolution) -> html.Div: """Get the page layout for the clicked tree item.""" if clicked is None: return html.Div(html.H1("Welcome!")) elif clicked == "problem_setup_step": return problem_setup_page.layout(project) elif clicked == "bolt_app": return bolt_app.layout else: return monitoring_page.layout(selected_actor=clicked, project=project) .. vale on .. _bolt-create-app-layout: Create the app layout --------------------- Create the layout for the Bolt app. #. Create the app layout file. a. In the folder ``src/ansys/solutions/bolt_app/ui``, create a folder named ``customization``. b. In this folder, create a file named ``bolt_app.py``. c. Open the file for editing. #. Add the imports. Add the imports to be used in the app: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 3-11 :lineno-start: 3 :linenos: .. note:: If you use the case built from the supplied project, the namespace is ``ansys.solutions.bolt_app`` and the solution name is ``Bolt_AppSolution``. These names are derived from the name of the solution ``.awa`` file, so you need to adjust them when the solution has been created from a different optiSLang project or when you specified a different filename during the solution creation process. You can use the imports in ``src/ansys/solutions/bolt_app/ui/pages/page.py`` as a reference to find the correct names. #. Define the variables. Define the following variables to be used in the app: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 64-73 :lineno-start: 64 :linenos: #. Define the page text and image. On the next lines, some markdown text is defined in the ``text`` variable. This text is put into the layout using a ``dcc.Markdown`` --- in this case, with ``mathjax=True`` to support display of the included equations. The custom image defined previously is also included. Place these as the content on the right side of the layout: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 75-80 :lineno-start: 75 :linenos: #. Add the tables. In the customized app, there are to be two tables for each design point: **Parameters** and **Responses**. Create these tables using the standard ``dash_table.DataTable``: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 87-95 :lineno-start: 87 :linenos: .. note:: At the moment, these tables are just placeholders which don't contain any data. The data is to be filled in by callbacks at a later stage, as described in :ref:`bolt-add-callbacks`. #. Add the bar charts. In the customized app, there are to be two bar charts: **Stresses** (with stress response values) and **Forces** (with force values). Create these charts using the ``dcc.Graph`` class, then define a flex container so the charts are shown in a single row: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 97-106 :lineno-start: 97 :linenos: #. Place the content. Place the tables and the bar chart container as the content on the left side of the layout, with a **Data Tables** heading for the tables and a **Data Graphs** heading for the bar charts: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 108-116 :lineno-start: 108 :linenos: #. Add the page layout. Assemble the page layout using four ``div`` elements that contain the tables, charts, and other components: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 118-122 :lineno-start: 118 :linenos: .. _bolt-add-callbacks: Add the callbacks ------------------ The tables are now added to the layout, but they don't yet contain any data. Create the callbacks to fill in and update the data used in the tables. #. Add the parameters table callback. Create a callback to update the **Parameters** table: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 125-155 :lineno-start: 125 :linenos: About this code: * The callback is triggered by the ``WebsocketStreamListeners.ACTOR_DATA`` stream, which triggers when the actor data is updated on the backend. * Using the ``project``, a ``DataAccess`` object is created, which allows for retrieval of the optiSLang project tree and the design points. If the project tree is uninitialized, an empty table is returned. * The project is checked for an actor with the name "Sensitivity." If it is not found, an empty table is returned. There is also a check for whether there are states. (Normally, this is a list with a single element containing ``0``.) If there are no states, an empty table is returned. * Using the unique identifier and state of the actor, the design point data for the actor is obtained. Using this object, a dict with the data is obtained using the ``to_dict_by_states`` method. By specifying ``DesignFields.PARAMETERS`` it is ensured that only the values for the *parameters* are returned. * The design point data for the actor is obtained using the unique identifier and state of the actor. Using this object, a dict with the data is obtained using the ``to_dict_by_states`` method. Specifying ``DesignFields.PARAMETERS``, ensures that only the values for the *parameters* are returned. * The ``to_dict_by_states`` method returns a dict with the states as keys and the parameters as values. * Lastly, the dict is converted to a pandas data frame and returns it as a dict using the ``to_dict`` method with the argument ``orient="records"``. This returns a list of dict, which is the format expected by the ``DataTable``. This results in the following table: .. image:: /img/bolt_app_table_parameters.jpeg :width: 60% :align: center :alt: Screenshot of the parameters table #. Add the response table callback. Create a callback to update the **Response** table: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 158-192 :lineno-start: 158 :linenos: About this code: * This callback is very similar to the parameter table callback, except that it uses the ``DesignFields.RESPONSES`` argument to get the values for the responses. * Also, a new column with the units is added. This is obtained from the ``UNITS`` dict, using as keys the response names from ``get_field_names``. * A new column with the units is added. This is obtained from the ``UNITS`` dict, using as keys the response names from ``get_field_names``. This results in the following table, which has the units added to it: .. image:: /img/bolt_app_table_responses.jpeg :width: 60% :align: center :alt: Screenshot of the responses table #. Add the stress chart callback. Create a callback to update the bar chart with stress values: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 195-241 :lineno-start: 195 :linenos: About this code: * The first part, up until the creation of the ``design_points``, is similar to the previous callbacks. * From the ``design_p*oints`` the data is extracted, but this time the ``to_dict_by_keys`` method is used, which returns a dict with the response names as keys and the states as values. * For the charts, an entry is created for every field defined in ``STRESS_RESPONSES``, (here only ``f_ub``). * The data is extracted from the ``design_points``, but in this case, the ``to_dict_by_keys`` method is used to return a dict with the response names as keys and the states as values. The states are used as the *x* axis, and the values corresponding state is used for *y*. * The layout of the chart is defined with the title and tick marks. This results in the following chart. Note that for the response ``f_ub``, it has two states on the *x* axis and only one value per state. .. image:: /img/bolt_app_chart_stress.jpeg :width: 35% :align: center :alt: Screenshot of the stress chart #. Add the force chart callback. Create a callback to update the bar chart with force values: .. literalinclude:: tutorial_files/bolt_app.py :language: python :lines: 244-290 :lineno-start: 244 :linenos: About this code: * This callback is similar to the previous one, except that it uses the ``FORCE_RESPONSES`` list (``F_shear`` and ``F_tension``) to get the values for the forces. This results in the following chart. Note that for the responses ``F_shear`` and ``F_tension``, there are two states on the *x* axis and two values per state. .. image:: /img/bolt_app_chart_forces.jpeg :width: 35% :align: center :alt: Screenshot of the forces chart .. _full_code_bolt: Full code ----------- Here is the full code for the Bolt app after the customizations are complete: .. literalinclude:: tutorial_files/bolt_app.py :language: python :linenos: