To access the tutorial projects in ArcGIS CityEngine, open CityEngine and click Help > Download Tutorials and Examples in the main menu. After choosing a tutorial or example, the project is automatically downloaded and added to your CityEngine workspace.

CityEngine represents streets as centerlines, basic lane shapes with sidewalk and roadbed groups, lane shapes with rule generated materials or lane shapes with more complex rule generated models. All these representations can be saved as a custom street configuration.
The added benefit of using Python when working with streets is that you can edit lanes and assign configurations according to attributes predetermined in your data. As you start to identify which workflow is best suited for automation using the Python API, keep in mind the following concepts when working with streets:
Concept | Description |
Graph Segment | A street segment between two nodes |
Graph Node | Intersection or endpoint |
Street Configuration | Defines lane layout (sidewalks, vehicle lanes, widths, etc.) |
Shape | Generated geometry tied to a graph object |
Rule File (.cga) | Controls visual appearance of lanes and nodes |
See Tutorial 10a: Python scripting: Introduction for an introduction to using Python in CityEngine.
Additionally, you will need to work with CityEngine API functions to create and edit street configurations. In the Python reference, explore the following:
Scene & object access
ce = CE()
ce.getObjectsFrom(objects, ce.isShape)
ce.getObjectsFrom(objects, ce.isGraphSegment)
ce.getObjectsFrom(objects, ce.isGraphNode).
Attributes
ce.getAttribute(object, 'attributeName')
ce.setAttribute(objects, 'attributeName', value)
ce.setAttribute(objects, 'attributeName', 'USER')
Rules
ce.setRuleFile(shapes, <ruleFile>.cga)
ce.setStartRule(shapes, "Start")
ce.generateModels(shapes, synchronous=False)
Street configuration
ce.getStreetConfigurations()
ce.getStreetConfiguration(segment)
ce.applyStreetConfigurationToSegment(segments, configuration)
These APIs form the foundation of everything done in the following example scripts provided to you for further customization and extension of these concepts.
Note:
@noUIupdate was also used in some scripts to help manage processing performance. See @noUIupdateRemove and add lanes
In this section, you will explore how to remove and add lanes. This is a common editing workflow, allowing you to have more flexibility while designing street configurations.
Remove the outermost lanes
You will use a partial selection to quickly run scripts while visualizing editing possibilities for streets. First, remove the outermost lane shapes to simplify streets:
- Open the scene PythonScripting_Streets.cej scene.
- Go to Window > Console in the main menu. Leave the console open and visible in the UI to track progress when running your script.
- In the Viewport window, notice that the street segments appear with shapes representing two sidewalk lanes and two roadbed lanes. Go to Select > Apply selection set > Mini Block in the main menu.
- To navigate to that location in the Viewport window, press F to frame the selection or click the Bookmarks menu
and click Mini Block.
- Create a new Python script and name it: editStreetLanes.py
- Click Finish.
- Select the Module Main template.
- Add the following below if __name__ == '__main__':.
if __name__ == '__main__': # Get the selected street segments. segments = ce.getObjectsFrom(ce.selection, ce.isGraphSegment) # Go through each selected street segment. for segment in segments: # Get all lanes on this segment. lanes = ce.getObjectsFrom(segment, ce.isLane) # Count the lanes. lane_count = len(lanes) # Make sure there are at least two lanes. if lane_count >= 2: # Remove the last (outer-right) lane first. ce.removeLane(segment, lane_count - 1) # Remove the first (outer-left) lane. ce.removeLane(segment, 0)Note:
The Python scripts created for editing streets use zero-based indexing so that you can identify lanes by their relative position on the segment. This means that lane 1 has an index of 0 and lane 5 has an index of 4.
These scripts also have comments to guide you through each process step. As steps repeat, less comments will appear in future snippets.
- Press Ctrl+S to save the file.
- Click in the script to activate the cursor and press F9 to run.
When the console says run finished, notice that all outermost lanes are removed from the selection. To view the script at this stage, see the removeOutermostLanes_dist.py in the scripts folder.

The left image shows a sample of the selected segments before running the script compared to the right image showing the result after running the script. Notice that all selected segments no longer have sidewalk lanes.
Remove all lanes
Next, you'll continue updating the editStreetLanes.py file to remove all the lanes revealing the stand-alone street centerline.
Tip:
To temporarily see the scene without shapes while working, you can simply hide shapes in the Visibility settings
. This following section shows you how to edit the lanes rather than hiding them.
- Replace the for segment in the segments section with the following:
for segment in segments: lanes = ce.getObjectsFrom(segment, ce.isLane) lane_count = len(lanes) #Repeat once for every Lane that exists. for i in range(lane_count): #Remove the lane at this position. ce.removeLane(segment, 0)This creates a loop in which, for example, if lane_count is 4, the loop runs four times, removing the lane at index 0 each time. The reason the script always removes lane 0 rather than lane i is that the lane indexes shift each time a lane is removed. By repeatedly removing the lane at index 0, all existing lanes are removed from the segment.
- Save the file and press F9 to run.

The Mini Block selection now appears without lanes. To view the script at this stage, see removeAllLanes_dist.py in the scripts folder.
Add lanes
Next, in the editStreetLanes.py script, try adding lanes to the selected segments. Use indexing to set each lane position with specific widths for each roadway and sidewalk lane.
- Add the following below the ce.removeLane(segment, 0) line
# Build the street cross-section from left to right. ce.addLane(segment, 0, 3.0, CE.SIDEWALK_LEFT) roadbedLane1 = ce.addLane(segment, 1, 3.6, CE.ROADBED) roadbedLane2 = ce.addLane(segment, 2, 3.6, CE.ROADBED) ce.addLane(segment, 3, 3.0, CE.SIDEWALK_RIGHT) - Save the file and press F9 to run..
Notice that as this runs, selected centerlines become street segments with the lanes.

- To add more information to the lanes, set the travel direction for each by adding the following to the bottom of the script:
ce.setAttribute(roadbedLane1, '/ce/street/lane/travelDirection', 'Reverse') ce.setAttributeSource(roadbedLane1, '/ce/street/lane/travelDirection', 'USER') ce.setAttribute(roadbedLane2, '/ce/street/lane/travelDirection', 'Forward') ce.setAttributeSource(roadbedLane2, '/ce/street/lane/travelDirection', 'USER') - Save the file and press F9 to run.
Looking closely at the lanes, you will see the travel directions represented as arrows at the end of each selected lane. When you apply rules, this can be referenced to help set the orientation of modeled assets such as cars, bikes, and people traveling on the lanes.

- Select only one lane in the Mini Block selection.
- In the Inspector window, expand the Object Attributes section.
The object attributes have been updated to include the travelDirections attribute according to the indexed position of each lane. In the image below, you can see that the selected lane has a laneIndex of 1 out of a list [0,1,2,3] totaling four lanes. Counting from left to right, you can see that the selected lane in the image below has the travel direction of Reverse.

- Save and keep the script open.
Assign rules to lanes
Rules require an initial shape to be assigned in CityEngine. Since you have already added lane shapes back to the Mini Block segments in the last section, now you can take advantage of the out-of-the-box ESRI.lib rules for lanes accessible in the Navigator window:
- /ESRI.lib/rules/Streets/Lane/Vehicle/Car_Lane.cga
- /ESRI.lib/rules/Streets/Lane/Vehicle/Sidewalk_Lane.cga
- With the editingStreetLanes.py script open, click Save As and save the file as addLaneRules.py
- Adapt the section of the script where you build a street cross-section from left to right using indexing by assigning a variable that stores a reference to the newly created lane under the names sidewalkLeft, roadbedLane1, roadbedLane2 and sidewalkRight.
sidewalkLeft = ce.addLane(segment, 0, 3.0, CE.SIDEWALK_LEFT) roadbedLane1 = ce.addLane(segment, 1, 3.6, CE.ROADBED) roadbedLane2 = ce.addLane(segment, 2, 3.6, CE.ROADBED) sidewalkRight = ce.addLane(segment, 3, 3.0, CE.SIDEWALK_RIGHT) - Leave the travel direction section as-is. Then, add the following to the bottom of the script to create lists to store lanes for rule assignment, assign the rules, and generate the models and materials.
# Lane collections sidewalkLanes = [] roadbedLanes = [] # Store lanes for rule assignment sidewalkLanes.append(sidewalkLeft) sidewalkLanes.append(sidewalkRight) roadbedLanes.append(roadbedLane1) roadbedLanes.append(roadbedLane2) # Assign CGA rules ce.setRuleFile(sidewalkLanes, '/ESRI.lib/rules/Streets/Lane/Sidewalk/Sidewalk_Lane.cga') ce.setStartRule(sidewalkLanes, 'Start') ce.setRuleFile(roadbedLanes, '/ESRI.lib/rules/Streets/Lane/Vehicle/Car_Lane.cga') ce.setStartRule(roadbedLanes, 'Start') # Generate lane models ce.generateModels(sidewalkLanes + roadbedLanes, False) - Click Select > Apply Selection Set > Mini Block.
- Save the file and press F9 to run.

The rules have been assigned to the lanes.
Create a generic street configuration
You can create your own street configuration. Using Python, add a few lines to programmatically save your street layout for reuse later.
- Add the following to the bottom of your addLaneRules.py script.
street_config = ce.createStreetConfiguration(segments[0]) street_config.setName('My Street Configuration') ce.applyStreetConfigurationToSegment(segments, street_config)This creates a new street configuration and applies it to all selected segments.
- Save the addLaneRules.py script.
- Run the script again to see the following results:
- Lanes are removed and readded as the script runs.
- The Mini Block selection is still enabled.
- In the Inspector window, there is a new tile named My Street Configuration and it is highlighted to show that it is now assigned to the selected streets.

- Save your PythonScripting_Streets.cej scene and leave it open.
This is updated in the scene, however, it is not yet updated in the data. This means it is not populated in the Object Attributes section in the Inspector window. In the next section, you will work with the data.
To view the script at this stage, see the addLaneRules_dist.py file in the scripts folder which adds lanes, travel direction, and rules. You are now done practicing with the Mini Block segments.
Add ESRI.lib street configurations
Another added benefit of using Python when working with streets, is the ability to programmatically assign existing street configurations based on attributes. In this section, you will learn how to assign street configurations as-is from ESRI.lib using object attributes.
- In the PythonScripting_Streets.cej scene, click Initial View in the Bookmarks menu
.Do not select any streets in the scene, as the following script does not require a selection. It will instead search the data and apply the default configurations that CityEngine provides out-of-the-box.
If necessary, press Ctrl+Shift+A to deselect all segments.
- Create a new script called assignStreetConfigurationByAttribute.py.
There is an object attribute in the data called street_configuration. The values in this attribute were populated with the built-in CityEngine configuration names that display in the Street Configuration section in Inspector window.
- Add the following function below CityEngine instance ce=CE() to retrieve all street configurations currently available in the open scene and reorganize them into a Python dictionary so that you can conveniently look up a configuration by its name:
def getStreetConfigurationsByName(): configurations = {} for configuration in ce.getStreetConfigurations(): configurationKey = configuration.getName().lower() configurations[configurationKey] = configuration return configurations - Add the function below to start the process of assigning street configurations to street segments based on an attribute stored on those segments:
def applyStreetConfigurationFromAttribute(segments, streetConfigAttribute): configurations = getStreetConfigurationsByName() segmentsByConfiguration = {} - Add the following statements to start doing the main work of looping through every street segment, reading the specified attribute, matching that value to a CityEngine street configuration, and grouping the matching segments together:
for segment in segments: attributeValue = ce.getAttribute(segment, streetConfigAttribute) if not attributeValue: continue configurationKey = attributeValue.lower() configuration = configurations.get(configurationKey) if configuration is None: print('Street configuration not found:', attributeValue) continue if configurationKey not in segmentsByConfiguration: segmentsByConfiguration[configurationKey] = { 'configuration': configuration, 'segments': [] } segmentsByConfiguration[configurationKey]['segments'].append(segment) - Add the following to list the unique IDs of the segments with matching attributes and the configuration assigned.
oid = ce.getOID(segment) print('Street Configuration:', configuration.getName(), '| OID:', oid) - Apply each street configuration to its corresponding street segments by adding the following below the text you added in the previous step:
for group in segmentsByConfiguration.values(): ce.applyStreetConfigurationToSegment( group['segments'], group['configuration'] ) - At the bottom, add the following below if__name__==‘__main__’ to ensure that the Python file is being run directly, get all street segments from the scene, and apply street configurations to them based on their street configuration attribute.
if __name__ == '__main__': segments = ce.getObjectsFrom(ce.scene, ce.isGraphSegment) applyStreetConfigurationFromAttribute(segments, 'street_configuration') - Save the file and run.
Notice that street configurations generated in the scene without any pre-selection required.

These street configurations are unique to the segment’s street_configuration object attribute value that is pre-populated for this example.
- Select one of the generated segments to see the applied street configuration in the Object Attributes section.

- Click Window > Console in the main menu if it is no longer open from the previous section.
In the Console window, you can see that the unique IDs have been printed so that you can examine exactly which segments were updated.

Open the assignStreetConfigurationByAttribute_dist.py script to view and compare the script set up.
Create street configurations from attributes
Now you have data with your desired total width, number of lanes, and lane types. You may want to customize and assign your own street configurations programmatically. This is an example of how to work with rule definitions to construct street configurations from a variety of attributes such as width, material, assets, and lane markings.
- Click Select > Apply Selection Set > Big Block in the main menu.

- Open thre createStreetConfigurationsFromAttributes_dist.py script.
- Explore how rule definitions are set up for more customization possibilities. Notice that some of the structure and functions in this script are reused from the last section.
- The difference is that this script builds lanes within the existing total street width instead of defining each lane width to determine the resulting total street width.
- Run the script to see that new custom street configurations have been created and assigned to the Big Block selection set.
There is an exception in the script to avoid overwriting preexisting configurations within your selection. This is why you can still see the ESRI.lib street configurations you assigned in the previous section.
- Select an updated segment to view the assigned street configuration.

You will also notice that there are distinct street configuration names added to the Inspector window. These are created for each unique lane setup in the process and can be renamed as needed.
- Click Select > Apply Selection Set > Remaining Streets in the main menu.
While selected, check the object attributes, notice that the street_configuration attribute is not assigned to any of these segments.
- Open and run createNewObjectAttribute_dist.py on the inverted selection.

- The street_configuration object attribute now has the value My Street Configuration populated for all selected street segments.

- Keep the selection and run the assignStreetConfigurationByAttribute.py script from the previous section of this tutorial again to reassign the configurations by attribute which now includes My Street Configuration.
Every segment in the scene now has a street configuration assigned, however, nodes are still only represented as shapes.
Keep the Remaining Streets selection as-is for the next section.
Assign node rule
Nodes are independent of the street configuration and therefore require a separate rule assignment so that they appear with pavement material in the scene. To do this, you can use Python to identify the connected nodes and apply the default node rule from ESRI.lib.
- Create a new script and name it assignDefaultNodeRule.py
- Define the CGA rule file and start rule used for the node shape:
ce = CE() ruleFile = "/ESRI.lib/rules/Streets/Node/Default_Node.cga" startRule = "Start" - Create lists for storing roadbed lanes and sidewalk shapes then loop through each node while also separating sidewalk shapes from roadbed lanes:
def getNodeShapesByType(nodes): roadbedShapes = [] sidewalkShapes = [] for node in nodes: shapes = ce.getObjectsFrom(node, ce.isShape) for shape in shapes: if ce.getAttribute(shape, "sidewalkSide") is not None: sidewalkShapes.append(shape) else: roadbedShapes.append(shape) return roadbedShapes, sidewalkShapes - Assign the Default_Node.cga rule and start rule to each:
if __name__ == "__main__": nodes = ce.getObjectsFrom(ce.selection, ce.isGraphNode) roadbedShapes, sidewalkShapes = getNodeShapesByType(nodes) if roadbedShapes: ce.setRuleFile(roadbedShapes, ruleFile, True) ce.setStartRule(roadbedShapes, startRule) if sidewalkShapes: ce.setRuleFile(sidewalkShapes, ruleFile, True) ce.setStartRule(sidewalkShapes, startRule) - Add the following to ensure that it generates so that you can visibly see the change in the scene.
ce.generateModels(roadbedShapes + sidewalkShapes, False) - Save and run the script

Nodes in the Remaining Streets selection now have a rule assigned and appear with materials on roadbeds and sidewalks to match the connected segments
- Select a single node and press F take a closer look.
- In the Inspector window, click Node Sidewalk button
to view the Surface material assigned. Do not change anything here, we will update this setting for the Big Block using Python instead.

- Click Select > Apply Selection Set > Big Block to switch the selection.
- Update your assignDefaultNodeRule.py script by adapting the if sidewalkShapes section of the script.
The goal here is to change the default surface material to instead match the red paver sidewalk material that appears on most street configurations in the Big Block:
if sidewalkShapes: ce.setRuleFile(sidewalkShapes, ruleFile, True) ce.setStartRule(sidewalkShapes, startRule) ce.setAttribute(sidewalkShapes, '/ce/rule/Street_Materials.Surface', 'Paver Stacked Bond Red') ce.setAttributeSource(sidewalkShapes, '/ce/rule/Street_Materials.Surface', 'USER') - Save and run the updated script on the Big Block selection to see the nodes updated for the whole scene.

To view the script at this stage, see assignDefaultNodeRule_dist.py in the scripts folder. To view the desired result in the Viewport window, see PythonScripting_Streets_dist.cej in the scenes folder.
Three ways of working with street configurations are now represented in your scene. These include building a custom configuration with rule definitions, saving a configuration by name called My Street Configuration, and applying out-of-the-box ESRI.lib configurations by attribute. You’ve also used Python to automate street editing tasks and assign CGA rule files. Now, you are well equipped to adapt and reuse any of these scripts in your future projects.

See the CityEngine Python reference for more information.
To continue your learning with CityEngine, see the complete CityEngine tutorial catalog.