Converting a Python Result to an ACT Extension

Options
Pierre Thieffry
Pierre Thieffry Member, Moderator, Employee Posts: 98
First Anniversary Ansys Employee Solution Developer Community of Practice Member Photogenic
edited February 23 in Structures

I recently got the request of converting a Python result a user had created to an extension.

Why would you want to do that? Well, simply to more easily share your work with your colleagues. The Python Result can be used to create a prototype of a specific post-processing, but might be harder to share, especially if you have details properties associated to it.

Below I'm showing you how to go from one to the other, with the necessary pieces of code. In this example, the Python Result has two properties: a float input to get the display time and a dropdown with a list of named selections to choose from. It will then display the deformation on the selection at the specified time - nothing complex, but that's not the purpose of this discussion. Also, the Python Result has two additional properties to provide the mean and maximum value of the result being displayed.

Let's start with the Python Results' property provider where you define the properties to be shown in the details view. This is the code you would put under the "Property Provider" tab of your Python Result.

def reload_props():
    this.PropertyProvider = None

    # comment the following return to allow the rest of the function definition to be executed and add properties
    # return

    """
    Some sample code is provided below that shows how to:
        1. Create an instance of the Provider. The Provider class is used to add custom properties to the details.
        2. Use the Provider instance to add custom properties.
        3. Configure those custom properties.
    """

    # Create the property instance
    provider = Provider()

    # Create a group named Group 1.
    group = provider.AddGroup("My Properties")

    # Create a property with control type Expression and a property with control type Double, and add it to the Group 1
    # Property for seleciton of a time value
    time_prop = group.AddProperty("Display Time", Control.Double)
    # DropDown to select a named selection
    selection_prop = group.AddProperty("Named selection", Control.Options)
    # Retrieve all named selections

# Fill the dropdown with the list of named selections
  all_named_sel=DataModel.GetObjectsByType(DataModelObjectCategory.NamedSelection)
    ns_list={}
    idx=1
    for ns in all_named_sel:
        ns_list[idx]=ns.Name
        idx+=1
    selection_prop.Options = ns_list

    # Second group for some results
    group2 = provider.AddGroup("My Results")
    # Mean value
    mean_prop = group2.AddProperty("Mean value", Control.Double)
    mean_prop.ParameterType = ParameterType.Output
    # Maximum value
    max_prop = group2.AddProperty("Maximum value", Control.Double)
    max_prop.ParameterType = ParameterType.Output



    # Connects the provider instance back to the object by setting the PropertyProvider member on this, 'this' being the 
    # current instance of the Python Code object.
    this.PropertyProvider = provider

And here's the script part of the Python Result:

def post_started(sender, analysis):# Do not edit this line
    define_dpf_workflow(analysis)

# Uncomment this function to enable retrieving results from the table/chart
# def table_retrieve_result(value):# Do not edit this line
    # import mech_dpf
    # import Ans.DataProcessing as dpf
    # wf = dpf.Workflow(this.WorkflowId)
    # wf.Connect('contour_selector', value)
    # this.Evaluate()

def define_dpf_workflow(analysis):
    import mech_dpf
    import Ans.DataProcessing as dpf
    mech_dpf.setExtAPI(ExtAPI)
    dataSource = dpf.DataSources(analysis.ResultFileName)
    u = dpf.operators.result.displacement()
    nrm = dpf.operators.math.norm_fc(u)


    # Get properties
    time=float(this.GetCustomPropertyByPath("My Properties/Display Time").Value)
    selection_name=this.GetCustomPropertyByPath("My Properties/Named selection").ValueString

    # Get selection corresponding to user choice
    if selection_name!='':
        scoping_op=dpf.operators.scoping.on_named_selection(data_sources=dataSource,named_selection_name=selection_name.ToUpper())
        scoping=scoping_op.outputs.getmesh_scoping()
        u.inputs.mesh_scoping.Connect(scoping)

    u.inputs.data_sources.Connect(dataSource)
    u.inputs.time_scoping.Connect(time)

    # Set output results
    max=dpf.operators.min_max.min_max(nrm)
    maxval=max.outputs.getfield_max().Data[0]

    nrmdata=nrm.outputs.getfields_container()[0].Data
    mean=sum(list(nrmdata))/len(nrmdata)
    this.GetCustomPropertyByPath("My Results/Mean value").Value=mean
    this.GetCustomPropertyByPath("My Results/Maximum value").Value=maxval


    dpf_workflow = dpf.Workflow()
    dpf_workflow.Add(u)
    dpf_workflow.Add(nrm)
    # dpf_workflow.SetInputName(u, 0, 'time')
    # dpf_workflow.Connect('time', timeScop)
    dpf_workflow.SetOutputContour(nrm)
    dpf_workflow.Record('wf_id', False)
    this.WorkflowId = dpf_workflow.GetRecordedId()

Now read the next comment to see how this is transformed into an ACT extension.

Comments

  • Pierre Thieffry
    Pierre Thieffry Member, Moderator, Employee Posts: 98
    First Anniversary Ansys Employee Solution Developer Community of Practice Member Photogenic
    Options

    Let's look at the conversion. An ACT extension in its scripted form is made at minimum from a XML file that describes how an object looks like and a Python File with callback functions.

    A callback function is a function called upon a certain event triggered by the user. In our case, we'll have three of them: one when the user inserts the result under Solution (createNewResult), one when the user activates the new object (fillNamedSelections) and one when the user evaluates the object (evaluateNewResult).

    Let's start with the XML File

    <extension version="1" name="New Result">
    
      <guid shortid="New Result">8fe89783-7da1-4679-b2cf-7f381f2434e9</guid>
    
      <script src="new_result.py" />
    
      <interface context="Mechanical">
    
        <images>images</images>
    
        <!-- Create a toolbar and add a button to insert a ACT load -->
        <toolbar name="New Result" caption="Conversion from Python Result">
          <entry name="New Result" icon="results">
            <callbacks>
              <!-- This will add the ACT object to the tree -->
              <onclick>createNewResult</onclick>
            </callbacks>
          </entry>
        </toolbar>
      </interface>
    
        <simdata context="Mechanical">
                <result name="new_result" version="1" caption="New Result" icon="result" location="node" type="scalar" timehistory="False">
                    <callbacks>
                        <evaluate>evaluateNewResult</evaluate>                  
                    </callbacks>
                    <property name="Geometry" caption="Geometry" control="scoping"></property>              
                    <property name="NamedSelectionChoice" caption="Named Selection" control="select" default="Margin">                    
                        <callbacks>
                            <onactivate>fillNamedSelections</onactivate>
                        </callbacks>
                    </property>
                    <property name="MeanValue" caption="Mean Value" control="float" isparameter="True" readonly="True"></property>
                    <property name="MaximumValue" caption="Maximum Value" control="float" isparameter="True" readonly="True"></property>                               
                </result>
            </simdata>
    
    </extension>
    

    In this file, we define:

    • <Interface>: the context we are in (Mechanical), The Toolbar that will host our new button and what happens when we click on this button (<onclick> callback)
    • <simdata>: we define a new result (so Mechanical knows where to put the object) and its properties and callbacks. As you can see, we define a selection list (for the named selections) and two properties, Mean Value and Maximum Value. But no display time! This is indeed a default property of a result object we don't have to define again. The callbacks are <onactivate> to fill the list of selections when the object is displayed and <evaluate> when the user evaluates the result

    Now on to the companion Python file. Note this file must be placed in a folder with the same name as your xml file. So your file structure will look like this (note the Python filename matching the <script src> in the xml file:

    extension.xml
    extension (folder)
    -- new_result.py

    And here is the code. I copied it from the Python Results script. I have added comments to show what you need to modify to make it work as an extension. The two biggest differences are in the way results are collected for display (dpf Worfklow for the Python result vs collector in an extension) and the way properties are set or retrieved.

    import units
    import math
    import os
    
    def createNewResult(analysis):
        """
           The method is called when the toolbar button is clicked.
    
           Keyword arguments:
           analysis -- the active analysis
        """
    
        # Add a "weldplot" object [defined in XML] in solution tree of the analyis.
        analysis.CreateResultObject("new_result", ExtAPI.ExtensionManager.CurrentExtension)
    
    def fillNamedSelections(result,property):
        named_selections = ExtAPI.DataModel.Components
        property.Options.Clear()
        for ns in named_selections:
            result.Properties["NamedSelectionChoice"].Options.Add(ns.Name)    
    
    
    def evaluateNewResult(result,stepInfo,collector):
        """
           The method is called when the "Evaluate" action button is clicked or
           from the generateChildren method.
    
           Keyword arguments:
           obj -- the object to avaluate
        """
        reader = result.Analysis.GetResultsData()       
    
    
        ##### Modification to Python result
        # Define analysis
        analysis=result.Analysis
    
        # Code copied from Python result
        import mech_dpf
        import Ans.DataProcessing as dpf
        mech_dpf.setExtAPI(ExtAPI)
        dataSource = dpf.DataSources(analysis.ResultFileName)
        u = dpf.operators.result.displacement()
        nrm = dpf.operators.math.norm_fc(u)
    
    
    
    
        ##### Modification to Python result
        # Get Properties
        #### Was this:
        ## Get properties
        #time=float(this.GetCustomPropertyByPath("My Properties/Display Time").Value)
        #selection_name=this.GetCustomPropertyByPath("My Properties/Named selection").ValueString
    
        #### Becomes:
        time=stepInfo.Time
        selection_name=result.Properties["NamedSelectionChoice"].Value
    
        ExtAPI.Log.WriteMessage(str(selection_name))
    
        # Get selection corresponding to user choice
        if selection_name!='':
            scoping_op=dpf.operators.scoping.on_named_selection(data_sources=dataSource,named_selection_name=selection_name.ToUpper())
            scoping=scoping_op.outputs.getmesh_scoping()
            u.inputs.mesh_scoping.Connect(scoping)
    
            u.inputs.data_sources.Connect(dataSource)
            u.inputs.time_scoping.Connect(time)
    
            # Set output results
            max=dpf.operators.min_max.min_max(nrm)
            maxval=max.outputs.getfield_max().Data[0]
    
            nrmdata=nrm.outputs.getfields_container()[0].Data
            mean=sum(list(nrmdata))/len(nrmdata)
    
            ##### Modification to Python result
            #### Was this:
            #this.GetCustomPropertyByPath("My Results/Mean value").Value=mean
            #this.GetCustomPropertyByPath("My Results/Maximum value").Value=maxval
            #### Becomes:
            result.Properties["MeanValue"].Value=mean
            result.Properties["MaximumValue"].Value=maxval
    
    
            ##### Modification to Python result
            ### Remove all "Workflow related lines" and replace with this
            # Get field to display
            nrm_field=nrm.outputs.getfields_container()[0]
            # Retrieve data and node ids
            node_ids=nrm_field.ScopingIds
            nrm_data=nrm_field.Data
    
            # Fill Collector for Mechanical to display values
            for i in range(len(node_ids)):
                collector.SetValues(node_ids[i],[nrm_data[i]])