F#

June 04, 2009

Creating Fibonacci spirals in AutoCAD using F#

I recently stumbled across this post which inspired me to do something similar in AutoCAD (the fact that both posts cover Fibonacci spirals and use F# is about where the similarity ends - they do things quite differently).

Fibonacci spirals are an approximation of the golden spiral, which for old timers out there will be reminiscent of the AutoCAD R12 (it was R12, wasn’t it?) design collateral - the same as this one from AME 2.1 - which I still find cool after all these years. :-)

The first thing was to create a function that returns a portion of the Fibonacci sequence:

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

A few comments about this implementation:

  • I searched online for tail-recursive Fibonacci implementations (not that we’re likely to create a stack overflow with the number of recursions we’re going to do, but I like to do things right when I can :-)
    • Tail-recursive solutions are easy if returning a specific number, but as we need to return a portion of the Fibonacci sequence things get a little more complicated
    • Here’s a quick reminder of how we can check that tail call optimization has happened, when we do choose to use tail recursion
  • I ended up going for a lazily-evalulated solution, copied and modified from the Foundations of F# book by Robert Pickering
    • Unfold (OK – I admit this Wikipedia entry is beyond obscure for most of us mere mortals – this post may be of more help) applies a function to a seed value to create what may be an infinite sequence of numbers (which is precisely what the Fibonacci sequence is, of course)
    • We use the Seq data-type (essentially an IEnumerable in .NET) which is lazy
      • This means that it only actually evaluates the various items in the list as we ask for them
    • We then “take” the first n items from the list (n will be specified by the user), so only that number of items get evaluated
    • We convert the results to a list to return to the caller
  • I like the elegance of this solution and it’s certainly efficient enough for our purposes

If you load this code into F# interactive and execute it against the first 20 numbers in the sequence, you get:

> fibs 20;;

val it : bigint list

= [0I; 1I; 1I; 2I; 3I; 5I; 8I; 13I; 21I; 34I; 55I; 89I; 144I; 233I; 377I; 610I;

  987I; 1597I; 2584I; 4181I]

Otherwise the below implementation should be reasonably straightforward. We define a local addSegment function which we then call on each member of the subset of the Fibonacci sequence (in reverse, as we’re drawing the curves from large to small). We use the iteri function to do this, as it provides us with a useful index into the list which then allows us to decide which of the four directions we’re facing (the orientation of the arc rotates 90 degrees each time).

Here’s the complete F# code:

#light

 

// Declare our namespace and module name

 

module Fibonacci.Spiral

 

// Import managed assemblies

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open System

 

// A lazy Fibonacci sequence generator

 

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

 

[<CommandMethod("fib")>]

let fibonacciSpiral() =

 

  // Let's get the usual helpful AutoCAD objects

 

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

 

  // Ask the user how deep to go

 

  let pio =

    new PromptIntegerOptions("\nEnter number of levels: ")

  pio.AllowNone <- true

  pio.AllowZero <- false

  pio.AllowNegative <- false

  pio.DefaultValue <- 10

  pio.LowerLimit <- 1

  pio.UpperLimit <- 50

  pio.UseDefaultValue <- true

 

  let pir = ed.GetInteger(pio)

 

  if pir.Status = PromptStatus.OK then

 

    // We'll actually add one to the value provided

    // as this gives more logical results

 

    let levels = pir.Value + 1

 

    // "use" has the same effect as "using" in C#

 

    use tr =

      db.TransactionManager.StartTransaction()

 

    // Get appropriately-typed BlockTable and BTRs

 

    let bt =

      tr.GetObject

        (db.BlockTableId,OpenMode.ForRead)

      :?> BlockTable

 

    let ms =

      tr.GetObject

        (bt.[BlockTableRecord.ModelSpace],

        OpenMode.ForWrite)

      :?> BlockTableRecord

 

    // Create our polyline, set its defaults,

    // add it to the modelspace and the transaction

 

    let pl = new Polyline()

    pl.SetDatabaseDefaults()

    ms.AppendEntity(pl) |> ignore

    tr.AddNewlyCreatedDBObject(pl, true)

    pl.AddVertexAt

      (pl.NumberOfVertices, Point2d.Origin, 0.0, 0.0, 0.0)

 

    // We need a mutable start point variable for

    // each of our arcs to connect

 

    let start = ref Point3d.Origin

 

    // Add an arc segment to our polyline

 

    let addSegment i size =

 

      // i is the index in the list provided by iteri

 

      // Decide the directions of the "axes" and the arc's start

      // angle based on one of four possibilities

 

      let (xdir, ydir, startAngle) =

        match (i % 4) with

        | 0 -> (Vector3d.XAxis, Vector3d.YAxis, 0.0)

        | 1 -> (-Vector3d.YAxis, Vector3d.XAxis, Math.PI * 1.5)

        | 2 -> (-Vector3d.XAxis, -Vector3d.YAxis, Math.PI)

        | 3 -> (Vector3d.YAxis, -Vector3d.XAxis, Math.PI / 2.0)

        | _ -> failwith "Invalid modulus remainder!"

 

      // The end angle is 90 degrees from the start

 

      let endAngle = startAngle + Math.PI / 2.0

 

      // The center of the arc is bottom right-hand corner

      // of the box (direction goes along the bottom from

      // left to right, so we go "size" units along the

      // direction from the start point)

 

      let center = !start + xdir * float size

 

      // Bulge is defined as the tan of one quarter of the

      // included angle (and negative, as we're going

      // clockwise)

 

      let bulge = Math.Tan((endAngle - startAngle) / -4.0)

 

      // We need to convert our 3D start point to a 2D point

      // on the plane of the polyline

 

      let pos = (!start).Convert2d(pl.GetPlane())

 

      // Now we just add the vertex at the end and mutate our

      // start variable to contain the end of the arc

 

      pl.AddVertexAt(pl.NumberOfVertices, pos, bulge, 0.0, 0.0)

      start.contents <- center + ydir * float size

 

    // Here's where we plug it all together...

 

    // Get the first n fibonacci numbers, reverse the list and

    // call our function on each one (passing its index along,

    // too)

 

    fibs levels |> List.rev |> List.iteri addSegment

    tr.Commit()

Here’s what happens when we run the FIB command and select levels 1 to 8 (we need to call FIB eight times to do this), creating eight different Fibonacci spirals:

Levels 1 to 8 of our Fibonacci spiral

And here’s the result for level 50, as a comparison (although unless you zoom right in it might as well be level 20):

Level 50 Fibonacci spiral

June 02, 2009

Winner of the F# programming contest

Back at the beginning of the year I launched a programming contest for using F# with Autodesk products. A few months ago I introduced one of the winning entries showing how to use F# to implement Overrules in AutoCAD 2010. Now I’ve finally got around to unveiling the second winning entry.

And the winner is… drumroll…

Nada Amin, who entered her Master of Engineering project which uses F# with AutoCAD. Here is a description of the project from the MIT website:

Micado: an AutoCAD plug-in for programmable microfluidic chips

Programmable microfluidics, using multi-layer soft lithography, are lab-on-chip systems that can automate biological computations or experiments by integrating a diverse set of biological sensors and by manipulating fluids at the picoliter scale.

Micado is a Computer-Aided Design (CAD) tool for designing and controlling programmable microfluidics, featuring:

  • standard and customizable design rules
  • automatic routing between control valves and punches
  • automatic generation of control instructions and GUI

The code is hosted here (under which you will find the majority of the code having been written in F#).

Nada is now based over in Zürich, so I hope someday to meet with her to discuss the project in person, but in the meantime I’ll be shipping across a copy of Expert F#.

Congratulations, Nada! :-)

March 27, 2009

Customizing the display of standard AutoCAD objects using F#

This post is one of the winning entries of the F# programming contest started at the beginning of the year. It was submitted by an old friend of mine, Qun Lu, who also happens to be a member of the AutoCAD engineering team, and makes use of a new API in AutoCAD 2010: the somewhat ominously-named Overrule API.

The Overrule API is really (and I mean really, really) cool. Yes, I know: another really cool API in AutoCAD 2010? Well, I’m honestly not one to hype things up, but I do have a tendency to get excited by technology that has incredibly interesting capabilities with a relatively low barrier of entry. And the Overrule API is one of those APIs. It’s the answer to the question posed in this previous post, which raises concerns about translating the power and complexity of custom objects to the world of .NET:

So what’s the right thing to do? Clearly we could just go ahead and expose the mechanism as it is today in ObjectARX. And yet here we are with a technology we know to be highly complex and difficult to implement, and an ideal opportunity to redesign it – enabling more people to harness it effectively at lower effort. The more favoured approach (at least from our perspective) would be to investigate further how better to meet developers’ needs for enabling custom graphics/behaviour (a.k.a. stylization) in AutoCAD – in a way that could be supported technically for many releases to come.

The Overrule API allows you to hook into the display and other aspects of the behaviour of entities inside AutoCAD. The below example is a great example: when enabled, the code overrules the display of lines and circles, to make them into coloured pipes. And all with very little code (which would also be true if the code were in C# or VB.NET).

Here’s the F# code:

#light

 

module DrawOverrule.Commands

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.GraphicsInterface

open Autodesk.AutoCAD.Colors

 

type public DrawOverrule public () as this =

  inherit DrawableOverrule()

 

  static member public theOverrule =

    new DrawOverrule()

 

  static member private Radius = 0.5

 

  member private this.sweepOpts = new SweepOptions()

 

  override this.WorldDraw (d : Drawable, wd : WorldDraw) =

    match d with

      // Type-test and cast. If succeeds, cast to "line"

      | :? Line as line ->

        // Draw the line as is, with overruled attributes

        base.WorldDraw(line, wd) |> ignore

        if not line.Id.IsNull && line.Length > 0.0 then

          // Draw a pipe around the line

          let c = wd.SubEntityTraits.TrueColor

          wd.SubEntityTraits.TrueColor <-

            new EntityColor(0x00AfAfff)

          wd.SubEntityTraits.LineWeight <-

            LineWeight.LineWeight000

          let clr =

            new Circle

              (line.StartPoint, line.EndPoint-line.StartPoint,

              DrawOverrule.Radius)

          let pipe = new ExtrudedSurface()

          try

            pipe.CreateExtrudedSurface

              (clr, line.EndPoint-line.StartPoint, this.sweepOpts)

          with

            | e -> printfn("Failed with CreateExtrudedSurface")

          clr.Dispose()

          pipe.WorldDraw(wd) |> ignore

          pipe.Dispose()

          wd.SubEntityTraits.TrueColor <- c

        true

      | :? Circle as circle ->

        // Draw the circle as is, with overruled attributes

        base.WorldDraw(circle, wd) |> ignore

 

        // needed to avoid ill-formed swept surface

        if circle.Radius > DrawOverrule.Radius then

          // draw a pipe around the cirle

          let c = wd.SubEntityTraits.TrueColor

          wd.SubEntityTraits.TrueColor <-

            new EntityColor(0x3fffe0e0)

          wd.SubEntityTraits.LineWeight <-

            LineWeight.LineWeight000

          let normal =

            (circle.Center-circle.StartPoint).

              CrossProduct(circle.Normal)

          let clr =

            new Circle

              (circle.StartPoint, normal, DrawOverrule.Radius)

          let pipe = new SweptSurface()

          pipe.CreateSweptSurface(clr, circle, this.sweepOpts)

          clr.Dispose()

          pipe.WorldDraw(wd) |> ignore

          pipe.Dispose()

          wd.SubEntityTraits.TrueColor <- c

        true

      | _ ->

        base.WorldDraw(d, wd)

 

  override this.SetAttributes (d : Drawable, t : DrawableTraits) =

    let b = base.SetAttributes(d, t)

    match d with

      | :? Line ->

        // If d is LINE, set color to index 6

        t.Color <- 6s

        // and lineweight to .40 mm

        t.LineWeight <- LineWeight.LineWeight040

      | :? Circle ->   

        // If d is CIRCLE, set color to index 2

        t.Color <- 2s

        // and lineweight to .60 mm

        t.LineWeight <- LineWeight.LineWeight060

      | _ -> ()

    b

 

let Overrule enable =

  // Regen to see the effect

  // (turn on/off Overruling and LWDISPLAY)

  DrawableOverrule.Overruling <- enable

  match enable with

    | true -> Application.SetSystemVariable("LWDISPLAY", 1)

    | false -> Application.SetSystemVariable("LWDISPLAY", 0)

  let doc =

    Application.DocumentManager.MdiActiveDocument

  doc.SendStringToExecute("REGEN3\n", true, false, false)

  doc.Editor.Regen()

 

// Now we declare our commands

 

[<CommandMethod("overrule1")>]

let OverruleStart() =

  // Targeting all Drawables, but only affects Lines and Circles

  ObjectOverrule.AddOverrule

    (RXClass.GetClass(typeof<Drawable>),

    DrawOverrule.theOverrule, true)

  Overrule(true)

 

[<CommandMethod("overrule0")>]

let OverruleEnd() =

  Overrule(false)

Here’s what happens when we load the application, turn the overrule on using the OVERRULE1 command (OVERRULE0 is the command to turn the overrule off – it’s details like this that tell you Qun’s a real programmer… ;-) and draw some lines and circles:

Overruled 2D wireframe display of lines and circles

Even in a 3D view – this time with the realistic visual style applied – you get the piping effect when you draw simple geometry:

Overruled 3D display of lines and circles

To be clear: these are standard AutoCAD lines and circles. When you use the OVERRULE0 command to disable the overrule, they revert to their original form:

Standard display of lines and circles

I expect to follow this post – in time – with various more harnessing the power of this very cool API. If you have questions or ideas about how it might be used, be sure to post a comment.

Thanks & congratulations, Qun! Your copy of “Expert F#” is on its way to you via inter-office mail. :-) More soon on the other winning entry…

March 20, 2009

Using IronPython with AutoCAD

I’ve been meaning to play around with the Python language for somePython Logo time, now, and with the recent release of IronPython 2 it seems a good time to start.

Why Python? A number of people in my team – including Jeremy Tammik and the people within our Media  & Entertainment workgroup who support Python’s use with Maya and MotionBuilder – are fierce proponents of the language. I’m told that it’s an extremely easy, general-purpose, dynamic programming language. All of which sounds interesting, of course, although I have to admit I’m less convinced of the importance of the dynamic piece: I’ve found a lot of value in static typing over the years (even F# is statically typed, although many people – even some who work with it - don’t realise this… its type inference system allows you to code safely without specifying types all over the place).

Let’s take a quick step back and talk about what makes a language dynamic. The most common example of a dynamic language – one that I’m sure most of you will have touched at some point – is JavaScript. In JavaScript you declare everything as a var, assign it, call methods on it and hope that they work at runtime. I admit that I’ve always disliked developing in JavaScript because of the lack of decent tool support: I’m a big fan of Intellisense (based on an object’s design-time type) and want the compiler to tell me if I’m dealing with an object that doesn’t support a particular method. But perhaps that’s largely what I’ve become used to from modern development tools, and I’m trying to remain open to new things. Really, I am.

Another dynamic language with which I’ve had much more favourable (but still, at times, frustrating) experiences is LISP. But my relationship with LISP is different: like most early AutoCAD programmers I adopted it out of necessity – and at the time I started with it programming environments were, in any case, generally very basic - I then grew to love it and have since never forgotten it, even when more attractive/productive development environments came along. So I’m extremely loathe to paint it with the same brush as the one I’ve used for JavaScript.

Python is also of interest because of its cross-platform availability: it’s an open source language with its roots in the UNIX/Linux world, but is now gaining popularity across a variety of OS platforms (one of the reasons it’s the scripting language chosen for at least one of our cross-platform products, Autodesk Maya).

Microsoft is definitely now very open to the possibilities of dynamic languages: they’re making a significant investment in the Dynamic Language Runtime, to support languages such as IronPython and IronRuby, as well as adding more dynamic features with C# 4.0 (which is finally going to get something comparable to VB’s “late binding” capability).

So all in all, the world we live in seems to be becoming increasingly dynamic. :-)

Anyway – now on to getting IronPython working with AutoCAD. I had originally hoped to build a .NET assembly directly using IronPython – something that appears to have been enabled with the 2.0 release of IronPython - which could then be loaded into AutoCAD. Unfortunately this was an exercise in frustration: AutoCAD makes heavy use of custom attributes for identifying commands etc., but IronPython doesn’t currently support the use of attributes. It is possible to do some clever stuff by compiling attributed C# on-the-fly and deriving classes from it (information on this is available here), which will – in theory, at least – get you something in memory that’s attributed but, as AutoCAD scans the physical assembly for custom attributes before loading it, this didn’t help. I also spent a great deal of time just trying to derive a class from Autodesk.AutoCAD.Runtime.IExtensionApplication – to have the Initialize() function called automatically on load – but I just couldn’t get this to work, either.

Then, thankfully, Tim Riley came to the rescue: we’ve been in touch on and off over the years since he started the PyAcad.NET project to run IronPython code inside AutoCAD, and Tim was able to put together some working code which actually registered commands (after I’d pointed him at a function he could use from AutoCAD 2009’s acmgdinternal.dll – an unsupported assembly that exposes some otherwise quite helpful functions). He ended up choosing an implementation that had also been suggested to me by Albert Szilvasy: to implement a PYLOAD command using C# which allows selection and loading of a Python script (because Python is, ultimately, all about scripting rather than building static, compiled assemblies).

Before we get on to the C# module, I should point out that I installed IronPython 2.0.1 as well as IronPython Studio 1.0 for the Visual Studio 2008 integration. It turns out that as we’re relying on C# to manage the loading of Python – rather than compiling a .NET assembly – the main advantage of IronPython Studio is around the ability to work with Python source code inside Visual Studio.

To build the below C# code into a standard .NET Class Library assembly (a .DLL) you’ll need to add assembly references to IronPython.dll, IronPython.Modules.dll, Microsoft.Scripting.dll and Microsoft.Scripting.Core.dll – all of which can be found in the main IronPython install folder (on my system this is in “C:\Program Files\IronPython 2.0.1”). As well as the standard references to acmgd.dll and acdbmgd.dll, of course.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.EditorInput;

using IronPython.Hosting;

using Microsoft.Scripting.Hosting;

using System;

 

namespace PythonLoader

{

  public class CommandsAndFunctions

  {

    [CommandMethod("-PYLOAD")]

    public static void PythonLoadCmdLine()

    {

      PythonLoad(true);

    }

 

    [CommandMethod("PYLOAD")]

    public static void PythonLoadUI()

    {

      PythonLoad(false);

    }

 

    public static void PythonLoad(bool useCmdLine)

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

 

      short fd =

        (short)Application.GetSystemVariable("FILEDIA");

 

      // As the user to select a .py file

 

      PromptOpenFileOptions pfo =

          new PromptOpenFileOptions(

            "Select Python script to load"

          );

      pfo.Filter = "Python script (*.py)|*.py";

      pfo.PreferCommandLine =

        (useCmdLine || fd == 0);

      PromptFileNameResult pr =

        ed.GetFileNameForOpen(pfo);

 

      // And then try to load and execute it

 

      if (pr.Status == PromptStatus.OK)

        ExecutePythonScript(pr.StringResult);

    }

 

    [LispFunction("PYLOAD")]

    public ResultBuffer PythonLoadLISP(ResultBuffer rb)

    {

      const int RTSTR = 5005;

 

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

 

      if (rb == null)

      {

        ed.WriteMessage("\nError: too few arguments\n");

      }

      else

      {

        // We're only really interested in the first argument

 

        Array args = rb.AsArray();

        TypedValue tv = (TypedValue)args.GetValue(0);

 

        // Which should be the filename of our script

 

        if (tv != null && tv.TypeCode == RTSTR)

        {

          // If we manage to execute it, let's return the

          // filename as the result of the function

          // (just as (arxload) does)

 

          bool success =

            ExecutePythonScript(Convert.ToString(tv.Value));

          return

            (success ?

              new ResultBuffer(

                new TypedValue(RTSTR, tv.Value)

              )

              : null);

        }

      }

      return null;

    }

 

    private static bool ExecutePythonScript(string file)

    {

      // If the file exists, let's load and execute it

      // (we could/should probably add some more robust

      // exception handling here)

 

      bool ret = System.IO.File.Exists(file);

      if (ret)

      {

        ScriptEngine engine = Python.CreateEngine();

        engine.ExecuteFile(file);

      }

      return ret;

    }

  }

}

The code behind the PYLOAD command is actually really simple. I could have kept it basic but decided it would be a good opportunity to show some best practices. So not only do we have the standard PYLOAD command, which respects the FILEDIA variable to decide whether to use dialogs or the command-line, we also have a command-line version –PYLOAD and a LISP function (pyload). All of which call into the same function to load a Python script.

OK, now let’s take a look at a simple IronPython script that calls into AutoCAD via its .NET API. Thanks again to Tim Riley for providing something that works. Even with Python being (apparently) so easy to learn, I’m such a neophyte that without his help I’d still be stumbling around in the dark.

import clr

path = 'C:\\Program Files\\Autodesk\\AutoCAD 2009\\'

clr.AddReferenceToFileAndPath(path + 'acdbmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgdinternal.dll')

 

import Autodesk

import Autodesk.AutoCAD.Runtime as ar

import Autodesk.AutoCAD.ApplicationServices as aas

import Autodesk.AutoCAD.DatabaseServices as ads

import Autodesk.AutoCAD.Geometry as ag

import Autodesk.AutoCAD.Internal as ai

from Autodesk.AutoCAD.Internal import Utils

 

# Function to register AutoCAD commands

# To be used via a function decorator

 

def autocad_command(function):

 

    # First query the function name

    n = function.__name__

 

    # Create the callback and add the command

    cc = ai.CommandCallback(function)

    Utils.AddCommand('pycmds', n, n, ar.CommandFlags.Modal, cc)

 

    # Let's now write a message to the command-line

    doc = aas.Application.DocumentManager.MdiActiveDocument

    ed = doc.Editor

    ed.WriteMessage("\nRegistered Python command: {0}", n)

 

# A simple "Hello World!" command

 

@autocad_command

def msg():

    doc = aas.Application.DocumentManager.MdiActiveDocument

    ed = doc.Editor

    ed.WriteMessage("\nOur test command works!")

 

# And one to do something a little more complex...

# Adds a circle to the current space

 

@autocad_command

def mycir():

 

    doc = aas.Application.DocumentManager.MdiActiveDocument

    db = doc.Database

 

    tr = doc.TransactionManager.StartTransaction()

    bt = tr.GetObject(db.BlockTableId, ads.OpenMode.ForRead)

    btr = tr.GetObject(db.CurrentSpaceId, ads.OpenMode.ForWrite)

 

    cir = ads.Circle(ag.Point3d(10,10,0),ag.Vector3d.ZAxis, 2)

 

    btr.AppendEntity(cir)

    tr.AddNewlyCreatedDBObject(cir, True)

 

    tr.Commit()

    tr.Dispose()

As we’re stuck without the ability to use custom attributes in IronPython, we’re making use of the Autodesk.AutoCAD.Internal namespace to register commands at runtime. I don’t like doing this, but at the same time I was left with little choice, unless we choose to find another way to call into the code. Please be warned that anything contained in the Autodesk.AutoCAD.Internal namespace is unsupported functionality, and subject to change without warning.

Now that I have that off my chest, let’s comment a little further on the above code…

  • Even without custom attributes, we have used a pretty cool Python language feature known as decorators (thanks *again* for the tip, Tim :-) which helps us to mark functions as commands. The autocad_command function is called for each decorated function, and this is where we register a command for the function based on the function’s name. Pretty cool.
  • You’ll notice a distinct lack of types in the code (and yes, that still scares me). When I was previously trying to compile a DLL based on this code, I had a lot of trouble getting anything at all to fail at compile-time, but clearly a lot would fail at runtime (when I could actually get anything to execute :-S). I feel as though I still need to get my head around this trade-off: I can see the argument for simplicity/elegance/succinctness – and even the power it brings in some situations - but the Computer Scientist in me is screaming for safety/reliability/determinism/debuggability (if that’s even a word). Oh well. The main thing is that I’m starting the journey, at least: we’ll see if it ends up somewhere I like. :-)

When we build and NETLOAD our PythonLoader C# application and execute the PYLOAD command, we can select our Python script:

Command: PYLOAD

File selection during the PYLOAD command

Once selected, the script gets loaded and should register a couple of commands:

Registered Python command: msg

Registered Python command: mycir

Running the MSG command will execute a simple “Hello World!”-like function, just printing a message to the command-line:

Command: MSG

Our test command works!

And running the MYCIR command should just add a simple circle to the current space in the active drawing.

Command: MYCIR

Results of the MYCIR command

That’s it for my initial foray into the world of Python. I hope you’ve found this helpful and enjoy playing around with the Python programming language inside AutoCAD. Please do post a comment if you have experiences or anecdotes to share on this topic!

February 26, 2009

Reminder: F# programming contest deadline looming...

There are just a few days left before the February 28th deadline of the F# programming contest. Thanks to those of you who have already submitted entries!

At least one potential prize is still up for grabs - beyond those I've ear-marked for existing entrants - so if you're working on something and plan on submitting it in the next week or two (even if you won't be able to meet the original deadline for the actual submission), then please let me know by email. If you haven't notified me by Saturday that you have something in the works then I'm afraid there's a good chance you won't be in the running for a prize.

February 02, 2009

Parallelized pixelization inside AutoCAD using F#

As promised in the last post, we're now going to look at how to change the code to make the colour averaging routine work in parallel. The overall performance is marginally better on my dual-core machine, but I fully expect it to get quicker and quicker as the number of cores multiply.

To start with, though, here's the modified "synchronous" version of the code - as I went through making the code work in parallel, I noticed a bunch of general enhancements that were applicable to both versions. Here's the updated F# code:

// Use lightweight F# syntax

 

#light

 

// Declare a specific namespace and module name

 

module SyncPixelizer.Commands

 

// Import managed assemblies

 

#nowarn "9" // ... because we're using NativePtr

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.Colors

open System.Drawing.Imaging

open Microsoft.FSharp.NativeInterop

 

// Add up the RGB values of a list of pixels

 

// We use a recursive function with an accumulator argument,

// (rt, gt, bt), to allow tail call optimization

 

let rec sumColors (pix : List<(byte*byte*byte)>) (rt,gt,bt) =

  match pix with

    | [] -> (rt, gt, bt)

    | (r, g, b) :: tl ->

        sumColors tl

          (rt + Byte.to_int r,

          gt + Byte.to_int g,

          bt + Byte.to_int b)

 

// Average out the RGB values of a list of pixels

 

let getAverageColour (pixels : List<(byte*byte*byte)>) =

  let (rsum, gsum, bsum) =

    sumColors pixels (0, 0, 0)

  let count = pixels.Length

  let ravg = Byte.of_int (rsum / count)

  let gavg = Byte.of_int (gsum / count)

  let bavg = Byte.of_int (bsum / count)

 

  // For some reason the pixel needs ro be reversed - probably

  // because of the bitmap format (needs investigation)

 

  Color.FromRgb(bavg, gavg, ravg)

 

// Function to get an index into our flat array

// from an x,y pair

 

let getIndexFromXY ysize x y =

  (x * ysize) + y

 

//  Get a chunk of pixels to average from one row

 

// We use a recursive function with an accumulator argument

// to allow tail call optimization

 

let rec getChunkRowPixels p xsamp acc =

  if xsamp = 0 then

    acc

  else

    let pix =

      [(NativePtr.get p 0,

        NativePtr.get p 1,

        NativePtr.get p 2)]

    let p = NativePtr.add p 3 // We do *not* mutate here

    getChunkRowPixels p (xsamp-1) (pix @ acc)

 

// Get a chunk of pixels to average from multiple rows

 

// We use a recursive function with an accumulator argument

// to allow tail call optimization

 

let rec getChunkPixels p stride xsamp ysamp acc =

  if ysamp = 0 then

    acc

  else

    let pix = getChunkRowPixels p xsamp []

    let p = NativePtr.add p stride  // We do *not* mutate here

    getChunkPixels p stride xsamp (ysamp-1) (pix @ acc)

 

// Get the various chunks of pixels to average across

// a complete bitmap image

 

let pixelizeBitmap (image:System.Drawing.Bitmap) xsize ysize =

 

  // Create a 1-dimensional array of pixel lists (one list,

  // which then needs averaging, per final pixel)

 

  let (arr : List<(byte*byte*byte)>[]) =

    Array.create (xsize * ysize) []

 

  // Lock the entire memory block related to our image

 

  let bd =

    image.LockBits

      (System.Drawing.Rectangle

        (0, 0, image.Width ,image.Height),

      ImageLockMode.ReadOnly, image.PixelFormat)

 

  // Establish the number of pixels to sample per chunk

  // in each of the x and y directions

 

  let xsamp = image.Width / xsize

  let ysamp = image.Height / ysize

 

  // We have a mutable pointer to step through the image

 

  let mutable (p:nativeptr<byte>) =

    NativePtr.of_nativeint (bd.Scan0)

 

  // Loop through the various chunks

 

  for i = 0 to ysize - 1 do

 

    // We take a copy of the current value of p, as we

    // don't want to mutate p while extracting the pixels

    // within a row

 

    let mutable xp = p

    for j = 0 to xsize - 1 do

 

      // Get the square chunk of pixels starting at

      // this x,y position

 

      let chk =

        getChunkPixels xp bd.Stride xsamp ysamp []

 

      // Add it into our array

 

      let idx = getIndexFromXY ysize j (ysize-1-i)

      arr.[idx] <- chk

 

      // Mutate the pointer to move along to the right

      // by a value of 3 (our RGB value) times the

      // number of pixels we're sampling in x

 

      xp <- NativePtr.add xp (xsamp * 3)

    done

 

    // Mutate the original p pointer to move on one row

 

    p <- NativePtr.add p (bd.Stride * ysamp)

  done

 

  // Finally unlock the bitmap data and return the array

 

  image.UnlockBits(bd)

  arr

 

// Create an array of ObjectIds from a collection

 

let getIdArray (ids : ObjectIdCollection) =

  [| for i in [0..ids.Count-1] -> ids.[i] |]

 

// Declare our command

 

[<CommandMethod("pix")>]

let pixelize() =

 

  // Let's get the usual helpful AutoCAD objects

 

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

 

  // Prompt the user for the file and the width of the image

 

  let pofo =

    new PromptOpenFileOptions

      ("Select an image to import and pixelize")

  pofo.Filter <-

    "Jpeg Image (*.jpg)|*.jpg|All files (*.*)|*.*"

  let pfnr = ed.GetFileNameForOpen(pofo)

 

  let file =

    match pfnr.Status with

    | PromptStatus.OK ->

        pfnr.StringResult

    | _ ->

        ""

 

  if System.IO.File.Exists(file) then

 

    let img = System.Drawing.Image.FromFile(file)

 

    let pio =

      new PromptIntegerOptions

        ("\nEnter number of horizontal pixels: ")

    pio.AllowNone <- true

    pio.UseDefaultValue <- true

    pio.LowerLimit <- 1

    pio.UpperLimit <- img.Width

    pio.DefaultValue <- 100

 

    let pir = ed.GetInteger(pio)

    let xsize =

      match pir.Status with

        | PromptStatus.None ->

            img.Width

        | PromptStatus.OK ->

            pir.Value

        | _ -> -1

 

    if xsize > 0 then

 

      // Calculate the vertical size from the horizontal

 

      let ysize = img.Height * xsize / img.Width

 

      if ysize > 0 then

 

        // We'll time the command, so we can check the

        // sync vs. async efficiency

 

        let starttime = System.DateTime.Now

 

        // "use" has the same effect as "using" in C#

 

        use tr =

          db.TransactionManager.StartTransaction()

 

        // Get appropriately-typed BlockTable and BTRs

 

        let bt =

          tr.GetObject

            (db.BlockTableId,OpenMode.ForRead)

          :?> BlockTable

 

        let ms =

          tr.GetObject

            (bt.[BlockTableRecord.ModelSpace],

            OpenMode.ForWrite)

          :?> BlockTableRecord

 

        // Function to create a filled circle (hatch) at a

        // specific location

 

        // Note the valid use of tr and ms, as they are in scope

 

        let createCircle pt rad =

          let hat = new Hatch()

          hat.SetDatabaseDefaults()

          hat.SetHatchPattern

            (HatchPatternType.PreDefined,

            "SOLID")

 

          let id = ms.AppendEntity(hat)

          tr.AddNewlyCreatedDBObject(hat, true)

 

          // Now we create the loop, which we make db-resident

          // (appending a transient loop caused problems, so

          // we're going to use the circle and then erase it)

 

          let cir = new Circle()

          cir.Radius <- rad

          cir.Center <- pt

 

          let lid = ms.AppendEntity(cir)

          tr.AddNewlyCreatedDBObject(cir, true)

 

          // Have the hatch use the loop we created

 

          let loops = new ObjectIdCollection()

          loops.Add(lid) |> ignore

          hat.AppendLoop(HatchLoopTypes.Default, loops)

          hat.EvaluateHatch(true)

 

          // Now we erase the loop

 

          cir.Erase()

          id

 

        // Function to create our grid of circles

 

        let createGrid xsize ysize rad offset =

          let ids = new ObjectIdCollection()

          for i = 0 to xsize - 1 do

            for j = 0 to ysize - 1 do

              let pt =

                new Point3d

                  (offset * (Int32.to_float i),

                   offset * (Int32.to_float j),

                   0.0)

              let id = createCircle pt rad

              ids.Add(id) |> ignore

          ids

 

        // Function to change the colour of an entity

 

        let changeColour (id : ObjectId) (col : Color) =

          if id.IsValid then

            let ent =

              tr.GetObject(id, OpenMode.ForWrite) :?> Entity

            ent.Color <- col

 

        // Create our basic grid

 

        let ids = createGrid xsize ysize 0.5 1.2

 

        // Cast our image to a bitmap and then

        // get the chunked pixels

 

        let bmp = img :?> System.Drawing.Bitmap

        let arr = pixelizeBitmap bmp xsize ysize

 

        // Loop through the pixel list and average them out

        // (which could be parallelized), using the results

        // to change the colour of the circles in our grid

 

        Array.map getAverageColour arr |>

          Array.iter2 changeColour (getIdArray ids)

 

        // Commit the transaction

 

        tr.Commit()

 

        // Check how long it took

 

        let elapsed =

          System.DateTime.op_Subtraction

            (System.DateTime.Now, starttime)

 

        ed.WriteMessage

          ("\nElapsed time: " + elapsed.ToString())

To change this to run the colour averaging asynchronously (in parallel, if you have the cores) is really simple. We replace one line of code "Array.map getAverageColour arr" with the following:

Async.Run

  (Async.Parallel

    [ for a in arr ->

        async { return getAverageColour a }])

This essentially performs a parallel array map (albeit a somewhat naive one), returning basically the same results as the previous line - just hopefully a little more quickly. In case you want to build the two files into one project to test them side-by-side, here they are, the synchronous and asynchronous versions, with the changes needed to allow them to live in and execute from the same assembly.

Here's one more image that's been processed by the PIX command:

Pixelized Swiss Air logo

In case you're interested, the original image can be found here.

February 01, 2009

Importing and pixelizing images inside AutoCAD using F#

A friend and esteemed colleague asked - very validly - why I decided to use circles on a grid to display the results of a mathematical function in this last post, rather than using a linear object of some kind. Well I did, in fact, have a plan in mind... :-)

This post extends the concept, introduced in that post, of displaying data in a grid of solid-hatched circles. This post focuses on importing a bitmap image from a file, pixelizing the contents and using the "averaged" pixel colours to modify our grid. The idea actually came to me during an R.E.M. concert I attended at Paléo, a Swiss music festival, last summer. The real-time manipulation performed on the live video feed of the band - and especially of Michael Stipe, the lead singer - which was then projected onto screens adjacent to the stage was really, really cool. They managed to pixelize and manipulate the colours in a way that I found incredible - it was like seeing real-time graphic design at work. I understand that much of the work is done in advance, but even so I found it very impressive. Some of you regular concert-goers may find what I've just described to be pretty run-of-the-mill, but I fully admit that these days - what with one thing and another - I don't get out much. :-)

The pixelization approach I decided to take was to read in square chunks of the bitmap image and then average out the RGB values for all the pixels in each square. These average values are then used to colour the circles representing the "pixels" for their respective squares. You'll notice some inadvertent cropping of the imported image, which happens because we ask for the width in terms of our circular pixels and then sample the bitmap in chunks that have a whole number of pixels on each side: if the picture's width is not exactly divisible by the width entered there will be a little cropping.

Why did I choose F# for this rather than C#? The image processing domain in general is a strong fit for functional programming techniques. And while I haven't yet taken the step in this post, certain parts of the below code are inherently parallelizable, especially the operations related to averaging of pixel colours. The reading of the bitmap itself might prove more difficult, as it uses "unsafe" direct memory access (via the NativePtr class), but it's by no means impossible to parallelize, at least in theory.

Anyway, here's the F# code I put together:

// Use lightweight F# syntax


#light


// Declare a specific namespace and module name


module Pixelizor.Commands


// Import managed assemblies


#nowarn "9" // ... because we're using NativePtr


open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.Colors

open System.Drawing.Imaging

open Microsoft.FSharp.NativeInterop


// Declare our command


[<CommandMethod("pix")>]

let pixelate() =


  // Let's get the usual helpful AutoCAD objects


  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database


  // "use" has the same effect as "using" in C#


  use tr =

    db.TransactionManager.StartTransaction()


  // Get appropriately-typed BlockTable and BTRs


  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable


  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

      OpenMode.ForWrite)

    :?> BlockTableRecord


  // Function to create a filled circle (hatch) at a

  // specific location


  // Note the valid use of tr and ms, as they are in scope


  let createCircle pt rad =

    let hat = new Hatch()

    hat.SetDatabaseDefaults()

    hat.SetHatchPattern

      (HatchPatternType.PreDefined,

      "SOLID")


    let id = ms.AppendEntity(hat)

    tr.AddNewlyCreatedDBObject(hat, true)


    // Now we create the loop, which we make db-resident

    // (appending a transient loop caused problems, so

    // we're going to use the circle and then erase it)


    let cir = new Circle()

    cir.Radius <- rad

    cir.Center <- pt


    let lid = ms.AppendEntity(cir)

    tr.AddNewlyCreatedDBObject(cir, true)


    // Have the hatch use the loop we created


    let loops = new ObjectIdCollection()

    loops.Add(lid) |> ignore

    hat.AppendLoop(HatchLoopTypes.Default, loops)

    hat.EvaluateHatch(true)


    // Now we erase the loop


    cir.Erase()

    id


  // Function to create our grid of circles


  let createGrid xsize ysize rad offset =

    let ids = new ObjectIdCollection()

    for i = 0 to xsize - 1 do

      for j = 0 to ysize - 1 do

        let pt =

          new Point3d

            (offset * (Int32.to_float i),

            offset * (Int32.to_float j),

            0.0)

        let id = createCircle pt rad

        ids.Add(id) |> ignore

    ids


  // Function to change the colour of an entity


  let changeColour (col : Color) (id : ObjectId) =

    if id.IsValid then

      let ent =

        tr.GetObject(id, OpenMode.ForWrite) :?> Entity

      ent.Color <- col


  // Add up the RGB values of a list of pixels


  // We use a recursive function with an accumulator argument,

  // (rt, gt, bt), to allow tail call optimization


  let rec sumColors (pix : List<(byte*byte*byte)>) (rt,gt,bt) =

    match pix with

      | [] -> (rt, gt, bt)

      | (r, g, b) :: tl ->

          sumColors tl

            (rt + Byte.to_int r,

            gt + Byte.to_int g,

            bt + Byte.to_int b)


  // Average out the RGB values of a list of pixels


  let getAverageColour (pixels : List<(byte*byte*byte)>) =

    let (rsum, gsum, bsum) =

      sumColors pixels (0, 0, 0)

    let count = pixels.Length

    let ravg = Byte.of_int (rsum / count)

    let gavg = Byte.of_int (gsum / count)

    let bavg = Byte.of_int (bsum / count)


    // For some reason the pixel needs ro be reversed - probably

    // because of the bitmap format (needs investigation)


    Color.FromRgb(bavg, gavg, ravg)


  //  Get a chunk of pixels to average from one row


  // We use a recursive function with an accumulator argument

  // to allow tail call optimization


  let rec getChunkRowPixels p xsamp acc =

    if xsamp = 0 then

      acc

    else

      let pix =

        [(NativePtr.get p 0,

          NativePtr.get p 1,

          NativePtr.get p 2)]

      let p = NativePtr.add p 3

      getChunkRowPixels p (xsamp-1) (pix @ acc)


  // Get a chunk of pixels to average from multiple rows


  // We use a recursive function with an accumulator argument

  // to allow tail call optimization


  let rec getChunkPixels p stride xsamp ysamp acc =

    if ysamp = 0 then

      acc

    else

      let pix = getChunkRowPixels p xsamp []

      let p = NativePtr.add p stride

      getChunkPixels p stride xsamp (ysamp-1) (pix @ acc)


  // Get the various chunks of pixels to average across

  // a complete bitmap image


  let pixelateBitmap (image:System.Drawing.Bitmap) xsize ysize =


    // Create a 2-dimensional array of pixel lists (one list,

    // which then needs averaging, per final pixel)


    let (arr2 : List<(byte*byte*byte)>[,]) =

      Array2.create xsize ysize []


    // Lock the entire memory block related to our image


    let bd =

      image.LockBits

        (System.Drawing.Rectangle

          (0, 0, image.Width ,image.Height),

        ImageLockMode.ReadOnly, image.PixelFormat)


    // Establish the number of pixels to sample per chunk

    // in each of the x and y directions


    let xsamp = image.Width / xsize

    let ysamp = image.Height / ysize


    // We have a mutable pointer to step through the image


    let mutable (p:nativeptr<byte>) =

      NativePtr.of_nativeint (bd.Scan0)


    // Loop through the various chunks


    for i = 0 to ysize - 1 do


      // We take a copy of the current value of p, as we

      // don't want to mutate p while extracting the pixels

      // within a row


      let mutable xp = p

      for j = 0 to xsize - 1 do


        // Get the square chunk of pixels starting at

        // this x,y position


        let chk =

          getChunkPixels xp bd.Stride xsamp ysamp []


        // Add it into our array


        arr2.[j,ysize-1-i] <- chk


        // Mutate the pointer to move along to the right

        // by a value of 3 (our RGB value) times the

        // number of pixels we're sampling in x


        xp <- NativePtr.add xp (xsamp * 3)

      done


      // Mutate the original p pointer to move on one row


      p <- NativePtr.add p (bd.Stride * ysamp)

    done


    // Finally unlock the bitmap data and return the array


    image.UnlockBits(bd)

    arr2


  // Prompt the user for the file and the width of the image


  let pofo =

    new PromptOpenFileOptions

      ("Select an image to import and pixelate")

  pofo.Filter <-

    "Jpeg Image (*.jpg)|*.jpg|All files (*.*)|*.*"

  let pfnr = ed.GetFileNameForOpen(pofo)


  let file =

    match pfnr.Status with

    | PromptStatus.OK ->

        pfnr.StringResult

    | _ ->

        ""


  if System.IO.File.Exists(file) then


    let img = System.Drawing.Image.FromFile(file)


    let pio =

      new PromptIntegerOptions

        ("\nEnter number of horizontal pixels: " )

    pio.AllowNone <- true

    pio.UseDefaultValue <- true

    pio.LowerLimit <- 1

    pio.UpperLimit <- img.Width

    pio.DefaultValue <- 100


    let pir = ed.GetInteger(pio)

    let xsize =

      match pir.Status with

        | PromptStatus.None ->

            img.Width

        | PromptStatus.OK ->

            pir.Value

        | _ -> -1


    if xsize > 0 then


      // Calculate the vertical size from the horizontal


      let ysize = img.Height * xsize / img.Width


      if ysize > 0 then


        // Create our basic grid


        let ids = createGrid xsize ysize 0.5 1.2


        // Some helper functions using values we've just set...


        // From a certain index in the list, get an object ID


        let getId i =

          if i >= 0 then

            ids.[i]

          else

            ObjectId.Null


        // From a certain x and y in the grid, get an object ID


        let getId x y =

          getId ((x * ysize) + y)


        // Cast our image to a bitmap and then

        // get the chunked pixels


        let bmp = img :?> System.Drawing.Bitmap

        let arr = pixelateBitmap bmp xsize ysize


        // Loop through the pixel list and average them out

        // (which could be parallelized), using the results

        // to change the colour of the circles in our grid


        for x = 0 to xsize - 1 do

          for y = 0 to ysize - 1 do

            let lst = arr.[x,y]

            let col = getAverageColour lst

            let id = getId x y

            changeColour col id

          done

        done


  // Commit the transaction


  tr.Commit()

Here are the results of running it and choosing a photo I took yesterday during my visit to Kodaikanal in Tamil Nadu (India's most southern state).

First the original image:

Colourful truck on jack

Here's what happens when we run the PIX command, selecting the above image and choosing 20 pixels in width:

 Truck with 20 pixel width

Now with a width of 50...

Truck with 50 pixel width

And finally with a width of 100...

 Truck with 100 pixel width

Give it a try yourself, pixelizing different images at different resolutions - you can get some very cool results.

This one is too fun to just let rest... I'm going to see if I get some time this week to work on the parallelization of the colour averaging operation, to see if that improves performance (even if it doesn't today, it will do eventually when I either get a 64-core machine or move some of the code to be hosted in the cloud... :-)

January 28, 2009

Implementing a simple graphing tool inside AutoCAD using F#

Well, I couldn't resist... as I mentioned in the last post - where we looked at creating a simple graph inside AutoCAD as an example of modifying objects inside nested transactions - the idea of graphing inside AutoCAD is a good fit for F#. This is for a number of reasons: F# is very mathematical in nature and excels at processing lists of data. I also spiced it up a bit by adding some code to parallelise some of the mathematical operations, but that didn't turn out to be especially compelling with my dual-core laptop. More on that later.

Here's the F# code:

// Use lightweight F# syntax


#light


// Declare a specific namespace and module name


module Grapher.Commands


// Import managed assemblies


open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry


// Define a common normalization function which makes sure

// our graph gets mapped to our grid


let normalize fn normFn x minInp maxInp maxOut =

  let res =

    fn ((maxInp - minInp) * x / maxOut)

  let normRes = normFn res

  if normRes >= 0.0 && normRes <= 1.0 then

    normRes * (maxOut - 1.0)

  else

    -1.0


// Define some shortcuts to the .NET Math library

// trigonometry functions


let sin x = System.Math.Sin x

let cos x = System.Math.Cos x       

let tan x = System.Math.Tan x       


// Implement our own normalized trig functions

// which each map to the size of the grid passed in


let normSin max x =

  let nf a = (a + 1.0) / 2.0 // Normalise to 0-1

  let res =

    normalize

      sin nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


let normCos max x =

  let nf a = (a + 1.0) / 2.0 // Normalise to 0-1

  let res =

    normalize

      cos nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


let normTan max x =

  let nf a = (a + 3.0) / 6.0 // Normalise differently for tan

  let res =

    normalize

      tan nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


// Now we declare our command


[<CommandMethod("graph")>]

let gridCommand() =


  // We'll time the command, so we can check the

  // sync vs. async efficiency


  let starttime = System.DateTime.Now


  // Let's get the usual helpful AutoCAD objects


  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database


  // "use" has the same effect as "using" in C#


  use tr =

    db.TransactionManager.StartTransaction()


  // Get appropriately-typed BlockTable and BTRs


  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable


  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

       OpenMode.ForWrite)

    :?> BlockTableRecord


  // Function to create a filled circle (hatch) at a

  // specific location


  // Note the valid use of tr and ms, as they are in scope


  let createCircle pt rad =

    let hat = new Hatch()

    hat.SetDatabaseDefaults();

    hat.SetHatchPattern

      (HatchPatternType.PreDefined,

       "SOLID")


    let id = ms.AppendEntity(hat)

    tr.AddNewlyCreatedDBObject(hat, true)


    // Now we create the loop, which we make db-resident

    // (appending a transient loop caused problems, so

    // we're going to use the circle and then erase it)


    let cir = new Circle()

    cir.Radius <- rad

    cir.Center <- pt


    let lid = ms.AppendEntity(cir)

    tr.AddNewlyCreatedDBObject(cir, true)


    // Have the hatch use the loop we created


    let loops = new ObjectIdCollection()

    loops.Add(lid) |> ignore

    hat.AppendLoop(HatchLoopTypes.Default, loops)

    hat.EvaluateHatch(true)


    // Now we erase the loop


    cir.Erase()

    id


  // Function to create our grid of circles


  let createGrid size rad offset =

    let ids = new ObjectIdCollection()

    for i = 0 to size - 1 do

      for j = 0 to size - 1 do

        let pt =

          new Point3d

            (offset * (Int32.to_float i),

            offset * (Int32.to_float j),

            0.0)

        let id = createCircle pt rad

        ids.Add(id) |> ignore

    ids


  // Function to change the colour of an entity


  let changeColour col (id : ObjectId) =

    if id.IsValid then

      let ent =

        tr.GetObject(id, OpenMode.ForWrite) :?> Entity

      ent.ColorIndex <- col


  // Shortcuts to make objects red and yellow


  let makeRed = changeColour 1

  let makeYellow = changeColour 2


  // Function to retrieve the contents of our

  // array of object IDs - this just calculates

  // the index based on the x & y values


  let getIndex fn size i =

    let res = fn size i

    if res >= 0 then

        (i * size) + res

    else

        -1


  // Apply our function synchronously for each value of x


  let applySyncBelowMax size fn =

    [| for i in [0..size-1] ->

       getIndex fn size i |]


  // Apply our function asynchronously for each value of x


  let applyAsyncBelowMax size fn =

    Async.Run

      (Async.Parallel

        [ for i in [0..size-1] ->

          async { return getIndex fn size i } ])


  // Hardcode the size of the grid and create it


  let size = 50

  let ids = createGrid size 0.5 1.2


  // Make the circles all red to start with


  Seq.iter makeRed (Seq.cast ids)


  // From a certain index in the list, get an object ID


  let getId i =

    if i >= 0 then

      ids.[i]

    else

      ObjectId.Null


  // Apply one of our trig functions, synchronously or

  // otherwise, to our grid


  applySyncBelowMax size normSin |>

    Array.map getId |>

      Array.iter makeYellow


  // Commit the transaction


  tr.Commit()


  // Check how long it took


  let elapsed =

      System.DateTime.op_Subtraction

        (System.DateTime.Now, starttime)


  ed.WriteMessage

    ("\nElapsed time: " +

    elapsed.ToString())

 

Here's what you see on AutoCAD's drawing canvas when you run the GRAPH command as it stands:

Sine from F#

If you want to play around with other functions, you can edit the call to applySyncBelowMax to pass normCos or normTan instead of normSin.

Cosine from F#

Tangent from F#

 

As I mentioned earlier, if you swap the call to be applyAsyncBelowMax instead of applySyncBelowMax you will actually run the mathematics piece as asynchronous tasks. These are CPU-bound operations - they don't call across the network or write to a hard-drive, which might have increased the benefit of calling them asynchronously - so right now the async version actually runs more slowly than the sync version. If I were to have more processing cores available to me, it might also give us more benefit, but right now with my dual-core machine there's more effort spent coordinating the tasks than you gain from the parallelism. But I'll let you play around with that yourselves... you may get better results. One other note on that piece of the code: at some point I'd like to make use of the Parallel Extensions for .NET (in particular the Task Parallel Library (TPL)), but for now I've continued with what I know, the asynchronous worklows capability which is now standard in F#.

I'm travelling in India this week (and working from our Bangalore office next week), so this is likely to be my last post of the week.

January 08, 2009

F# programming contest

Firstly, I'd like to wish you all a very Happy New Year! I've just taken three consecutive weeks of vacation for the first time in years, and it's done me the world of good. In fact I'm also on holiday this week but am easing back into the rhythm of things, and have decided to get back into the blogging saddle, while I'm at it. Thanks for your patience during the interruption in service. :-)

So to start 2009 with a bang (or a pop, at least) I'm going to run a programming contest. The basic idea is to generate some cool examples of using functional programming (more specifically F#) with design applications (more specifically those developed by Autodesk :-). See below for some ideas on what areas people might be interested in looking into.Expert F#

Up for grabs are three copies of Don Syme's book, Expert F#, which have been kindly donated by Apress, its publisher. I've mentioned this excellent book in the past, when I introduced my interview with Don (the interview itself is here). With any luck I'll find a way to get Don to sign the books before sending them to the lucky winners.

So, now for some ground-rules...

  • To enter the contest you need to email me a source project which uses F# for a particular task within one or more of Autodesk's products.
    • The project should be accompanied with a description of what it does and how to use it. Please provide sample models, if they're needed to make the code work.
      • I do need to see the full source, as I can't assess the quality of the code otherwise.
    • There's no limit on the size, as long as I can get the code to work easily.
      • F# code has the potential to be extremely succinct, so please don't assume that bigger means better. Code will be assessed for its elegance as well as what it does.
  • The code submitted must be considered non-confidential and license-free.
    • Which means it can be published (with appropriate credit) via this blog and potentially used by people in commercial applications.
  • I will act as the sole judge, although I may include other people if assessing the use of APIs with which I'm unfamiliar. The judge's decision is final.
    • I can feel the power coursing through my fingertips as I write this... :-)
  • Each of the best three entries will win a copy of "Expert F#".
  • Absolutely anyone can enter, including Autodesk employees, their family members and pets.
    • I'll be publishing the winners' names and probably some information provided by them, so please don't expect to remain completely anonymous if you enter the competition.
  • The closing date for the contest is February 28th, 2009.

Here are some thoughts on areas that might prove interesting...

Data-centric problems

Functional programming is very good at managing (parsing, analysing, even creating) large sets of data. One previous, simple example was to gather and sort all the words found in an AutoCAD drawing. Here's the original post showing this, but an update was provided in my F# AU handouts.

Control flow-centric problems

If you have tasks that can easily be parallelized - in that they can safely be run asynchronously and/or concurrently - then functional programming provides a clean way to capture and execute the logic behind them. An example being a simple application that went away and queried RSS feeds asynchronously, using the results to generate AutoCAD entities. Here are the original posts showing this, but an update was provided in my F# AU handouts.

Domain Specific Languages (DSLs)

Implementing new, custom-purpose programming languages can be an efficient way to control the subset of language features used by a non-programmers to implement code. An example of this was the 3D LOGO implementation inside AutoCAD.

Units of measure

One F# language feature that I also see as being very relevant to our domain is the units of measure implementation. I haven't yet worked with this, myself, but I have certainly been meaning to.

I think that's everything... if I've missed something or you need clarification, please email me or post a comment and I'll respond. Hopefully some of you find this a fun challenge for the (often quite gloomy) start of the year.

October 27, 2008

AU Handouts: AutoCAD® .NET - Developing for AutoCAD® Using F# - Part 2

This post continues on from Part 1 of this series. You'll find much of this content has been used before in these previous posts, although post does include content updated for F# 1.9.6.2 (the September 2008 CTP).

The first thing we need to do is – as with any AutoCAD .NET project – add project references to AutoCAD’s managed assemblies, acmgd.dll and acdbmgd.dll. With F#’s integration into Visual Studio 2008 you do this in exactly the same way as you would for a C# or VB.NET project, by selecting Project -> Add Reference... from the pull-down menu or right-clicking the project inside the Solution Explorer and selecting Add Reference... from the context menu.

Here you then browse to the AutoCAD 2009 folder and filter for *mgd* files (at least this is the way I do it), and select the two we want:

Add project references to AutoCAD's managed assemblies

Figure 10 – Adding project references to AutoCAD’s managed assemblies

Now we need to make sure AutoCAD recognizes a module within a namespace, from which it is able to load commands. I found – by using .NET Reflector – that the appropriate structure is to declare your functions as the contents of a module (this needs to come after the #light directive):

module MyNamespace.MyApplication

Next we’re going to specify the .NET namespaces we’ll be using inside this application:

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

We’ll then skip past our definitions of words and sortedWords, and define our command function:

[<CommandMethod("Words")>]

let listWords () =

  // Let's get the usual helpful AutoCAD objects

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

  // "use" has the same effect as "using" in C#

  use tr =

    db.TransactionManager.StartTransaction();

  // Get appropriately-typed BlockTable and BTRs

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  let ps =

    tr.GetObject

      (bt.[BlockTableRecord.PaperSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

Most of this section should be familiar to anyone who has used the .NET API to AutoCAD – there are really only a couple of ideas that may need explanation:

  1. The use keyword is just like C#’s using – but we don’t use curly braces to define scope. The scope gets defined as the remainder of the function in which the use statement has been used. Once the function completes the used object will be disposed of automatically.
  2. We’re using the dynamic cast operator (:?>) to specify the type of object we’re opening with GetObject(). This operator involves a query to check whether this is valid – if we wanted to do a static cast we could use :> instead.

Now we have opened our modelspace and paperspace objects (we could go further and open other layouts, but – once again – I’ll leave that as a follow-on exercise for those who feel the need to do it :-) we can look at the code we need to extract the text from our database-resident objects.

Let’s start by defining a local function which takes an ObjectId and uses it to open an object, and for a textual object (DBText or MText) it will return its contents:

  // A function that accepts an ObjectId and returns

  // a list of the text contents, or an empty list.

  // Note the valid use of tr, as it is in scope

  let extractText x =

    let obj = tr.GetObject(x,OpenMode.ForRead)

    match obj with

    | :? MText as m -> m.Contents

    | :? DBText as d -> d.TextString

    | _ -> ""

Once again we haven’t specified the type of the argument – this will be inferred by the system – but we could very easily do so. We’re using the transaction previously started in the listWords function – the reason for defining extractText local to it – which is quite valid, as it’s in scope.

After opening the object for read from its ID we’re using pattern-matching – a technique that is a huge timesaver for functional programmers – to check on the type of the object and return the appropriate property of it. This is just like a much cleaner switch statement in C#.

We could choose to match against any property of the object, but in our case we want to check the type, so use this operator: :?. The as keyword is a syntactic shortcut that defines a value we can then use to easily dereference the object and get at its properties and methods.

The final clause of the three is a wildcard: it will match all object that are not DBText or MText objects and return an empty string.
Now that we can get at the contents of our text objects, let’s write a quick recursive function to display the contents of the final list of words inside AutoCAD:

  // A recursive function to print the contents of a list

  let rec printList x =

    match x with

    | h :: t -> ed.WriteMessage("\n" + h); printList t

    | [] -> ed.WriteMessage("\n")

Once again, this is a local function, so using our Editor (accessed via the ed value) is quite valid. We’re using pattern-matching again to create a recursive function (indicated via the rec keyword and then the recursive call to printList). When we find an empty list ([]) we simply print a newline, but when we find a list with a head (h) and a tail (t – which may well end up being empty, by the way, we’ll find out the next time we recurse into printList), we print the head and recurse with the tail.

One thing to look out for when defining recursive functions: they really need to be defined as tail-recursive, which means that the recursive call should be the last operation. This allows the compiler to perform tail call optimization, which replaces the declared recursion with a simply while loop inside the generated code.

Why does this matter? Well, calling a function does have some overhead, as stack space is required to store information about the function and its arguments, so if we have a list of 10,000 words to print and the function hasn’t been optimized, the recursion could cause problems. (The number could be 100,000 or 1,000,000, but the point is there is a number).

The above code does get optimized properly (even if the pattern for the empty list comes after the recursive call – it’s really about the position of the recursive call in the clause that recurses, rather than the overall program), and this is easy to check with .NET Reflector. In fact there’s an article on my blog covering just this:

http://through-the-interface.typepad.com/through_the_interface/2008/02/using-reflector.html

  Seq.to_list (Seq.cast ms) @    // Create a list of modelspace ids,

    Seq.to_list (Seq.cast ps) |> //  appending those from paperspace

    List.map extractText |>      // Extract the text from each object

    sortedWords |>               // Get a sorted, canonical list of words

    printList                    // Print the resultant words

A couple of comments on the above plumbing: ms and ps are both IEnumerable types (which correspond to the Seq class in F#), but are both untyped. This means we have to cast them, to be able to access them properly from F#, and then we can simply call Seq.to_list to get the contents into a list. The @ operator appends the list of ObjectIds of objects in modelspace with those in paperspace, and we then pipe the list into a call to List.map which runs our extractText function on all the objects in the combined list. The results get piped into our sortedWords function, and we finally print them to the command-line using our recursive printList function.

Finally, we’re just going to call commit on our transaction object, as for performance reasons this is currently best practice:

  // As usual, committing is cheaper than aborting

  tr.Commit()

That’s it for our first AutoCAD application. Let’s see the entire listing:

#light

// Declare a specific namespace and module name

module MyNamespace.MyApplication

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

// Partial application of split which can then be

// applied to a string to retrieve the contained words

let words =

  let seps = " \t~`!@#$%^&*()-=_+{}|[]\\;':\"<>?,./"

  seps.ToCharArray() |> Array.to_list |> String.split

let sortedWords x =

    List.map words x |>  // Get the words from each string

    List.concat |>       // No need for the outer list

    Set.of_list |>       // Create a set from the list

    Set.to_list          // Create a list from the set

// Now we define our command

[<CommandMethod("Words")>]

let listWords () =

  // Let's get the usual helpful AutoCAD objects

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

  // "use" has the same effect as "using" in C#

  use tr =

    db.TransactionManager.StartTransaction();

  // Get appropriately-typed BlockTable and BTRs

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  let ps =

    tr.GetObject

      (bt.[BlockTableRecord.PaperSpace],

       OpenMode.ForRead)

    :?> BlockTableRecord

  // Now the fun starts...

  // A function that accepts an ObjectId and returns

  // a list of the text contents, or an empty list.

  // Note the valid use of tr, as it is in scope

  let extractText x =

    let obj = tr.GetObject(x,OpenMode.ForRead)

    match obj with

    | :? MText as m -> m.Contents

    | :? DBText as d -> d.TextString

    | _ -> ""

  // A recursive function to print the contents of a list

  let rec printList x =

    match x with

    | h :: t -> ed.WriteMessage("\n" + h); printList t

    | [] -> ed.WriteMessage("\n")

  // And here's where we plug everything together...

  Seq.to_list (Seq.cast ms) @    // Create a list of modelspace ids,

    Seq.to_list (Seq.cast ps) |> //  appending those from paperspace

    List.map extractText |>      // Extract the text from each object

    sortedWords |>               // Get a sorted, canonical list of words

    printList                    // Print the resultant words

  // As usual, committing is cheaper than aborting

  tr.Commit()

Introducing parallel processing in AutoCAD via F# Asynchronous Workflows

As mentioned previously, pure functional code lends itself to be run on multiple computing cores in parallel. While the tools aren’t yet there to make this happen automatically – via implicit parallelization – this is a likely outcome, over the coming years. For now we have the possibility of writing code that uses explicit parallelization – where we specify the tasks we know can be executed at the same time and leave the language and runtime to take care of the coordination.

There are a couple of ways to do this, right now: the Parallel Extensions to .NET (also in CTP stage and due for inclusion in Visual Studio 2010) provide a number of parallel constructs, such as parallel versions of for and while loops. F# currently provides the capability to define and execute Asynchronous Workflows, which is what we’re going to look at now.

First, let’s take a look at a sample application that we’re going to parallelize. This sample goes through and queries, via RSS, the latest posts on a number of different blogs. It then generates AutoCAD geometry – text with a hyperlink – for each of these posts. So we turn AutoCAD into an RSS reader, for all intents and purposes.

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module MyNamespace.MyApplication

    8

    9 // Import managed assemblies

   10

   11 open Autodesk.AutoCAD.Runtime

   12 open Autodesk.AutoCAD.ApplicationServices

   13 open Autodesk.AutoCAD.DatabaseServices

   14 open Autodesk.AutoCAD.Geometry

   15 open System.Xml

   16 open System.IO

   17 open System.Net

   18

   19 // The RSS feeds we wish to get. The first two values are

   20 // only used if our code is not able to parse the feed's XML

   21

   22 let feeds =

   23   [ ("Through the Interface",

   24     "http://blogs.autodesk.com/through-the-interface",

   25     "http://through-the-interface.typepad.com/through_the_interface/atom.xml");

   26

   27     ("Don Syme's F# blog",

   28     "http://blogs.msdn.com/dsyme/",

   29     "http://blogs.msdn.com/dsyme/rss.xml");

   30

   31     ("Shaan Hurley's Between the Lines",

   32     "http://autodesk.blogs.com/between_the_lines",

   33     "http://autodesk.blogs.com/between_the_lines/rss.xml");

   34

   35     ("Scott Sheppard's It's Alive in the Lab",

   36     "http://blogs.autodesk.com/labs",

   37     "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

   38

   39     ("Volker Joseph's Beyond the Paper",

   40     "http://blogs.autodesk.com/beyond_the_paper",

   41     "http://dwf.blogs.com/beyond_the_paper/atom.xml") ]

   42

   43 // Fetch the contents of a web page, synchronously

   44

   45 let httpSync (url:string) =

   46   let req = WebRequest.Create(url)

   47   use resp = req.GetResponse()

   48   use stream = resp.GetResponseStream()

   49   use reader = new StreamReader(stream)

   50   reader.ReadToEnd()

   51

   52 // Load an RSS feed's contents into an XML document object

   53 // and use it to extract the titles and their links

   54 // Hopefully these always match (this could be coded more

   55 // defensively)

   56

   57 let titlesAndLinks (name, url, xml) =

   58   try

   59     let xdoc = new XmlDocument()

   60     xdoc.LoadXml(xml)

   61

   62     let titles =

   63       [ for n in xdoc.SelectNodes("//*[name()='title']")

   64           -> n.InnerText ]

   65     let links =

   66       [ for n in xdoc.SelectNodes("//*[name()='link']") ->

   67           let inn = n.InnerText

   68           if  inn.Length > 0 then

   69             inn

   70           else

   71             let href = n.Attributes.GetNamedItem("href").Value

   72             let rel = n.Attributes.GetNamedItem("rel").Value

   73             if href.Contains("feedburner") or rel.Contains("enclosure") then

   74                 ""

   75             else

   76               href ]

   77

   78     let descs =

   79       [ for n in xdoc.SelectNodes

   80           ("//*[name()='description' or name()='subtitle' or name()='summary']")

   81             -> n.InnerText ]

   82

   83     // A local function to filter out duplicate entries in

   84     // a list, maintaining their current order.

   85     // Another way would be to use:

   86     //    Set.of_list lst |> Set.to_list

   87     // but that results in a sorted (probably reordered) list.

   88

   89     let rec nub lst =

   90       match lst with

   91       | a::[] -> [a]

   92       | a::b ->

   93         if a = List.hd b then

   94           nub b

   95         else

   96           a::nub b

   97       | [] -> []

   98

   99     // Filter the links to get (hopefully) the same number

  100     // and order as the titles and descriptions

  101

  102     let real = List.filter (fun (x:string) -> x.Length > 0) 

  103     let lnks = real links |> nub

  104

  105     // Return a link to the overall blog, if we don't have

  106     // the same numbers of titles, links and descriptions

  107

  108     let lnum = List.length lnks

  109     let tnum = List.length titles

  110     let dnum = List.length descs

  111

  112     if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

  113       [(name,url,url)]

  114     else

  115       List.zip3 titles lnks descs

  116   with _ -> []

  117

  118 // For a particular (name,url) pair,

  119 // create an AutoCAD HyperLink object

  120

  121 let hyperlink (name,url,desc) =

  122   let hl = new HyperLink()

  123   hl.Name <- url

  124   hl.Description <- desc

  125   (name, hl)

  126

  127 // Download an RSS feed and return AutoCAD HyperLinks for its posts

  128

  129 let hyperlinksSync (name, url, feed) =

  130   let xml = httpSync feed

  131   let tl = titlesAndLinks (name, url, xml)

  132   List.map hyperlink tl

  133

  134 // Now we declare our command

  135

  136 [<CommandMethod("rss")>]

  137 let createHyperlinksFromRss() =

  138

  139   let starttime = System.DateTime.Now

  140

  141   // Let's get the usual helpful AutoCAD objects

  142

  143   let doc =

  144     Application.DocumentManager.MdiActiveDocument

  145   let ed = doc.Editor

  146   let db = doc.Database

  147

  148   // "use" has the same effect as "using" in C#

  149

  150   use tr =

  151     db.TransactionManager.StartTransaction()

  152

  153   // Get appropriately-typed BlockTable and BTRs

  154

  155   let bt =

  156     tr.GetObject

  157       (db.BlockTableId,OpenMode.ForRead)

  158     :?> BlockTable

  159   let ms =

  160     tr.GetObject

  161       (bt.[BlockTableRecord.ModelSpace],

  162        OpenMode.ForWrite)

  163     :?> BlockTableRecord

  164

  165   // Add text objects linking to the provided list of

  166   // HyperLinks, starting at the specified location

  167

  168   // Note the valid use of tr and ms, as they are in scope

  169

  170   let addTextObjects (pt : Point3d) lst =

  171     // Use a for loop, as we care about the index to

  172     // position the various text items

  173

  174     let len = List.length lst

  175     for index = 0 to len - 1 do

  176       let txt = new DBText()

  177       let (name:string,hl:HyperLink) = List.nth lst index

  178       txt.TextString <- name

  179       let offset =

  180         if index = 0 then

  181           0.0

  182         else

  183           1.0

  184

  185       // This is where you can adjust:

  186       //  the initial outdent (x value)

  187       //  and the line spacing (y value)

  188

  189       let vec =

  190         new Vector3d

  191           (1.0 * offset,

  192            -0.5 * (Int32.to_float index),

  193            0.0)

  194       let pt2 = pt + vec

  195       txt.Position <- pt2

  196       ms.AppendEntity(txt) |> ignore

  197       tr.AddNewlyCreatedDBObject(txt,true)

  198       txt.Hyperlinks.Add(hl) |> ignore

  199

  200   // Here's where we use the varous functions

  201   // we've defined

  202

  203   let links =

  204     List.map hyperlinksSync feeds

  205

  206   // Add the resulting objects to the model-space 

  207

  208   let len = List.length links

  209   for index = 0 to len - 1 do

  210

  211     // This is where you can adjust:

  212     //  the column spacing (x value)

  213     //  the vertical offset from origin (y axis)

  214

  215     let pt =

  216       new Point3d

  217         (15.0 * (Int32.to_float index),

  218         30.0,

  219         0.0)

  220     addTextObjects pt (List.nth links index)

  221

  222   tr.Commit()

  223

  224   let elapsed =

  225       System.DateTime.op_Subtraction(System.DateTime.Now, starttime)

  226

  227   ed.WriteMessage("\nElapsed time: " + elapsed.ToString())

I have numbered the lines, to make it easier for us to talk about the changes that are needed to introduce parallelism into this sample. Both synchronous and asynchronous versions of this application are available on my blog.

I won’t go through the above code in detail, here: firstly, it’s not intended as a perfect implementation of an RSS consumer – there are too many variations in the way RSS is implemented by different sites, so I know for a fact that this code will not work for certain blogs – it’s really intended to be an example of a – potentially time-consuming – asynchronous (in this case network-based) activity that is easy to run in parallel.

A word of caution: AutoCAD is not thread-safe – it is very much a single-threaded application – so we need to coordinate the results of these tasks prior to making the changes to the AutoCAD database. Luckily F# makes this very easy for us to do, so that’s really not a problem.

Here is the updated source that makes use of Asynchronous Workflows, with the modified/new lines highlighted in red (with a grey background for those reading this in black & white :-):

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module MyNamespace.MyApplicationAsync

    8

    9 // Import managed assemblies

   10

   11 open Autodesk.AutoCAD.Runtime

   12 open Autodesk.AutoCAD.ApplicationServices

   13 open Autodesk.AutoCAD.DatabaseServices

   14 open Autodesk.AutoCAD.Geometry

   15 open System.Xml

   16 open System.IO

   17 open System.Net

   18

   19 // The RSS feeds we wish to get. The first two values are

   20 // only used if our code is not able to parse the feed's XML

   21

   22 let feeds =

   23   [ ("Through the Interface",

   24     "http://blogs.autodesk.com/through-the-interface",

   25     "http://through-the-interface.typepad.com/through_the_interface/atom.xml");

   26

   27     ("Don Syme's F# blog",

   28     "http://blogs.msdn.com/dsyme/",

   29     "http://blogs.msdn.com/dsyme/rss.xml");

   30

   31     ("Shaan Hurley's Between the Lines",

   32     "http://autodesk.blogs.com/between_the_lines",

   33     "http://autodesk.blogs.com/between_the_lines/rss.xml");

   34

   35     ("Scott Sheppard's It's Alive in the Lab",

   36     "http://blogs.autodesk.com/labs",

   37     "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

   38

   39     ("Volker Joseph's Beyond the Paper",

   40     "http://blogs.autodesk.com/beyond_the_paper",

   41     "http://dwf.blogs.com/beyond_the_paper/atom.xml") ]

   42

   43 // Fetch the contents of a web page, asynchronously

   44

   45 let httpAsync(url:string) =

   46   async { let req = WebRequest.Create(url)

   47           use! resp = req.GetResponseAsync()

   48           use stream = resp.GetResponseStream()

   49           use reader = new StreamReader(stream)

   50           return reader.ReadToEnd() }

   51

   52 // Load an RSS feed's contents into an XML document object

   53 // and use it to extract the titles and their links

   54 // Hopefully these always match (this could be coded more

   55 // defensively)

   56

   57 let titlesAndLinks (name, url, xml) =

   58   try

   59     let xdoc = new XmlDocument()

   60     xdoc.LoadXml(xml)

   61

   62     let titles =

   63       [ for n in xdoc.SelectNodes("//*[name()='title']")

   64           -> n.InnerText ]

   65     let links =

   66       [ for n in xdoc.SelectNodes("//*[name()='link']") ->

   67           let inn = n.InnerText

   68           if  inn.Length > 0 then

   69             inn

   70           else

   71             let href = n.Attributes.GetNamedItem("href").Value

   72             let rel = n.Attributes.GetNamedItem("rel").Value

   73             if href.Contains("feedburner") or rel.Contains("enclosure") then

   74                 ""

   75             else

   76               href ]

   77

   78     let descs =

   79       [ for n in xdoc.SelectNodes

   80           ("//*[name()='description' or name()='subtitle' or name()='summary']")

   81             -> n.InnerText ]

   82

   83     // A local function to filter out duplicate entries in

   84     // a list, maintaining their current order.

   85     // Another way would be to use:

   86     //    Set.of_list lst |> Set.to_list

   87     // but that results in a sorted (probably reordered) list.

   88

   89     let rec nub lst =

   90       match lst with

   91       | a::[] -> [a]

   92       | a::b ->

   93         if a = List.hd b then

   94           nub b

   95         else

   96           a::nub b

   97       | [] -> []

   98

   99     // Filter the links to get (hopefully) the same number

  100     // and order as the titles and descriptions

  101

  102     let real = List.filter (fun (x:string) -> x.Length > 0) 

  103     let lnks = real links |> nub

  104

  105     // Return a link to the overall blog, if we don't have

  106     // the same numbers of titles, links and descriptions

  107

  108     let lnum = List.length lnks

  109     let tnum = List.length titles

  110     let dnum = List.length descs

  111

  112     if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

  113       [(name,url,url)]

  114     else

  115       List.zip3 titles lnks descs

  116   with _ -> []

  117

  118 // For a particular (name,url) pair,

  119 // create an AutoCAD HyperLink object

  120

  121 let hyperlink (name,url,desc) =

  122   let hl = new HyperLink()

  123   hl.Name <- url

  124   hl.Description <- desc

  125   (name, hl)

  126

  127 // Use asynchronous workflows in F# to download

  128 // an RSS feed and return AutoCAD HyperLinks

  129 // corresponding to its posts

  130

  131 let hyperlinksAsync (name, url, feed) =

  132   async { let! xml = httpAsync feed

  133           let tl = titlesAndLinks (name, url, xml)

  134           return List.map hyperlink tl }

  135

  136 // Now we declare our command

  137

  138 [<CommandMethod("arss")>]

  139 let createHyperlinksFromRssAsync() =

  140

  141   let starttime = System.DateTime.Now

  142

  143   // Let's get the usual helpful AutoCAD objects

  144

  145   let doc =

  146     Application.DocumentManager.MdiActiveDocument

  147   let ed = doc.Editor

  148   let db = doc.Database

  149

  150   // "use" has the same effect as "using" in C#

  151

  152   use tr =

  153     db.TransactionManager.StartTransaction()

  154

  155   // Get appropriately-typed BlockTable and BTRs

  156

  157   let bt =

  158     tr.GetObject

  159       (db.BlockTableId,OpenMode.ForRead)

  160     :?> BlockTable

  161   let ms =

  162     tr.GetObject

  163       (bt.[BlockTableRecord.ModelSpace],

  164        OpenMode.ForWrite)

  165     :?> BlockTableRecord

  166

  167   // Add text objects linking to the provided list of

  168   // HyperLinks, starting at the specified location

  169

  170   // Note the valid use of tr and ms, as they are in scope

  171

  172   let addTextObjects (pt : Point3d) lst =

  173     // Use a for loop, as we care about the index to

  174     // position the various text items

  175

  176     let len = List.length lst

  177     for index = 0 to len - 1 do

  178       let txt = new DBText()

  179       let (name:string,hl:HyperLink) = List.nth lst index

  180       txt.TextString <- name

  181       let offset =

  182         if index = 0 then

  183           0.0

  184         else

  185           1.0

  186

  187       // This is where you can adjust:

  188       //  the initial outdent (x value)

  189       //  and the line spacing (y value)

  190

  191       let vec =

  192         new Vector3d

  193           (1.0 * offset,

  194            -0.5 * (Int32.to_float index),

  195            0.0)

  196       let pt2 = pt + vec

  197       txt.Position <- pt2

  198       ms.AppendEntity(txt) |> ignore

  199       tr.AddNewlyCreatedDBObject(txt,true)

  200       txt.Hyperlinks.Add(hl) |> ignore

  201

  202   // Here's where we do the real work, by firing

  203   // off - and coordinating - asynchronous tasks

  204   // to create HyperLink objects for all our posts

  205

  206   let links =

  207     Async.Run

  208       (Async.Parallel

  209         [ for (name,url,feed) in feeds ->

  210           hyperlinksAsync (name,url,feed) ])

  211     |> Array.to_list

  212

  213   // Add the resulting objects to the model-space 

  214

  215   let len = List.length links

  216   for index = 0 to len - 1 do

  217

  218     // This is where you can adjust:

  219     //  the column spacing (x value)

  220     //  the vertical offset from origin (y axis)

  221

  222     let pt =

  223       new Point3d

  224         (15.0 * (Int32.to_float index),

  225         30.0,

  226         0.0)

  227     addTextObjects pt (List.nth links index)

  228

  229   tr.Commit()

  230

  231   let elapsed =

  232       System.DateTime.op_Subtraction(System.DateTime.Now, starttime)

  233

  234   ed.WriteMessage("\nElapsed time: " + elapsed.ToString())

Let's look at the specific changes:

  • Line 7 has been changed to allow both files to be part of the same project.
  • Lines 45-50 implement a new, asynchronous function to download content from a URL. The async primitive coordinates a set of activities, while the let! and use! statements indicate that these right-hand side of the operation will be run asynchronously and that the results should be bound to the left. So here we're only getting the HTTP content asynchronously - the reading is to occur synchronously.
  • Lines 131-134 implement an asynchronous task that not only calls our asynchronous HTTP request function but coordinates the creation of AutoCAD geometry based on the contents received.
  • Lines 207-211 are where we make use of these newly-defined functions by firing them off in parallel (the framework will use the processing capabilities available to it to execute the tasks as efficiently as possible) and coordinating the results into a single array, which we convert to a list to maintain our previous processing code.

When we run either the RSS or ARSS (its asynchronous version), we should see this kind of result:

RSS feeds inside AutoCAD

Figure 11 – AutoCAD geometry created from our RSS feeds

Now let’s see how they compare in terms of performance. I executed the RSS and ARSS commands a number of times in sequence to get a feel for relative performance.

Command: rss

Elapsed time: 00:00:08.1958195

Command: arss

Elapsed time: 00:00:02.2802280

Command: rss

Elapsed time: 00:00:04.1264126

Command: arss

Elapsed time: 00:00:03.6343634

Command: rss

Elapsed time: 00:00:03.6563656

Command: arss

Elapsed time: 00:00:01.9891989

Command: rss

Elapsed time: 00:00:03.1673167

Command: arss

Elapsed time: 00:00:03.1223122

Command: rss

Elapsed time: 00:00:05.7375737

Command: arss

Elapsed time: 00:00:01.9391939

The first execution time is much higher due to an initial startup penalty or the need to fill some page cache with the content. On average, though, the asynchronous code runs in 60-70% of the time needed by the synchronous version. The code was run a dual-core notebook: some of the performance will be related to using both cores, but most will be due to the parallelization of asynchronous tasks that have some latency due to use of the network. With more accesses in parallel you would see this performance difference become increasingly exaggerated.

RSS Feed

Search