Layout Import with Python

Defining layout in AnyLogic

For defining a layout (paths, roads, walls...) in AnyLogic you have the following two conventional methods:

  1. Draw the layout manually in the AnyLogic editor

While this is totally fine for small layouts, it gets very quickly very time consuming. Imagine modeling a railway yard (the place where trains are staying overnight) with hundreds of tracks and intersections.

  1. Write code that dynamically creates the layout during the model runtime (for example triggered by the OnStartup code)

This is a lot better for big layouts: You retrieve the layout data from whatever source you want (Collections inside AnyLogic, internal Database, external Database, Excel, CSV.....). During the simulation run, a function with a loop goes through the data and creates all the elements, initializes them and you can use them as if they were defined manually in the editor. You are very flexible, at each run your data is up to date.

In which case might you need more? You have too many elements to manually draw them, but

  • want to be able to see and modify them in the AnyLogic editor
  • you have other elements that you don't want to create during runtime, but you need a visual reference where to put them in the editor

A third layout option: Python

An AnyLogic model source code is stored in single .ALP files. Those use an XML structure to save all the actions that you do in the AnyLogic editor. While this XML gets quite large and confusing, the parts where layout is stored is actually pretty easy to read.

Let's have a look at a part of ALP file, which already contains a RailLibrary element:

			<Shapes>
				<Railyard>
					<Id>1547632154490</Id>
					<Name><![CDATA[railwayNetwork]]></Name>
					<X>20</X><Y>20</Y>
					<Label><X>10</X><Y>0</Y></Label>
					<PublicFlag>true</PublicFlag>
					<PresentationFlag>true</PresentationFlag>
					<ShowLabel>false</ShowLabel>
					<DrawMode>SHAPE_DRAW_2D3D</DrawMode>

			<Shapes>
				<RailTrack>
					<Id>1547632152170</Id>
					<Name><![CDATA[railwayTrack]]></Name>
					<X>100</X><Y>100</Y>
					<Label><X>10</X><Y>0</Y></Label>
					<PublicFlag>true</PublicFlag>
					<PresentationFlag>true</PresentationFlag>
					<ShowLabel>false</ShowLabel>
					<DrawMode>SHAPE_DRAW_2D3D</DrawMode>
					<Z>0</Z>
					<LineColor>-16777216</LineColor>
					<LineMaterial>null</LineMaterial>
					<LineWidth>1</LineWidth>
					<PathType>railroad</PathType>
					<Width Class="UnitValue">
						<Value Class="Double">1.5</Value>
						<Unit Class="LengthUnits"><![CDATA[METER]]></Unit>
					</Width>
					<Points>
						<Point><X>0</X><Y>0</Y><Z>0</Z></Point>
						<Point><X>25</X><Y>0</Y><Z>0</Z></Point>
						<Point><X>25</X><Y>0</Y><Z>0</Z></Point>
						<Point><X>100</X><Y>0</Y><Z>0</Z></Point>
						<Point><X>75</X><Y>0</Y><Z>0</Z></Point>
						<Point><X>75</X><Y>0</Y><Z>0</Z></Point>
					</Points>
					<Bidirectional>true</Bidirectional>
				</RailTrack>
			</Shapes>
				</Railyard>
			</Shapes>

Pretty straightforward: The first part defines a railway network, and the second one specifies that there is one track inside this network. The properties of the elements are mostly not surprising - they mirror the properties that you can set for this element in the AnyLogic editor. The only thing you cannot see in the AnyLogic GUI, is the ID that each element gets automatically, to reference the element in other parts of the XML.

You can see where this is going: All we have to do is add all the elements that we want in our layout.

Procedure

For adding our own layout to the existing ALP file, the following steps are necessary:

  1. Get the layout information from somewhere
  2. Convert the information into the XML format needed for the ALP file
  3. Open the existing ALP and read its XML tree
  4. Find the correct location inside the existing ALP file
  5. Insert my converted XML entries at this position
  6. Save the modified ALP under a new name

Code

For no other reason than that I always wanted to try this language, I choose to implement it in Python. In this example I hold my layout data in a CSV file and I want to insert some switches of the RailwayLibrary.

Parse.py
import csv
import xml.etree.ElementTree as ET

#read input from csv file
with open('input.csv', 'r') as csvfile:
    reader = csv.reader(csvfile, delimiter=';', quotechar='|')
    datalist = list(reader)
    print(datalist)

#open ALP file and read as XML tree
tree = ET.parse('input.alp')
root = tree.getroot()

#find the correct location inside XML tree
g = tree.iterfind('.//Shapes/Railyard/Shapes‘)

#Check if a location could be found
if g is None:
    print("There is not yet any rail element in the AnyLogic project!")
else:
    location = next(g)

#add elements
for index,line in enumerate(datalist):
    newSwitch = ET.Element('RailroadSwitch')
    tag1 = ET.SubElement(newSwitch,'Id')
    tag1.text = "123456789"+str(line[2]).zfill(4)
location.insert(0,newSwitch)

#save modified ALP file under new name
tree.write('output.alp')

For each layout element that one wants to add, the finding of the correct location, as well as the element adding steps (create and fill the right tags,....) will be different.

Conclusion

Admittedly the described procedure requires a high initial effort. But once the script for the specific layout element that needs to be imported is created, it is reusable and quick.

What would be even better (and I hope I'll find the time to do this as well) would be a converter both ways. So instead of only adding elements to the ALP, retrieving the complete layout information, being able to modify it and then rewriting it to the ALP.