<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CAD | Algorist</title><link>https://www.algorist.co.uk/tag/cad/</link><atom:link href="https://www.algorist.co.uk/tag/cad/index.xml" rel="self" type="application/rss+xml"/><description>CAD</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-gb</language><lastBuildDate>Tue, 28 Sep 2021 00:00:00 +0000</lastBuildDate><image><url>https://www.algorist.co.uk/images/icon_hu1a112552fd764c0568141c667be1573d_16776_512x512_fill_lanczos_center_2.png</url><title>CAD</title><link>https://www.algorist.co.uk/tag/cad/</link></image><item><title>Extract features from CAD files Part 3: The future</title><link>https://www.algorist.co.uk/post/extract-features-from-cad-files-part-3-the-future/</link><pubDate>Tue, 28 Sep 2021 00:00:00 +0000</pubDate><guid>https://www.algorist.co.uk/post/extract-features-from-cad-files-part-3-the-future/</guid><description>&lt;p>In this third part of the series of posts on extracting objects from a CAD document, I’ll discuss how you might develop this CAD extraction tool further and the problems you might be able to solve.&lt;/p>
&lt;h2 id="prologue">Prologue&lt;/h2>
&lt;p>In &lt;a href="https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-1-a-primer/" target="_blank" rel="noopener">Part 1&lt;/a> we looked at the structure of a CAD file and built up a strategy to extract seat types and locations from an architect&amp;rsquo;s floor plan. The motivation for this is to provide seat location data to a model that creates a stack plan with optimal locations of teams to office amenities and other teams they collaborate with.&lt;/p>
&lt;p>In &lt;a href="https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-2-using-ezdxf/" target="_blank" rel="noopener">Part 2&lt;/a> we built an extraction tool based on the Python &lt;code>ezdxf&lt;/code> package that can read and query DXF files. We loaded a floor plan in DXF, printed the block types in a layer, extracted all block inserts that matched a layer and/or block type query and outputted them to a pandas dataframe.&lt;/p>
&lt;h2 id="additional-feature-extraction">Additional feature extraction&lt;/h2>
&lt;p>As long as a CAD drawing is segregated by block type and/or layer, several floor features can be extracted with their own query and appended to the dataframe of floor features. Currently only assignable seating has been extracted from a single layer, but what else might you want to extract?&lt;/p>
&lt;h3 id="floor-features-a-team-may-need">Floor features a team may need&lt;/h3>
&lt;p>Often teams will use meeting rooms. Some teams use meeting rooms more than others, or need more meeting rooms simultaneously. Large teams will typically require larger meeting rooms compared with small teams. The typical meeting room layout is to have a single table in the centre of the room, which means that running a query for, e.g., &amp;ldquo;small conference table&amp;rdquo; and &amp;ldquo;large conference table&amp;rdquo; will return you the count and the centre point of every small and large conference room, respectively. Alternatively, if the seats in a meeting room are stored on a layer separate to assignable desk seats then this could be a way of tracking the location and seating capacity of a meeting room.&lt;/p>
&lt;p>A similar approach could be used to extract printers, breakout rooms, kitchen facilities and everything else a team may have a preference for. It could even be used to track accessibility requirements (e.g., proximity to a lift, disabled toilets, etc.).&lt;/p>
&lt;h3 id="neighbourhoodszoning">Neighbourhoods/zoning&lt;/h3>
&lt;p>A related - but more complicated - concept is to define neighbourhoods or zones within a floor. Does a team need quiet space away from stairwells, kitchens and thoroughfares? What about key swipe access to secured areas on a floor? As long as this is defined in an appropriately named block or layer then we can extract it, calculating the spatial overlap with seats in that zone using a Python function.&lt;/p>
&lt;h2 id="drawingexport">Drawing/export&lt;/h2>
&lt;p>&lt;code>ezdxf&lt;/code> has addon modules, one of which deals with drawing and export of CAD files. Below is a very simple example of how to read a DXF file and render as a PNG image file to disk.&lt;/p>
&lt;pre>&lt;code class="language-python">from ezdxf import recover
from ezdxf.addons.drawing import matplotlib
# Exception handling left out for compactness:
doc, auditor = recover.readfile('your.dxf')
if not auditor.has_errors:
matplotlib.qsave(doc.modelspace(), 'your.png')
&lt;/code>&lt;/pre>
&lt;h2 id="streamline-the-use-of-oda-converter">Streamline the use of ODA Converter&lt;/h2>
&lt;p>In part 1 we discussed the need to convert a CAD file from proprietary DWG format to DXF format using the open source file converter provided by the &lt;a href="https://www.opendesign.com/guestfiles/oda_file_converter" target="_blank" rel="noopener">Open Design Alliance&lt;/a>. Instead of opening this software separately we can call it from &lt;code>ezdxf&lt;/code> using another addon.&lt;/p>
&lt;pre>&lt;code class="language-python">from ezdxf.addons import odafc
# Load a DWG file
doc = odafc.readfile('my.dwg')
# Use loaded document like any other ezdxf document
print(f'Document loaded as DXF version: {doc.dxfversion}.')
msp = doc.modelspace()
...
# Export document as DWG file for AutoCAD R2018
odafc.export_dwg(doc, 'my_R2018.dwg', version='R2018')
&lt;/code>&lt;/pre>
&lt;h2 id="generative-design">Generative design&lt;/h2>
&lt;p>The ultimate aim of the stack plan modelling tool is to develop it into a product. What better way to do this than to create a workflow that starts with a CAD drawing and ends with an annotated CAD drawing labelled according to team locations? You could reassign seats from your seats layer to new layers named after the relevant teams on that floor. However, the more appropriate method is likely to use attribute definitions.&lt;/p>
&lt;p>Below is an example of isolating an entity and changing an attribute. For a real use case, &lt;code>entities&lt;/code> would be a list of block IDs of seats that belong to a certain team and the attribute to change would be the team ID.&lt;/p>
&lt;pre>&lt;code class="language-python">doc = ezdxf.readfile(filepath)
model = doc.modelspace()
# load a data frame with unique ID that refers to a block insert &amp;quot;handle&amp;quot;, and a team name
# define a query_string to search for the appropriate layer/blocks that refer to your allocatable seats
entities = [x for x in model.query(query_string) if x.has_dxf_attrib('name')]
if len(entities):
entity = entities[0] # process first entity found
for attrib in entity.attribs:
if attrib.dxf.tag == &amp;quot;diameter&amp;quot;: # identify attribute by tag
attrib.dxf.text = &amp;quot;17mm&amp;quot; # change attribute content
&lt;/code>&lt;/pre>
&lt;h2 id="stretch-goal">Stretch goal&lt;/h2>
&lt;p>In practice a client will take a stack plan recommendation and remodel a floor to include additional teams, additional seats (if the recommended teams for a floor mean the floor is over capacity) or to space desks out if the floor is under capacity. Wouldn&amp;rsquo;t it be great if we could generate floor plans automatically, or at least edit an existing one to accommodate these changes? Between a model that can learn floor plan design principles (e.g., minimum separation between objects, coincidence of a seat with a desk, etc.) and the ability to insert new block references into the model (see below), we would have all we need to achieve this.&lt;/p>
&lt;p>An example from the &lt;a href="https://ezdxf.readthedocs.io/en/stable/tutorials/blocks.html?highlight=random#block-references-insert" target="_blank" rel="noopener">ezdxf&lt;/a> documentation.&lt;/p>
&lt;pre>&lt;code class="language-python">import ezdxf
import random
def get_random_point():
&amp;quot;&amp;quot;&amp;quot;Returns random x, y coordinates.&amp;quot;&amp;quot;&amp;quot;
x = random.randint(-100, 100)
y = random.randint(-100, 100)
return x, y
# Get the modelspace of the drawing.
msp = doc.modelspace()
# Get 50 random placing points.
placing_points = [get_random_point() for _ in range(50)]
for point in placing_points:
# Every flag has a different scaling and a rotation of -15 deg.
random_scale = 0.5 + random.random() * 2.0
# Add a block reference to the block named 'FLAG' at the coordinates 'point'.
msp.add_blockref('FLAG', point, dxfattribs={
'xscale': random_scale,
'yscale': random_scale,
'rotation': -15
})
# Save the drawing.
doc.saveas(&amp;quot;blockref_tutorial.dxf&amp;quot;)
&lt;/code>&lt;/pre></description></item><item><title>Extract features from CAD documents Part 2: Using ezdxf</title><link>https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-2-using-ezdxf/</link><pubDate>Wed, 14 Apr 2021 00:00:00 +0000</pubDate><guid>https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-2-using-ezdxf/</guid><description>&lt;p>In this second part of the series of posts on extracting objects from a CAD document, I&amp;rsquo;ll go through the process of using the &lt;code>ezdxf&lt;/code> package to implement the extraction strategy discussed in &lt;a href="https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-1-a-primer/" target="_blank" rel="noopener">part one&lt;/a>.&lt;/p>
&lt;h2 id="prologue">Prologue&lt;/h2>
&lt;p>In part one we looked at the structure of a CAD file and built up a strategy to extract seat types and locations from an architect&amp;rsquo;s floor plan. The motivation for this is to provide seat location data to a model that creates a stack plan with optimal locations of teams to office amenities and other teams they collaborate with.&lt;/p>
&lt;p>In summary, our strategy is:&lt;/p>
&lt;ul>
&lt;li>load the DXF file;&lt;/li>
&lt;li>create an object from the model space;&lt;/li>
&lt;li>build a layer and block type query;&lt;/li>
&lt;li>extract unique ID, x and y features from every element returned from the query, recording the block type and layer it came from;&lt;/li>
&lt;li>check for errors and write to csv file.&lt;/li>
&lt;/ul>
&lt;h2 id="ezdxf">ezdxf&lt;/h2>
&lt;p>Since the genetic algorithm we&amp;rsquo;ve built is in R, I tried to build an extraction tool in R. However, the only DXF file loader package I could find is called &lt;a href="https://ezdxf.readthedocs.io/en/stable/" target="_blank" rel="noopener">ezdxf&lt;/a> and is built in Python. I&amp;rsquo;m not a fan of reinventing the wheel so I wrote a custom package in Python that is built on top of &lt;code>ezdxf&lt;/code>&amp;rsquo;s classes and methods.&lt;/p>
&lt;p>The following functions are building blocks to load the model space from a dxf file, query the model space object for block inserts that belong to a certain block type or layer, and to extract them. They rely heavily on the documentation and tutorials from the &lt;code>ezdxf&lt;/code> readme website so I would encourage the interested reader to refer to those documents for background information.&lt;/p>
&lt;h3 id="load-file">Load file&lt;/h3>
&lt;pre>&lt;code class="language-python">import ezdxf
import numpy as np
import pandas as pd
import sys
def load(filepath):
&amp;quot;&amp;quot;&amp;quot;
Loads the modelspace of a dxf file using the ezdxf package.
Parameters
----------
filepath: string
The file path to a dxf file
Returns
An exdxf modelspace object.
-------
&amp;quot;&amp;quot;&amp;quot;
try:
doc = ezdxf.readfile(filepath)
msp = doc.modelspace()
except IOError:
print(f'Not a DXF file or a generic I/O error.')
sys.exit(1)
except ezdxf.DXFStructureError:
print(f'Invalid or corrupted DXF file.')
sys.exit(2)
return msp
&lt;/code>&lt;/pre>
&lt;h3 id="print-blocks-that-belong-to-a-layer">Print blocks that belong to a layer&lt;/h3>
&lt;pre>&lt;code class="language-python">def print_blocks(model, layer):
&amp;quot;&amp;quot;&amp;quot;
Prints all blocks belonging to a layer
Parameters
----------
model : ezdxf modelspace object
A modelspace object created from a loaded dxf file.
See the ezdxf help file for 'readfile' and 'modelspace' methods
layer : string
A string specifying the layer name in the modelspace object
Returns
A list of string elements
-------
&amp;quot;&amp;quot;&amp;quot;
query_string = '*[layer ? \&amp;quot;%s\&amp;quot;]' % layer
entities = model.query(query_string)
layers = set(i.dxf.layer for i in entities)
for i in layers:
layer_entity = model.query('*[layer == \&amp;quot;%s\&amp;quot;]' % i)
layer_entity = [x for x in layer_entity if x.has_dxf_attrib('name')]
blocks = set(j.dxf.name for j in layer_entity)
print(i + &amp;quot;:&amp;quot;)
for x in blocks:
print(x)
&lt;/code>&lt;/pre>
&lt;h3 id="extract-inserts-belonging-to-a-block-type-or-layer">Extract inserts belonging to a block type or layer&lt;/h3>
&lt;p>The first function extracts all inserts of all blocks based on a query made on the model object. Within the body of the function there is code to check for negative x values and mirror them if it finds any. This is because when I compared the DWG and DXF files I noticed that some objects had been mirrored in the DXF file for some reason. This fix worked for me but it may not work for you, depending on your version of CAD software you used, the conversion software and the point of origin (the 0,0 point). This issue is not due to Python as the issue appeared in the input DXF file prior to extraction. You have been warned!&lt;/p>
&lt;pre>&lt;code class="language-python">def extract_query(model, query_string, scaling = 0.001):
&amp;quot;&amp;quot;&amp;quot;
Extracts all objects from an ezdxf model object that are returned by a query.
Parameters
----------
model : ezdxf modelspace object
A modelspace object created from a loaded dxf file.
See the ezdxf help file for 'readfile' and 'modelspace' methods
query_string : string
A string specifying a layer or block based query
scaling : positive real value
Depending on the units of the modelspace and the desired units, a scaling
may be preferred. The default is a unit of mm and a desired unit of metre.
(Default value = 0.001)
Returns
Pandas dataframe object with ID, x and y coordinate values. One row per object.
-------
&amp;quot;&amp;quot;&amp;quot;
entities = [x for x in model.query(query_string) if x.has_dxf_attrib('name')]
coords = [i.dxf.insert for i in entities]
id = ['UID' + i.dxf.handle for i in entities]
output = pd.DataFrame(coords, columns = ['x', 'y', 'z'], index = id).drop(['z'], axis=1)
output.index.name = &amp;quot;id&amp;quot;
output['type'] = [i.dxf.name for i in entities]
output['layer'] = [i.dxf.layer for i in entities]
# A known issue with converting DWG files to DXF is that some elements have their x coordinate reversed
if len(output.x[output.x &amp;lt;= 0]) != 0:
print(&amp;quot;Negative x values! This is a known issue with dxf files&amp;quot;)
print(&amp;quot;Mirroring negative x values&amp;quot;)
output.x = np.abs(output.x)
# Apply scaling factor
output = output.apply(lambda x: x * scaling if x.name in ['x', 'y'] else x)
return output
&lt;/code>&lt;/pre>
&lt;p>The second function builds the query and passes it to the first.&lt;/p>
&lt;pre>&lt;code class="language-python">def extract(model, layer=None, block=None, scaling = 0.001):
&amp;quot;&amp;quot;&amp;quot;
Extracts all objects of a certain block type within a specified layer of a dxf file.
Parameters
----------
model : ezdxf modelspace object
A modelspace object created from a loaded dxf file.
See the ezdxf help file for 'readfile' and 'modelspace' methods
layer : string
A string specifying the layer name in the modelspace object
block : string
A string specifying the block name in the modelspace object
scaling : positive real value
Depending on the units of the modelspace and the desired units, a scaling
may be preferred. The default is a unit of mm and a desired unit of metre.
(Default value = 0.001)
Returns
Pandas dataframe object with ID, x and y coordinate values. One row per object.
-------
&amp;quot;&amp;quot;&amp;quot;
if layer is None:
query_string = '*[name==\&amp;quot;%s\&amp;quot;]' % block
elif block is None:
query_string = '*[layer==\&amp;quot;%s\&amp;quot;]' % layer
else:
query_string = '*[layer==\&amp;quot;%s\&amp;quot; &amp;amp; name==\&amp;quot;%s\&amp;quot;]' % (layer, block)
output = extract_query(model, query_string, scaling)
return output
&lt;/code>&lt;/pre>
&lt;p>Note that the default scaling is 0.01. This is because typical CAD files dealing with building drawings have units of cm, and I would like to store x,y positions in metres.&lt;/p>
&lt;h2 id="taking-it-further">Taking it further&lt;/h2>
&lt;p>At &lt;a href="https://arcadisgen.com/" target="_blank" rel="noopener">Arcadis Gen&lt;/a> we take a package based approach to consultancy to make enduring products from one-off engagements and shorten the development cycle for future similar projects. To do this I developed a Python package called &lt;code>cadextract&lt;/code>, built on top of ezdxf and providing helper functions to extract seat plans. The functions in this package look a little similar to the above, but also include &lt;code>fuzzy_extract&lt;/code> to deal with queries using regular expressions, &lt;code>batch_extract&lt;/code> that extracts from multiple DXF files using the same query and stores them in a single dataframe, plotting objects, and more.&lt;/p>
&lt;p>In the third and final part of this series on CAD I&amp;rsquo;ll take a more speculative look at where you might be able to take this programmatic approach, and where future opportunities might lie.&lt;/p></description></item><item><title>Extract features from CAD documents Part 1: A primer</title><link>https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-1-a-primer/</link><pubDate>Mon, 01 Mar 2021 00:00:00 +0000</pubDate><guid>https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-1-a-primer/</guid><description>&lt;p>This is the first part of a series of posts describing my experience of extracting objects from a CAD document.&lt;/p>
&lt;h2 id="motivation">Motivation&lt;/h2>
&lt;p>Recently at work a project came up that involved the optimisation of building occupancy during the refit of a 16 floor central London office block. As we intend to optimise team locations down to the seat level within a floor, we were given floor plans in both pdf and dwg file formats.&lt;/p>
&lt;p>The original scope allowed for us to rely on another team to manually encode the distances between each desk on each floor, but there are many downsides to this approach: human error; costly rework when plans change; lack of ability to iteratively improve on feature extraction; and huge risk of schedule overrun when modelling has to wait on this input data. The alternative was to programmatically scrape the CAD file for seat ID and X and Y coordinates. I&amp;rsquo;ll present the solution in &lt;a href="https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-2-using-ezdxf/" target="_blank" rel="noopener">Part 2&lt;/a>, but first here is what I learned about the structure of CAD files that I needed before writing an extraction utility.&lt;/p>
&lt;h2 id="file-types">File types&lt;/h2>
&lt;p>There are generally two file types in the world of CAD: DWG and DXF. DWG is meant as shorthand for &lt;em>drawing&lt;/em>, and is a proprietary file format belonging to AutoCAD. It&amp;rsquo;s not very useful on its own so I found it best to convert to DXF using the open source file converter provided by the &lt;a href="https://www.opendesign.com/guestfiles/oda_file_converter" target="_blank" rel="noopener">Open Design Alliance&lt;/a>.&lt;/p>
&lt;p>DXF is short for &lt;em>design exchange format&lt;/em>, and comes in binary and ASCII flavours. I prefer the ASCII version as it is human readable but the downside is that it is uncompressed and so file sizes have the potential to be large. It is much more easily shared between programs (even Inkscape) and I find that LibreCAD is a really lightweight way of browsing floor plans in this format.&lt;/p>
&lt;h2 id="file-structure">File structure&lt;/h2>
&lt;p>A CAD document comprises one model space and zero or more paper spaces. A model space is a limitless field in XYZ space with a certain unit and coordinate frame of reference. A paper space is what you might expect: a layout designed for presentation and printing. Paper spaces are constrained in spatial extent and will have a scale, where the units in model space are scaled for rendering. A perspective is also defined, which affects the perception of distance and distortion (if in 3d).&lt;/p>
&lt;p>For extracting objects and their locations we need the model space rather than the paper space.&lt;/p>
&lt;h2 id="components-of-a-cad-drawing">Components of a CAD drawing&lt;/h2>
&lt;p>Building up from the smallest elements, we have&amp;hellip;&lt;/p>
&lt;h3 id="entities">Entities&lt;/h3>
&lt;p>Entities are the most primitive element of a CAD drawing, including points, lines, rectangles, circles and elliptical arcs.&lt;/p>
&lt;h3 id="blocks">Blocks&lt;/h3>
&lt;p>Blocks are a group of one or more entities. It functions as a template, which can be inserted into the model space multiple times. Each instance of a block is called an insert. If you update a block (by editing an element within the block, for instance) then every insert of that block is updated.&lt;/p>
&lt;h3 id="layers">Layers&lt;/h3>
&lt;p>A layer organises many elements and inserts under common attributes and under common command (e.g., visibility, locked/unlocked). Whilst a layer may contain many elements, an element can only be on a single layer. This fact makes layers useful for searching for entities and inserts.&lt;/p>
&lt;h2 id="extraction-strategy">Extraction strategy&lt;/h2>
&lt;p>For my purpose, I need to extract each seat (or alternatively, each desk) from each floor. This means that I should search for every insert of the appropriate seat block, likely located within a common layer. Ideally that search would return a list containing all the inserts, each of which should contain a unique identifier and an X/Y coordinate.&lt;/p>
&lt;p>For that we will use the &lt;a href="https://ezdxf.readthedocs.io/en/stable/" target="_blank" rel="noopener">ezdxf&lt;/a> package in Python in &lt;a href="https://www.algorist.co.uk/post/extract-features-from-cad-documents-part-2-using-ezdxf/" target="_blank" rel="noopener">Part 2&lt;/a>.&lt;/p></description></item></channel></rss>