Python for MEP simplification

Scorri col mouse

Solution by Paolo Zecchini

In Volcano siamo abituati a trovare soluzioni efficaci alle richieste dei clienti e delle aziende per i quali svolgiamo consulenza, dall’ottimizzazione di workflow BIM fino allo sviluppo di soluzioni personalizzate. A volte capita che abbiano una classe di problemi che noi chiamiamo “scheggia”: piccoli all’apparenza, ma che possono portare a un disagio (inefficienza) costante e fastidiosa. Problemi che, se affrontati chirurgicamente – cioè cuciti sulle necessità del cliente -, permettono un netto miglioramento dell'”agio” professionale e dell'”umore” produttivo.

Il problema

Il problema è semplice e molto diffuso nella rielaborazione dei progetti MEP partendo dai CAD.
Nel caso specifico si tratta di poter modellare le tubazioni all’interno di progetti in cui è collegato un file CAD e nel quale la quota specificata è quella di scorrimento dell’acqua; non una cosa di semplice realizzazione dal momento che Autodesk Revit permette di posizionare questo tipo di oggetti usando la quota centrale del tubo o, al massimo, con la quota inferiore esterna o superiore esterna. La quota di posizionamento centrale del tubo è quindi quella letta nel file CAD con l’aggiunta di metà diametro interno, parametro di istanza della tubazione posizionata.
Questo porta a un flusso di lavoro inefficiente: è necessario modificare a mano le quote di tutti i tubi perché rispettino l’indicazione tratta dal CAD, ovvero lo scorrimento delle acque.

Il problema: il file CAD riporta la quota di scorrimento dell’acqua, indipendentemente da diametro e spessore della tubatura.

L’obiettivo

L’obiettivo è diventato quindi lo sviluppo di una soluzione personalizzata che permettesse di creare un modello MEP partendo da un CAD bidimensionale, utilizzando le quote indicate nel CAD (lo scorrimento acqua) e facendo calcolare i valori di posa in automatico al sistema.

Un modello MEP ricavato con Python da un file CAD e la quota di scorrimento dell’acqua.

La domanda

A questo punto la domanda che ci si è posti era se fosse possibile automatizzare questo processo in modo da semplificare il flusso di lavoro creando una maschera di input che aiutasse a lavorare più comodamente in Revit chi non ha dimestichezza con i sistemi di scripting (Dynamo) e programmazione (Python).

Una comoda maschera di input.

La soluzione

Spesso, come in questo caso, la soluzione al problema scheggia richiede un’elaborazione più articolata del previsto a causa delle limitazioni imposte dai software; sono situazioni nelle quali è necessario utilizzare una maggior dose di pensiero laterale, perché oltre a generare la soluzione è necessario che chi lo ha posto riesca imparare ad acquisire un metodo che gli permetta di affrontare la stessa classe di problemi in molti altri scenari. La soluzione stessa, quindi, dev’essere semplificata al massimo, limata, pulita, ottimizzata. In poche parole, la soluzione dev’essere sottoposta a un processo di design: togliere dettagli inutili e irrilevanti, per ridurre al minimo il numero di passaggi o di iterazioni. Cose da programmatori. Ma non tutti lo sono. Scripter con Dynamo magari si, basi di Python anche. Qualcosa sulle API di Revit. Ma niente di più, perché la soluzione dev’essere utilizzabile anche da chi non ha le skill da Coder di Google o Apple.

Tornando al caso affrontato, dato che Autodesk Revit posiziona i condotti con la quota centrale del tubo o con quelle inferiore esterna o superiore esterna, la quota di posizionamento centrale del tubo sarebbe quindi quella letta nel file CAD con l’aggiunta di metà diametro interno, parametro di istanza della tubazione posizionata.
Da qui deriva il paradosso: per poter posizionare il tubo è necessario aver già posizionato il tubo…
E questo è solo il primo problema. A questo si aggiunge la necessità di creare un apposito form in cui l’utente “non skillato” possa inserire i valori di altezza desiderati utilizzando metodi esterni alle API di Autodesk di Revit che, invece, richiederebbero la sottoscrizione al gestore degli eventi o la creazione di TextBox all’interno dell’interfaccia ribbon del programma.

Come sempre quando si scrive il codice di un Add-In, è fondamentale compilarlo e testarlo in un ambiente in cui il debug può avvenire in maniera immediata, nello specifico è utile sfruttare il nodo “Python Script” di Dynamo. Andando con ordine: perché lo script funzioni è necessario aggiungere al modulo clr (Common Language Runtime, il “traduttore” utilizzato dalle librerie .Net) la referenza alle API di Revit, quelle per la gestione del database, quelle per la gestione delle classi Plumbing e quelle per la gestione dell’interfaccia utente.

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Plumbing import *
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *

Per poter procedere è opportuno che il programma sappia su quale documento sta lavorando: è necessario quindi aggiungere al codice i comandi per ricavare gli oggetti dell’applicazione e del documento aperto riferiti sia alla parte di database sia a quella di interfaccia utente. Partendo dalla libraria delle API dei servizi di Revit, una libreria propria di Dynamo che permette di dialogare in lettura e scrittura con il software, è possibile ricavare:

  • l’oggetto Interfaccia Utente dell’Applicazione Aperta, quella dalla quale si è avviato il comando “Dynamo”, è tutti gli altri oggetti.
  • l’oggetto Applicazione Aperta.
  • l’oggetto Interfaccia Utente del Documento Aperto ed Attivo nella sessione dell’Applicazione Aperta.
  • l’oggetto Documento Aperto ed Attivo nella sessione dell’Applicazione Aperta.
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager as DM
from RevitServices.Transactions import TransactionManager as TM
ui_app = DM.Instance.CurrentUIApplication
app = ui_app.Application
ui_doc = ui_app.ActiveUIDocument
doc = ui_doc.Document

Dalla vista di pianta attiva, quella in cui è collegato il file CAD, viene ricavato il livello di base per la creazione delle tubazioni.

level = doc.ActiveView.GenLevel
level_id = level.Id

Per decidere la strada da intraprendere è ora necessario capire quali API Autodesk ha messo a disposizione per il posizionamento degli oggetti della classe tubazione. Navigando quindi nel corretto spazio del nomi, all’interno della classe Pipe, si trovano tre metodi per la creazione di una nuova tubazione:

  • Dati due connettori, uno iniziale e uno finale,
  • Dati un connettore iniziale e un punto finale
  • Dati due punti, uno iniziale e uno finale, e il sistema di tubazioni.

L’ultimo metodo, quello che fa al caso nostro, chiede la specifica del sistema di tubazioni poiché non è in grado di ereditarlo da nessun connettore.

Il metodo di creazione delle tubazioni.

Collegamento a https://www.revitapidocs.com/2020/9550265f-5760-3c28-d023-d0373285855b.htm

Oltre alla quota altimetrica da livello e al diametro della tubazione, si aggiungono alle informazioni da richiedere all’utente anche il Tipo di famiglia della tubazione ed il Tipo di sistema di tubazione. Tali informazioni devono essere inserite nel sistema; come specificato in apertura, si è scelto di non utilizzare le API di Autodesk Revit per l’input di dati, ma una più comoda finestra di input con Windows.Form .

Per poter usare questa libreria è necessario importarla all’interno del codice.

clr.AddReference('System')
clr.AddReference('System.Drawing')
clr.AddReference("System.Windows.Forms")
from System.Windows import Forms
from System.Windows.Forms import Application, Button, Form, Label
from System.Windows.Forms import CheckBox, DialogResult, TextBox
from System.Drawing import Point, Icon

Si può procedere quindi inizializzando una classe personalizzata per il WinForm definendo le cinque caselle (TextBox) di input ed il tasto di conferma che avvia l’aggiornamento dei dati memorizzati del form e li restituisce allo script sotto forma di lista di cinque valori, associata con la variabile “res” .

Sarà necessario convertire i primi tre input in float poiché il valore inserito sarà inteso dal form come una stringa testuale. Da tenere in considerazione che le API di Revit, operando direttamente sul codice del programma, lavorano con le unità interne allo stesso, che sono quelle imperiali (piedi, galloni, etc.) e che gli input definiti in metri o millimetri richiedono un’ulteriore conversione. Per praticità si sono quindi definite quattro funzioni di conversione, che convertono da unità interne a millimetri o metri e da millimetri o metri in unità interne.

def convert_m_from( x ):
return UnitUtils.ConvertFromInternalUnits( x , UnitTypeId.Meters )
def convert_mm_from( x ):
return UnitUtils.ConvertFromInternalUnits( x , UnitTypeId.Millimeters )
def convert_m_to( x ):
return UnitUtils.ConvertToInternalUnits( x , UnitTypeId.Meters )
def convert_mm_to( x ):
return UnitUtils.ConvertToInternalUnits( x , UnitTypeId.Millimeters )

Gli ultimi due valori invece saranno la chiave per la ricerca del sistema di tubazioni e del tipo di famiglia di tubazioni desiderato. Queste ultime sono entrambe famiglia di sistema, è quindi possibile ricercarne tutti i tipi con la ricerca tramite Collector per classe “xxxType” . Nel collector è poi possibile iterare con un ciclo for per ricercare l’esatta corrispondenza di nome del tipo e quindi assegnare l’elemento alle variabili piping_system_id e pipe_type_id .

piping_systems = FilteredElementCollector(doc).OfClass(PipingSystemType).ToElements()
for p in piping_systems:
name = p.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM).AsString()
if name == res[3]: piping_system_id = p.Id
pipe_types = FilteredElementCollector(doc).OfClass(PipeType).ToElements()
for t in pipe_types:
name = t.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM).AsString()
if name == res[4]: pipe_type_id = t.Id

Per il posizionamento dei tubi l’ultima cosa che ancora rimane ignota sono i punti iniziale e finale. Grazie all’interfaccia utente del documento aperto e attivo è possibile, utilizzando la classe Selection dello spazio dei nomi Autodesk.Revit.UI.Selection, selezionare i punti di inizio e di fine del tubo. La selezione avverrà nel piano di lavoro della vista in cui la selezione è effettuata: in questo caso sarà quindi effettuata in corrispondenza del livello associato alla vista di pianta attiva e ne erediterà la quota altimetrica. Nel caso specifico, inoltre, si è scelto di limitare gli snap ai punti finali delle linee.

snap = Selection.ObjectSnapTypes.Endpoints

start_point = ui_doc.Selection.PickPoint(snap, 'Seleziona punto iniziale') end_point =
ui_doc.Selection.PickPoint(snap, 'Seleziona punto finale

Una volta ricavati i due punti iniziale e finale non resta che creare la tubazione vera e propria. Come detto in precedenza, il paradosso della quota di scorrimento interna è che la tubazione eredita le proprietà di diametro interno. Per poter ovviare a questo limite abbiamo è stato necessario una strategia semplice ma efficace:

  1. Creazione della tubazione dal punto iniziale a quello finale, punti ottenuti direttamente come output del metodo PickPoint precedente, e quindi senza alcuna differenza altimetrica tra loro poiché legati alla quota altimetrica del livello.
  2. Impostazione del valore di diametro nominale.
  3. Lettura del valore di diametro esterno che il valore di diametro interno.
  4. Associazione dei valori dei diametri a due variabili.
  5. Eliminazione dal progetto della tubazione (poiché posizionata non correttamente).
  6. Creazione dei punti di inizio e di fine con quota altimetrica corretta.
  7. Creazione del tubo in posizione corretta.

Tutto il codice deve essere incapsulato all’interno di una Transazione per fare in modo che Dynamo (o qualunque altro plug-in) esterno possa dialogare con il database di Revit. A scelta dello sviluppatore può essere usato il TransactionManager dei RevitServices oppure la classe Transaction propria della libreria delle API di Revit. Per la compilazione del codice abbiamo scelto inoltre di procedere alla selezione dei parametri utilizzando la definizione interna degli stessi, come BuiltInParameter, in modo che la lingua di esecuzione di Autodesk Revit non influenzi negativamente l’esecuzione dello script: è risaputo, infatti, che il software traduce automaticamente i nomi dei parametri incorporati nella lingua in cui è eseguito e la ricerca per nome dei parametri (con il metodo LookupParameter) ne verrebbe influenzata se non inficiata.

t = Transaction(doc, 'Crea tubazione da quote di scorrimento')
t.Start()

# Creazione della tubazione placeholder
pipe = Pipe.Create(doc, piping_system_id, pipe_type_id, level_id, start_point, end_point)

# Impostazione del diametro nominale corretto
pipe.get_Parameter(BuiltInParameter.RBS_PIPE_DIAMETER_PARAM).Set(convert_mm_to(float(res[2])))

# Lettura e salvataggio dei valori di diametro esterno ed interno
outer_d
= pipe.get_Parameter(BuiltInParameter.RBS_PIPE_OUTER_DIAMETER).AsDouble()

inner_d
= pipe.get_Parameter(BuiltInParameter.RBS_PIPE_INNER_DIAM_PARAM).AsDouble()
sp = 0.5 * (outer_d - inner_d)

# Eliminazione del tubo placeholder
doc.Delete(pipe.Id)

Per la creazione dei nuovi punti ci si serve della classe XYZ che permette di definire punti e vettori. Le coordinate X e Y dei nuovi punti sono le medesime di quelle dei vecchi punti mentre la quota altimetrica centrale della tubazione è pari al valore ereditato dal vecchio punto (pari alla quota altimetrica del livello) aumentato del valore inserito con il WinForm e di metà del diametro interno.

# Creazione di nuovo punto di inizio
start_point_ok
= XYZ(start_point.X, start_point.Y, start_point.Z + convert_m_to( float ( res[0] ) ) + inner_d /2)

# Creazione di nuovo punto di fine
end_point_ok
= XYZ(end_point.X, end_point.Y, end_point.Z + convert_m_to( float ( res[1] ) ) + inner_d /2)

# Creazione della tubazione corretta
pipe_ok = Pipe.Create(doc, piping_system_id, pipe_type_id, level_id, start_point_ok, end_point_ok)

# Impostazione del diametro nominale corretto
pipe_ok.get_Parameter(BuiltInParameter.RBS_PIPE_DIAMETER_PARAM).Set(convert_mm_to(float(res[2])))
t.Commit()

A questo punto è possibile associare la tubazione appena creata all’output.

OUT = pipe_ok

Quando, dopo una attenta fase di debug, lo script viene eseguito senza restituire criticità, è possibile passarlo in PyRevit per lo sviluppo dell’Add-In, creando il pushbutton che attiva la creazione della tubazione senza la necessità di eseguire il codice tramite Dynamo.

Il risultato

Quello che è stato ottenuto è un workflow semplice e intuitivo per il posizionamento delle tubazioni partendo da un file CAD e utilizzando la quota di scorrimento dell’acqua.

Il file CAD di partenza importato in Autodesk Revit.
L’attivazione dell’Add-in sviluppato con Python.
Compilazione dei dati nell’apposito form.
Selezione diretta sul CAD del primo punto di posa della tubazione.
Selezione diretta sul CAD del secondo punto di posa.
La tubazione creata dall’Add-in in Autodesk Revit, vista in pianta.
La tubazione creata dall’Add-in in Autodesk Revit, vista in sezione.
L’altezza indicata sul CAD è quella dello scorrimento acque.
Con l’Add-in è ora possibile creare tutte le altre tubazioni.

Conclusioni

Soluzioni come questa, che fanno gran uso del pensiero laterale, permettono non solo di ovviare i limiti del software ma di sfruttarne i limiti traendone vantaggio: creando la prima istanza di tubazione vengono lette le informazioni necessarie da un elemento che sarà poi eliminato, permettendo la creazione di un altro che non sarà manipolato, riducendo quindi al minimo la possibilità di errori nel risultato finale.

Il risultato è quello che chiamiamo una “Soluzione Volcano”: non solo porre rimedio al problema ma rendere il problema stesso la miglior soluzione.

Paolo Zecchini
Official Instructor & BIM Coordinator Volcano