In the last post we saw some code combining F# with C# to create a random "hatch" - a polyline path that bounces around within a boundary.
This post extends the code to make use of Asynchronous Workflows in F# to parallelize the testing of points along a segment. In the initial design of the application I decided to test 10 points along each segment, to see whether it remained entirely within our boundary: the idea being that this granularity makes it very likely the segment will fail the test, should it happen to leave the boundary at any point. Not 100% guaranteed, but a high probability event. What this code does is take the 10 tests and queue them up for concurrent processing (where the system is capable of it).
Asynchronous Workflows - as suggested by the name - were intended to fire off and manage asynchronous tasks (ones that access network resources, for instance). The segment testing activity is actually very local and processor-bound, so it's not really what the mechanism was intended for, but I thought it would be interesting to try. One interesting point: while testing this code I noticed that it actually ran slower on a single processor machine, which is actually quite logical: only one core is available for processing, so the amount of sequential processing is not reduced but the overhead of synchronizing the various tasks is added. So it was fairly inevitable it would take longer. In the post I first talked about Asynchronous Workflows I showed a sample that queried multiple RSS sites for data: even on a single processor machine this was significantly quicker, as parallelizing the network latency led to a clear gain.
Anyway, as it was slower on this machine I decided only to enable the parallel version of the code in cases where the computer's NUMBER_OF_PROCESSORS environment variable is greater than 1. I checked quickly on a colleague's dual-core machine, and sure enough this variable is set to 2 on his system. I haven't, however, tested the code on a dual- or multi-core system, but I'll be getting a new system in a matter of weeks, which will give me the chance to test it out.
Here's the F# code, with the line numbers highlighting the changes in red:
1 // Use lightweight F# syntax
2
3 #light
4
5 // Declare a specific namespace and module name
6
7 module BounceHatchAsync.Commands
8
9 // Import managed assemblies
10
11 #I @"C:\Program Files\Autodesk\AutoCAD 2008"
12 #I @".\PointInCurve\bin\Debug"
13
14 #r "acdbmgd.dll"
15 #r "acmgd.dll"
16
17 #R "PointInCurve.dll" // R = CopyFile is true
18
19 open System
20 open Autodesk.AutoCAD.Runtime
21 open Autodesk.AutoCAD.ApplicationServices
22 open Autodesk.AutoCAD.DatabaseServices
23 open Autodesk.AutoCAD.EditorInput
24 open Autodesk.AutoCAD.Geometry
25 open PointInCurve
26
27 // Get a random vector on a plane
28
29 let randomUnitVector pl =
30
31 // Create our random number generator
32 let ran = new System.Random()
33
34 // First we get the absolute value
35 // of our x and y coordinates
36
37 let absx = ran.NextDouble()
38 let absy = ran.NextDouble()
39
40 // Then we negate them, half of the time
41
42 let x = if (ran.NextDouble() < 0.5) then -absx else absx
43 let y = if (ran.NextDouble() < 0.5) then -absy else absy
44
45 // Create a 2D vector and return it as
46 // 3D on our plane
47
48 let v2 = new Vector2d(x, y)
49 new Vector3d(pl, v2)
50
51 type traceType =
52 | Accepted
53 | Rejected
54 | Superseded
55
56 // Draw one of three types of trace vector
57
58 let traceSegment (start:Point3d) (endpt:Point3d) trace =
59
60 let ed =
61 Application.DocumentManager.MdiActiveDocument.Editor
62
63 let vecCol =
64 match trace with
65 | Accepted -> 3
66 | Rejected -> 1
67 | Superseded -> 2
68 let trans =
69 ed.CurrentUserCoordinateSystem.Inverse()
70 ed.DrawVector
71 (start.TransformBy(trans),
72 endpt.TransformBy(trans),
73 vecCol,
74 false)
75
76 // Test a segment to make sure it is within our boundary
77
78 let testSegment cur (start:Point3d) (vec:Vector3d) =
79
80 // (This is inefficient, but it's not a problem for
81 // this application. Some of the redundant overhead
82 // of firing rays for each iteration could be factored
83 // out, among other enhancements, I expect.)
84
85 let pts =
86 [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]
87
88 // Call into our IsInsideCurve library function,
89 // "and"-ing the results
90
91 let inside pt =
92 PointInCurve.Fns.IsInsideCurve(cur, pt)
93
94 List.for_all inside pts
95
96 // Test a segment to make sure it is within our boundary
97
98 let testSegmentAsync cur (start:Point3d) (vec:Vector3d) =
99
100 let pts =
101 [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]
102
103 // Call into our IsInsideCurve library function,
104 // "and"-ing the results
105
106 let inside pt =
107 async { return PointInCurve.Fns.IsInsideCurve(cur, pt) }
108
109 let tests =
110 Async.Run
111 (Async.Parallel
112 [for pt in pts -> inside pt])
113
114 Array.for_all (fun x -> x) tests
115
116 // For a particular boundary, get the next vertex on the
117 // curve, found by firing a ray in a random direction
118
119 let nextBoundaryPoint (cur:Curve)
120 (start:Point3d) trace runAsync =
121
122 // Get the intersection points until we
123 // have at least 2 returned
124 // (will usually happen straightaway)
125
126 let rec getIntersect (cur:Curve)
127 (start:Point3d) vec =
128
129 let plane = cur.GetPlane()
130
131 // Create and define our ray
132
133 let ray = new Ray()
134 ray.BasePoint <- start
135 ray.UnitDir <- vec
136
137 let pts = new Point3dCollection()
138 cur.IntersectWith
139 (ray,
140 Intersect.OnBothOperands,
141 pts,
142 0, 0)
143
144 ray.Dispose()
145
146 if (pts.Count < 2) then
147 let vec2 = randomUnitVector plane
148 getIntersect cur start vec2
149 else
150 pts
151
152 // For each of the intersection points - which
153 // are points elsewhere on the boundary - let's
154 // check to make sure we don't have to leave the
155 // area to reach them
156
157 let plane =
158 cur.GetPlane()
159 let pts =
160 randomUnitVector plane |> getIntersect cur start
161
162 // Get the distance between two points
163
164 let getDist fst snd =
165 let (vec:Vector3d) = fst - snd
166 vec.Length
167
168 // Compare two (dist, pt) tuples to allow sorting
169 // based on the distance parameter
170
171 let compDist fst snd =
172 let (dist1, pt1) = fst
173 let (dist2, pt2) = snd
174 if dist1 = dist2 then
175 0
176 else if dist1 < dist2 then
177 -1
178 else // dist1 > dist2
179 1
180
181 // From the list of points we create a list
182 // of (dist, pt) pairs, which we then sort
183
184 let sorted =
185 [ for pt in pts -> (getDist start pt, pt) ] |>
186 List.sort compDist
187
188 // A test function to check whether a segment
189 // is within our boundary. It draws the appropriate
190 // trace vectors, depending on success
191
192 let testItem dist =
193 let (distval, pt) = dist
194 let vec = pt - start
195 if (distval > Tolerance.Global.EqualVector) then
196 let res =
197 if runAsync then
198 testSegmentAsync cur start vec
199 else
200 testSegment cur start vec
201 if res then
202 if trace then
203 traceSegment start pt traceType.Accepted
204 Some(dist)
205 else
206 if trace then
207 traceSegment start pt traceType.Rejected
208 None
209 else
210 None
211
212 // Get the first item - which means the shortest
213 // non-zero segment, as the list is sorted on distance
214 // - that satisifies our condition of being inside
215 // the boundary
216
217 let ret = List.first testItem sorted
218 match ret with
219 | Some(d,p) -> p
220 | None -> failwith "Could not get point"
221
222 // We're using a different command name, so we can compare
223
224 [<CommandMethod("fba")>]
225 let bounceHatch() =
226 let doc =
227 Application.DocumentManager.MdiActiveDocument
228 let db = doc.Database
229 let ed = doc.Editor
230
231 // Get various bits of user input
232
233 let getInput =
234 let peo =
235 new PromptEntityOptions
236 ("\nSelect point on closed loop: ")
237 let per = ed.GetEntity(peo)
238 if per.Status <> PromptStatus.OK then
239 None
240 else
241 let pio =
242 new PromptIntegerOptions
243 ("\nEnter number of segments: ")
244 pio.DefaultValue <- 500
245 let pir = ed.GetInteger(pio)
246 if pir.Status <> PromptStatus.OK then
247 None
248 else
249 let pko =
250 new PromptKeywordOptions
251 ("\nDisplay segment trace: ")
252 pko.Keywords.Add("Yes")
253 pko.Keywords.Add("No")
254 pko.Keywords.Default <- "Yes"
255 let pkr = ed.GetKeywords(pko)
256 if pkr.Status <> PromptStatus.OK then
257 None
258 else
259 Some
260 (per.ObjectId,
261 per.PickedPoint,
262 pir.Value,
263 pkr.StringResult.Contains("Yes"))
264
265 match getInput with
266 | None -> ignore()
267 | Some(oid, picked, numBounces, doTrace) ->
268
269 // Capture the start time for performance
270 // measurement
271
272 let starttime = System.DateTime.Now
273
274 // We'll run asynchronously only when we have
275 // multiple processing cores
276
277 let runAsync =
278 (Int32.of_string
279 (Environment.GetEnvironmentVariable
280 ("NUMBER_OF_PROCESSORS")))
281 > 1
282
283 use tr =
284 db.TransactionManager.StartTransaction()
285
286 // Check the selected object - make sure it's
287 // a closed loop (could do some more checks here)
288
289 let obj =
290 tr.GetObject(oid, OpenMode.ForRead)
291
292 match obj with
293 | :? Curve ->
294 let cur = obj :?> Curve
295 if cur.Closed then
296 let latest =
297 picked.
298 TransformBy(ed.CurrentUserCoordinateSystem).
299 OrthoProject(cur.GetPlane())
300
301 // Create our polyline path, adding the
302 // initial vertex
303
304 let path = new Polyline()
305 path.Normal <- cur.GetPlane().Normal
306
307 path.AddVertexAt
308 (0,
309 latest.Convert2d(cur.GetPlane()),
310 0.0, 0.0, 0.0)
311
312 // A recursive function to get the points
313 // for our path
314
315 let rec definePath start times =
316 if times <= 0 then
317 []
318 else
319 try
320 let pt =
321 nextBoundaryPoint cur start doTrace runAsync
322 (pt :: definePath pt (times-1))
323 with exn ->
324 if exn.Message = "Could not get point" then
325 definePath start times
326 else
327 failwith exn.Message
328
329 // Another recursive function to add the vertices
330 // to the path
331
332 let rec addVertices (path:Polyline)
333 index (pts:Point3d list) =
334
335 match pts with
336 | [] -> []
337 | a::[] ->
338 path.AddVertexAt
339 (index,
340 a.Convert2d(cur.GetPlane()),
341 0.0, 0.0, 0.0)
342 []
343 | a::b ->
344 path.AddVertexAt
345 (index,
346 a.Convert2d(cur.GetPlane()),
347 0.0, 0.0, 0.0)
348 addVertices path (index+1) b
349
350 // Plug our two functions together, ignoring
351 // the results
352
353 definePath picked numBounces |>
354 addVertices path 1 |>
355 ignore
356
357 // Now we'll add our polyline to the drawing
358
359 let bt =
360 tr.GetObject
361 (db.BlockTableId,
362 OpenMode.ForRead) :?> BlockTable
363 let btr =
364 tr.GetObject
365 (bt.[BlockTableRecord.ModelSpace],
366 OpenMode.ForWrite) :?> BlockTableRecord
367
368 // We need to transform the path polyline so
369 // that it's over our boundary
370
371 path.TransformBy
372 (Matrix3d.Displacement
373 (cur.StartPoint - Point3d.Origin))
374
375 // Add our path to the modelspace
376
377 btr.AppendEntity(path) |> ignore
378 tr.AddNewlyCreatedDBObject(path, true)
379
380 // Commit, whether we added a path or not.
381
382 tr.Commit()
383
384 // Print how much time has elapsed
385
386 let elapsed =
387 System.DateTime.op_Subtraction
388 (System.DateTime.Now, starttime)
389
390 ed.WriteMessage
391 ("\nElapsed time: " + elapsed.ToString())
392
393 // If we're tracing, pause for user input
394 // before regenerating the graphics
395
396 if doTrace then
397 let pko =
398 new PromptKeywordOptions
399 ("\nPress return to clear trace vectors: ")
400 pko.AllowNone <- true
401 pko.AllowArbitraryInput <- true
402 let pkr = ed.GetKeywords(pko)
403 ed.Regen()
404
405 | _ ->
406 ed.WriteMessage("\nObject is not a curve.")
Here's the the complete project which defines both FB and FBA commands for simple side-by-side comparison.