Tips for maximizing scripting performance in Coreform Cubit

In our most recent webinar there were some questions about how to maximize the performance of Coreform Cubit scripts. While I included a few tips in a post a couple years ago that are still relevant I thought it would be good to provide an update / more thorough guidance. First, I’ll provide the list then I’ll show them in action:

The Tips List

  1. Precompute and reuse data to minimize Cubit-Python API calls.
    • Minimize redundant calls into the Cubit-Python API
  2. Minimize repeated calls into nested objects / functions
  3. Minimize usage of compress (e.g. move to outer-most loop)
  4. Turn off messages
    • Information: info off
    • Warnings: warning off – not as critical as info, but if your script generates a lot of warnings this is a good setting
  5. Turn off undo: undo off
  6. Use the Python time module to measure performance

Additional performance tips that are relevant if you’re using your script within the GUI application:

  1. Pause graphics: graphics off (then do graphics on to unpause)
    • Equivalently: graphics pausegraphics flush
  2. If pausing graphics is unacceptable, then at least ensure:
    • A coarse graphics faceting ( e.g. graphics tolerance angle 15 )
    • That you’re not using the transparent graphics mode

Simple Examples

Precompute and reuse data to minimize Cubit-Python API calls.

import numpy
import time

cubit.cmd( "reset" )
cubit.cmd( "bri x 1" )
cubit.cmd( "vol 1 size 0.025" )
cubit.cmd( "mesh vol 1" )

def do_something( node_coords ):
  val = numpy.sqrt( node_coords[0]**2 + node_coords[1]**2 + node_coords[2]**2 )
  return val

# DON'T DO THIS
t0 = time.perf_counter()
hex_id_list = cubit.get_entities( "hex" )
for hex_id in hex_id_list:
  hex_node_ids = cubit.get_connectivity( "hex", hex_id )
  for node_idx in range( 0, len( hex_node_ids ) ):
    node_coords = cubit.get_nodal_coordinates( hex_node_ids[node_idx] )
    do_something( node_coords )
t1 = time.perf_counter()
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

# DO THIS
t0 = time.perf_counter()
node_id_list = cubit.get_entities( "node" )
node_coords = {}
for node_id in node_id_list:
  node_coords[node_id] = cubit.get_nodal_coordinates( node_id )

for node_id in hex_node_ids:
  do_something( node_coords[node_id] )

t1 = time.perf_counter()
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

On my machine the first approach takes 1.25 seconds while the latter takes 0.1 seconds.

Minimize repeated calls into nested objects / functions

cubit.cmd( "reset" )
cubit.cmd( "create torus major radius 1 minor radius 0.1" )
cubit.cmd( "split periodic vol all" )
cubit.cmd( "Volume all copy move x 3 repeat 10" )
cubit.cmd( "Volume all copy move y 3 repeat 10" )
cubit.cmd( "Volume all copy move z 3 repeat 10" )

curve_id_list = cubit.get_entities( "curve" )

def dosomething( val ):
  return 2 * val

# DON'T DO THIS
t0 = time.perf_counter()
for cid in curve_id_list:
  val = cubit.curve( cid ).tangent( cubit.curve( cid ).position_from_u( 0.5 ) )
  thing = dosomething( val )
t1 = time.perf_counter()
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

# DO THIS
t0 = time.perf_counter()
for cid in curve_id_list:
  C = cubit.curve( cid )  # <--- CAPTURE THE CURVE IN A VARIABLE
  val = C.tangent( C.position_from_u( 0.5 ) )  # <--- MINIMAL NESTED CALLS
  thing = dosomething( val )
t1 = time.perf_counter()
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

On my machine the first approach takes 27 seconds while the latter takes 12.5 seconds.

Turn off messages

xyz = numpy.random.random( shape=(1000,3) )

# DON'T DO THIS
cubit.cmd( "reset" )
t0 = time.perf_counter()
for n in range( 0, xyz.shape[0] ):
  cubit.cmd( f"create vertex location {xyz[n,0]} {xyz[n,1]} {xyz[n,2]}" )
t1 = time.perf_counter()
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

# DO THIS
cubit.cmd( "reset" )
cubit.cmd( "info off" )  # <--- TURN 'INFORMATION' OFF
t0 = time.perf_counter()
for n in range( 0, xyz.shape[0] ):
  cubit.cmd( f"create vertex location {xyz[n,0]} {xyz[n,1]} {xyz[n,2]}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

On my machine the first approach takes 4 seconds while the latter takes 2 seconds.

Turn off undo

def do_setup():
  cubit.cmd( "reset" )
  cubit.cmd( "create torus major radius 1 minor radius 0.1" )
  cubit.cmd( "split periodic vol all" )
  cubit.cmd( "Volume all copy move x 3 repeat 10" )
  cubit.cmd( "Volume all copy move y 3 repeat 10" )
  torus_vols = cubit.get_entities( "volume" )
  cubit.cmd( "create brick bounding box Volume all tight" )
  target_vid = cubit.get_last_id( "volume" )
  center = cubit. Volume(target_vid).center_point()
  cubit.cmd( f"volume {target_vid} scale x 1.25 y 1.25 z 4 about {center[0]} {center[1]} {center[2]}" )

# DON'T DO THIS
cubit.cmd( "info off" ) # do this though
t0 = time.perf_counter()
for vid in torus_vols:
  target_vid = cubit.get_last_id( "volume" )
  cubit.cmd( f"subtract volume {vid} from volume {target_vid}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

# DO THIS
cubit.cmd( "info off" )
cubit.cmd( "undo off" )  # <--- TURN 'UNDO' OFF
t0 = time.perf_counter()
for vid in torus_vols:
  target_vid = cubit.get_last_id( "volume" )
  cubit.cmd( f"subtract volume {vid} from volume {target_vid}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

On my machine the first approach takes 21 seconds while the latter takes 8 seconds.

Pause graphics

def do_setup():
  cubit.cmd( "reset" )
  cubit.cmd( "create torus major radius 1 minor radius 0.1" )
  cubit.cmd( "split periodic vol all" )
  cubit.cmd( "Volume all copy move x 3 repeat 10" )
  cubit.cmd( "Volume all copy move y 3 repeat 10" )
  torus_vols = cubit.get_entities( "volume" )
  cubit.cmd( "create brick bounding box Volume all tight" )
  target_vid = cubit.get_last_id( "volume" )
  center = cubit. Volume(target_vid).center_point()
  cubit.cmd( f"volume {target_vid} scale x 1.25 y 1.25 z 4 about {center[0]} {center[1]} {center[2]}" )
  return torus_vols, target_vid

# DON'T DO THIS
cubit.cmd( "info off" )
cubit.cmd( "undo off" )
torus_vid, target_vid = do_setup()
t0 = time.perf_counter()
for vid in torus_vid:
  target_vid = cubit.get_last_id( "volume" )
  cubit.cmd( f"subtract volume {vid} from volume {target_vid}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

# DO THIS
cubit.cmd( "info off" )
cubit.cmd( "undo off" )
cubit.cmd( "graphics off" )  # <--- PAUSE GRAPHICS
torus_vid, target_vid = do_setup()
t0 = time.perf_counter()
for vid in torus_vid:
  target_vid = cubit.get_last_id( "volume" )
  cubit.cmd( f"subtract volume {vid} from volume {target_vid}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )
cubit.cmd( "graphics on" )  # <--- UNPAUSE GRAPHICS

On my machine the first approach takes 293 seconds while the latter takes 8 seconds.

Of course, with this simple example it would be even better to consider the first few tips… we can combine these many subtract operations with a single subtract operation:

def list_to_str( input_list ):
  return " ".join( [ str( val ) for val in input_list ] )

cubit.cmd( "info off" )
cubit.cmd( "undo off" )
cubit.cmd( "graphics on" )
torus_vid, target_vid = do_setup()
t0 = time.perf_counter()
cubit.cmd( f"subtract volume {list_to_str( torus_vid )} from {target_vid}" )
t1 = time.perf_counter()
cubit.cmd( "info on" )
print( f"ELAPSED TIME: {t1 - t0} SECONDS" )

Which then only takes approximately 0.005 seconds.

Use a coarse graphics tolerance

See this previous post

Summary

If this was helpful to you, if you have any other questions, or know of some other performance tips, leave a comment!