Kean Walmsley

July 2009

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Twitter Updates

    follow me on Twitter



    « Robotic hatching inside AutoCAD using F# and .NET | Main | Pointing at clouds: more random musings on AutoCAD and F# »

    February 21, 2008

    Parallelizing robotic AutoCAD hatching with F# and .NET

    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.

    TrackBack

    TrackBack URL for this entry:
    http://www.typepad.com/services/trackback/6a00d83452464869e200e55072a8d98834

    Listed below are links to weblogs that reference Parallelizing robotic AutoCAD hatching with F# and .NET:

    Comments

    Hi Kean,

    Thanks for the nifty F# projects.

    Do you know if it's possible to get better Intellisense support in F# for AutoCAD objects? In particular, right-click Go To Declaration doesn't seem enabled like it is in C#.

    That's really part of the integration between F# and Visual Studio (which will certainly improve over time). It's not something that we - as providers of managed assemblies - have any control over.

    Kean

    Verify your Comment

    Previewing your Comment

    This is only a preview. Your comment has not yet been posted.

    Working...
    Your comment could not be posted. Error type:
    Your comment has been posted. Post another comment

    The letters and numbers you entered did not match the image. Please try again.

    As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

    Having trouble reading this image? View an alternate.

    Working...

    Post a comment

    Feed & Share

    Search