RAM Elements and pyRevit

Our team of structural engineers sometimes is required to report the base reaction of fairly complicated structural models to third-parties. We primarily use RAM Elements for these models, and if there exists a Revit model for the project that the RAM model is based on, it can be a tedious task to create a sheet neatly showing all the nodal coordinates, numbers, and reactions. Of course, we could just send the RAM output directly but that is never formatted cleanly and is sometimes difficult to relate back to a well understood Revit model.

I sought out to create a tool to read in a RAM model, create keynotes at the locations of all base reactions with the corresponding node number, and create a schedule based on a chosen load case/combo with the reactions at each node. After some research, it turns out that a RAM Element's model file (.retx) is an sqlite database under the hood. You can test this out by tossing in your favorite .retx to an online sqlite reader such as this one. Interestingly enough, you'll notice that the database entries are mostly in Spanish. 

In order for any of the code within this post to be of use to anyone, the below context needs to be understood:

  1. We have a family file called 2D-RAM-Nodes.rfa located on a network drive with a path resembling:
    • network_drive->Structural->Drawing Tools->Revit->Families->2D-RAM-Nodes.rvt
  2. The above mentioned family file has a parameter KEY tied to a label which is updated to display the node number.
  3. The following transformations are required between RAM/Revit:
    1. RAM Element's X-axis translates directly to Revit's X-axis
    2. RAM Element's Y-axis translates directly to Revit's Z-axis
    3. RAM Element's Z-axis translates to the inverse of Revit's Y-Axis
  4. For the purposes of reporting nodal reactions in the schedule, RAM Element's axis notation is used directly. This may require an axis legend shown on the relevant sheet.
  5. I have tweaked the output schedule row spacing/header shading/etc to match a format I consider to look good when sheeted up, but YMMV.

The general outline of the script operation is as such:

  1. Verify the currently active view is a Plan View.
  2. Verify that the 2D-RAM-Nodes.rfa file can be found.
  3. Asks the user to select a RAM Elements model file (.retx).
  4. Ask the user to select the alignment point between RAM/Revit.
  5. Create the 2D-RAM-Nodes.rfa family at all locations that have base reactions and change the KEY parameter to that nodes number.
  6. Ask the user if they want to create a schedule based on the input .retx file.
  7. Prompt the user to name the new schedule using the .retx file name as default. 
  8. If the number of base reactions exceeds 30, prompt the user that multiple sections may be required and ask for a maximum number of nodes per section. 
  9. Prompt the user to select what load case or combination to use as the basis for the reactions input to the schedule.
  10. Create the schedule and close the transaction group.

A preview of the tool is shown below:

script.py

from pyrevit import revit, DB
from pyrevit import forms
from pyrevit import HOST_APP
from pyrevit import script

import os
import sqlite3
import math

app = HOST_APP.uiapp.Application
doc = revit.doc
uidoc = revit.uidoc

logger = script.get_logger()

familyPath = r"network_drive\Structural\Drawing Tools\Revit\Families\2D-RAM-Nodes.rfa"
familySymbolName = "2D-RAM-Nodes"

def dict_factory(cursor, row):
    fields = [column[0] for column in cursor.description]
    return {key: value for key, value in zip(fields, row)}
    
class nodeData:
    def __init__(self, number, x, y, z, fx=None, fy=None, fz=None, mx=None, my=None, mz=None):
        self.number = number
        self.x = x
        self.y = y
        self.z = z
        self.fx = fx
        self.fy = fy
        self.fz = fz
        self.mx = mx
        self.my = my
        self.mz = mz

activeView = doc.ActiveView

if "Plan" not in str(activeView.ViewType):
    forms.alert("Active view must be a plan view...", title="DB Tools", exitscript=True)

if not os.path.exists(familyPath):
    forms.alert("Error locating 2D-RAM-Nodes family from network...", title="DB Tools", exitscript=True)

retxPath = forms.pick_file(title="Select a .retx file...", file_ext="retx", restore_dir=True, multi_file=False)
nodes = []

if retxPath is not None:
    con = sqlite3.connect(os.path.normpath(retxPath))

    con.row_factory = dict_factory
    cur = con.cursor()
    cur.execute("SELECT * FROM 'Nudos'")
    rows = cur.fetchall()

    for row in rows:
        if row.get("NudosRestric2") == 1:
            nodes.append(nodeData(row.get("NudosID"),
                         int(0) if row.get("NudosX") is None else int(float(row.get("NudosX"))),
                         int(0) if row.get("NudosY") is None else int(float(row.get("NudosY"))),
                         int(0) if row.get("NudosZ") is None else int(float(row.get("NudosZ")))))

    with forms.WarningBar(title="Pick origin to align RAM nodes..."):
        origin = uidoc.Selection.PickPoint()

    with revit.TransactionGroup("DB Tools - Apply RAM Nodes To Plan...") as tg:
        collector = DB.FilteredElementCollector(doc)\
                    .OfCategory(DB.BuiltInCategory.OST_GenericAnnotation)\
                    .OfClass(DB.FamilySymbol)

        familyTypeItr = collector.GetElementIdIterator()
        familyTypeItr.Reset()

        familySymbol = None
        for item in familyTypeItr:
            if getattr(getattr(doc.GetElement(item), "Family", None), "Name", "") == familySymbolName:
                familySymbol = doc.GetElement(item)
                
        if familySymbol is None:
            with revit.Transaction("DB Tools - Apply RAM Nodes To Plan - Loading Family", log_errors=False) as t:
                doc.LoadFamily(familyPath)

                familyTypeItr = collector.GetElementIdIterator()
                familyTypeItr.Reset()

                for item in familyTypeItr:
                    if getattr(getattr(doc.GetElement(item), "Family", None), "Name", "") == familySymbolName:
                        familySymbol = doc.GetElement(item)
                
            if t.status != DB.TransactionStatus.Committed:
                forms.alert("Error loading 2D-RAM-Nodes family from network...", title="DB Tools", exitscript=True)

        with revit.Transaction("DB Tools - Apply RAM Nodes To Plan", log_errors=False) as t:
            for node in nodes:
                if not familySymbol.IsActive:
                    familySymbol.Activate()

                loc = DB.XYZ(origin[0]+node.x, origin[1]+(node.z*-1), origin[2]+node.y) 
                familyInst = doc.Create.NewFamilyInstance(loc, familySymbol, activeView)

                element = doc.GetElement(familyInst.Id)
                revit.query.get_param(element, "KEY").Set(node.number)
            
    if tg.status != DB.TransactionStatus.Committed:
        forms.alert("Unknown error. Aborting...", title="DB Tools")  
    elif tg.status == DB.TransactionStatus.Committed:
        result = forms.alert(
            "Node tags applied. Do you want to create a schedule with node reactions?",
            title="DB Tools",
            options = ["Yes", "No"])
        if result == "Yes":
            with revit.Transaction("DB Tools - Apply Schedule...", log_errors=False) as t:
                schedTitle = forms.GetValueWindow.show(
                    None,
                    value_type="string",
                    default=str(os.path.splitext(os.path.basename(retxPath))[0]).upper(),
                    prompt="Enter schedule name:",
                    title="Apply RAM Nodes To Plan")

                max_rows = len(nodes)

                if max_rows >= 30:
                    max_rows = forms.GetValueWindow.show(
                        None,
                        value_type="slider",
                        default=30,
                        interval=5,
                        max=120,
                        title="Enter a max number of nodes per section...",
                        prompt="Input file has more than 30 reaction nodes which may need to be broken up on the sheet. Please enter a maximum number of rows per section.")

                    max_rows = int(max_rows)
                
                cur.execute("SELECT PropertyValue FROM 'ModelData_LoadConditions' WHERE PropertyName = 'EstadosDescripcion'")

                conditions = cur.fetchone().values()[0]

                conditions = [i[1] for i in enumerate(conditions[1:-1].split(","))]

                cur.execute("SELECT PropertyValue FROM 'ModelData_LoadConditions' WHERE PropertyName = 'EstadosEsComb'")
                iscombo = cur.fetchone().values()[0]
                iscombo = [i[1] for i in enumerate(iscombo[1:-1].split(","))]
                iscombo = list(map(lambda x: x.replace("0", "Case"), iscombo))
                iscombo = list(map(lambda x: x.replace("1", "Combination"), iscombo))

                conditions_dict = {iscombo[k]: [conditions[x] for x in range(len(conditions)) if iscombo[x] == iscombo[k]] for k in range(len(iscombo))}

                result = forms.SelectFromList.show(conditions_dict, title="Select a load condition...")

                selected_condition = conditions.index(result)

                options = ["Node #", "Node X", "Node Y", "Node Z", "Force X", "Force Y", "Force Z", "Moment X", "Moment Y", "Moment Z"]

                headers = forms.SelectFromList.show(options, title="Select schedule information...", multiselect=True)
                
                query = "SELECT * FROM 'Nudos_Estados_Result' WHERE Estados IS " + str(selected_condition)

                cur.execute(query)

                rows = cur.fetchall()

                for row in rows:
                    if row.get("Nudos") is None:
                        continue
                    
                    if any(x.number == row.get("Nudos") for x in nodes):
                        index = [x.number for x in nodes].index(row.get("Nudos"))

                        nodes[index].fx = float(0.0) if row.get("NudosReacc1") is None else round(float(row.get("NudosReacc1")), 2)
                        nodes[index].fy = float(0.0) if row.get("NudosReacc2") is None else round(float(row.get("NudosReacc2")), 2)
                        nodes[index].fz = float(0.0) if row.get("NudosReacc3") is None else round(float(row.get("NudosReacc3")), 2)
                        nodes[index].mx = float(0.0) if row.get("NudosReacc4") is None else round(float(row.get("NudosReacc4")), 2)
                        nodes[index].my = float(0.0) if row.get("NudosReacc5") is None else round(float(row.get("NudosReacc5")), 2)
                        nodes[index].mz = float(0.0) if row.get("NudosReacc6") is None else round(float(row.get("NudosReacc6")), 2)    

                con.close()

                sched = DB.ViewSchedule.CreateSchedule(doc, DB.ElementId.InvalidElementId)
                sched.Name = schedTitle
                sched.Definition.AddField(next((x for x in sched.Definition.GetSchedulableFields() if (x.GetName(doc) == "Assembly Code")), None))

                fieldId = sched.Definition.GetFieldId(0)

                filter1 = DB.ScheduleFilter(fieldId, DB.ScheduleFilterType.Equal)
                filter1.SetValue("NO VALUES FOUND")
                filter2 = DB.ScheduleFilter(fieldId, DB.ScheduleFilterType.Equal)
                filter2.SetValue("ALL VALUES FOUND")

                sched.Definition.AddFilter(filter1)
                sched.Definition.AddFilter(filter2)

                sched.Definition.ShowHeaders = False

                table_data = sched.GetTableData()

                num_sections = int(math.ceil(float(len(nodes)) / float(max_rows)))
                col_width = 0.75
                row_height = 0.18

                table_data.Width = num_sections*float((len(headers)*col_width)/12.0)
                section_data = table_data.GetSectionData(DB.SectionType.Header)
                section_data.InsertRow(section_data.LastRowNumber + 1)

                header_style = DB.TableCellStyle()
                header_style.BackgroundColor = DB.Color(217, 217, 217)
                header_style.IsFontBold = True
                header_style.TextSize = 6.75

                header_style_override = DB.TableCellStyleOverrideOptions()
                header_style_override.BackgroundColor = True
                header_style_override.Bold = True
                header_style_override.FontSize = True
                
                header_style.SetCellStyleOverrideOptions(header_style_override)

                data_style = DB.TableCellStyle()
                data_style.BackgroundColor = DB.Color(254, 255, 255)
                data_style.TextSize = 6.75

                data_style_override = DB.TableCellStyleOverrideOptions()
                data_style_override.BackgroundColor = True
                data_style_override.FontSize = True
                
                data_style.SetCellStyleOverrideOptions(data_style_override)
                
                section_data.SetColumnWidth(section_data.LastColumnNumber, float(col_width/12.0))
                section_data.SetCellText(section_data.LastRowNumber, section_data.LastColumnNumber, str(headers[0]).upper())
                section_data.SetCellStyle(section_data.LastRowNumber, section_data.LastColumnNumber, header_style)

                for k in range(num_sections):
                    if k != 0:
                        section_data.InsertColumn(section_data.LastColumnNumber + 1)                
                        section_data.SetColumnWidth(section_data.LastColumnNumber, float(col_width/12.0)/4.0)
                        section_data.SetCellStyle(section_data.LastRowNumber, section_data.LastColumnNumber, data_style)            
                        
                    for i, header in enumerate(headers):
                        if (i == 0 and k == 0):
                            continue
                        
                        section_data.InsertColumn(section_data.LastColumnNumber + 1)
                        section_data.SetColumnWidth(section_data.LastColumnNumber, float(col_width/12.0))
                        section_data.SetCellText(section_data.LastRowNumber, section_data.LastColumnNumber, str(header).upper())
                        section_data.SetCellStyle(section_data.LastRowNumber, section_data.LastColumnNumber, header_style)


                title_merge = DB.TableMergedCell(section_data.FirstRowNumber, section_data.FirstColumnNumber, section_data.FirstRowNumber, section_data.LastColumnNumber)

                section_data.MergeCells(title_merge)

                title_style = DB.TableCellStyle()
                title_style.BackgroundColor = DB.Color(254, 255, 255)
                title_style.IsFontBold = True
                title_style.TextSize = 10.125

                title_style_override = DB.TableCellStyleOverrideOptions()
                title_style_override.BackgroundColor = True
                title_style_override.Bold = True
                title_style_override.FontSize = True
                
                title_style.SetCellStyleOverrideOptions(title_style_override)

                section_data.SetCellText(section_data.FirstRowNumber, section_data.FirstColumnNumber, schedTitle)
                section_data.SetCellStyle(section_data.FirstRowNumber, section_data.FirstColumnNumber, title_style)

                section_data.SetRowHeight(section_data.FirstRowNumber, float((row_height*1.5)/12.0))
                section_data.SetRowHeight(section_data.FirstRowNumber+1, float((row_height)/12.0))  

                for j in range(max_rows):
                    section_data.InsertRow(section_data.LastRowNumber + 1)
                    section_data.SetRowHeight(section_data.LastRowNumber, float(row_height/12.0))

                    for k in range(num_sections-1):
                        section_data.SetCellStyle(section_data.LastRowNumber, section_data.FirstColumnNumber+len(headers)+(k*(len(headers)+1)), data_style)           
                    
                for k in range(num_sections):
                    for j in range(k*max_rows, min((k+1)*max_rows, len(nodes))):
                        for i, header in enumerate(headers):
                            if header == "Node #":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(nodes[j].number))
                            elif header == "Node X":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(nodes[j].x))
                            elif header == "Node Y":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(nodes[j].y))
                            elif header == "Node Z":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(nodes[j].z))
                            elif header == "Force X":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].fx is None else nodes[j].fx))
                            elif header == "Force Y":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].fy is None else nodes[j].fy))
                            elif header == "Force Z":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].fz is None else nodes[j].fz))
                            elif header == "Moment X":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].mx is None else nodes[j].mx))
                            elif header == "Moment Y":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].my is None else nodes[j].my))
                            elif header == "Moment Z":
                                section_data.SetCellText((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), str(0.0 if nodes[j].mz is None else nodes[j].mz))
                                
                            section_data.SetCellStyle((section_data.FirstRowNumber+2)+j-(k*max_rows), section_data.FirstColumnNumber+i+(k*(len(headers)+1)), data_style)
            if t.status != DB.TransactionStatus.Committed:
                forms.alert("Unknown error. Aborting...", title="DB Tools")  

Let me know if anyone has any comments or criticisms! 

AK