Kean Walmsley


  • About the Author
    Kean on Google+

July 2014

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    








« 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:

blog comments powered by Disqus

10 Random Posts