How to use GH_Structure to dump a Lines Tree from into GH in C#?

Dear experts,

I´m trying to extract a tree of lines as fast as possible from a C# component. In the DataTree documentation it is said that “If you’re using the Grasshopper SDK you should consider using the GH_Structure class instead.

Since the number of lines could be huge, e.g. over 500000. In the following example I´ve used just around 45000 lines, but feel free to play with sliders to see how it affects performance. DumpLinesGH.gh (17.4 KB)


Using “Dump Mode” dropdown list you can switch between Line, Tree, GH_Line and GH_Goo alternatives. Whereas the times given by profiler are 230, 320, 80 and 50 ms, respectively, the Watch timer produces 2, 33, 25 and 30 ms. This indicates that a lot of work is being carried out outside the user introduced C# code. Basically, the four alternatives try to use:

List<Line> lines = genLines(pts); // Generate Lines
DataTree<Line> linesTree = new DataTree<Line>();
GH_Structure<IGH_Goo> gh_linesTree = new GH_Structure<IGH_Goo>();
GH_Structure<GH_Line> gh_linesTree = new GH_Structure<GH_Line>();

Unfortunately, despite all four data structures are able to allocate and use the Lines inside C# component, outside it is not possible when using a GH_Structure (red Line component).

What am I doing wrong? Any help is highly appreciated !!

PS: I´m sorry, I´m not an expert in C#… but I´ve tried (unsuccessfully) to follow what it is said here:

Perhaps pasting the code here helps:

  private void RunScript(List<Point3d> Pts, int x, ref object Lines)
  {
    // Initialize some watches
    var watch = new System.Diagnostics.Stopwatch();
    var watch2 = new System.Diagnostics.Stopwatch();
    watch2.Start();

    GH_Path pth;
    pts = Pts.ToArray(); // Convert List to Array (arrays are fastere)

    watch = new System.Diagnostics.Stopwatch();
    watch.Start();
    List<Line> lines = genLines(pts); // Generate Lines
    Print("Total Lines [#]: " + lines.Count);
    watch.Stop();
    Print("genLines: " + watch.ElapsedMilliseconds + " ms");

    watch = new System.Diagnostics.Stopwatch();
    watch.Start();
    switch(x)
    {
      case 0:
        Lines = lines;
        break;

      case 1:
        DataTree<Line> linesTree = new DataTree<Line>();
        pth = new GH_Path(0); // first path
        for(int i = 0; i < lines.Count; i++)
        {
          pth = new GH_Path(i);
          linesTree.Add(lines[i], pth);
        }
        Lines = linesTree;
        break;

      case 2:
        GH_Structure<GH_Line> gh_linesTree = new GH_Structure<GH_Line>();
        for(int i = 0; i < lines.Count; i++)
        {
          pth = new GH_Path(i);
          GH_Line gh_line = new GH_Line(lines[i]);
          gh_linesTree.Append(gh_line, pth);
        }
        Lines = gh_linesTree;
        pth = new GH_Path(135);
        Print("gh_linesTree--> " + gh_linesTree.get_DataItem(pth, 0));// [GH_Path, Int32]
        break;

      case 3:
        GH_Structure<IGH_Goo> gh_gooTree = new GH_Structure<IGH_Goo>();
        for(int i = 0; i < lines.Count; i++)
        {
          pth = new GH_Path(i);
          GH_Line gh_line = new GH_Line(lines[i]);
          gh_gooTree.Append(gh_line, pth);
        }
        Lines = gh_gooTree;
        pth = new GH_Path(135);
        Print("gh_gooTree--> " + gh_gooTree.get_DataItem(pth, 0));// [GH_Path, Int32]
        break;
    }
    watch.Stop();
    Print("Dump: " + watch.ElapsedMilliseconds + " ms");

    watch2.Stop();
    Print("Total time: " + watch2.ElapsedMilliseconds + " ms");

  }

  // <Custom additional code> 

  // GLOBALS
  Point3d[] pts; // Convert List to Array (arrays are faster)

  // GENERATE ALL POSSIBLE LINES (SEGMENTS) FOR GIVEN POINTS
  List<Line> genLines(Point3d[] pts)
  {
    // Lines List Output
    List<Line> lines = new List<Line>();

    // Generate all possible lines (no directional)
    for(int i = 0; i < pts.Length - 1; i++)
      for(int j = i + 1; j < pts.Length; j++)
        lines.Add(new Line(pts[i], pts[j]));

    return lines;
  }

  // </Custom additional code> 
}

Well …

  1. The rnd pts native stuff is “reasonably” slow since it attempts to create an “even” collection (I do hope that you understand what this means).
  2. If you need a C# that does all that (*) notify (MINUS GH_xxx structures and the likes: I never work with similar collections plus I hate VS)

(*) including a grid distort (for speed - see below) rnd pts Method

BTW: A fast way to create some sort of “even” rnd pts collection - for this case anyway - is to create a 2d grid and then “distort” (in X and/or Y) randomly (using distort_Min/Max values) any given pt.

Say:


1 Like

Thanks @PeterFotiadis for your quick answer.

I´m sorry, I´m not interested in the random points generation stuff at all.

The GH sketch I´ve commented above is just a simple example I quickly figured out to illustrate what I am trying to do: “extract a (huge) tree of lines as fast as possible from a C# component”. With “extract” I meant to get it out from the C# component and load it into GH to work with it further.

While playing around I´ve realized a 10x slowdown (measured by Watch) between the true C# calculations (using Rhino stuff) and the real time reported by the Profiler. My hypothesis is that such 10x slowdown is caused at some point when dumping the Lines tree structure (whatever way it is implemented) into GH. Do you agree?

What I have implemented so far with IGH_Goo and GH_Line stuff (as recommended by Rhino documentation) seems to be ok. For example, some random (e.g. 135) line information can be printed in “out” using:

    pth = new GH_Path(135);
    Print("gh_linesTree--> " + gh_linesTree.get_DataItem(pth, 0));

Any idea about why

 Lines = gh_linesTree;

does not produce a valid Lines tree in GH?

Hi,

GH_Structure<T> is reserved to components built in Visual Studio and is used to get data in and out of a component.
In a C# script component, you can only use DataTree<T> as an input/output tree structure.

2 Likes

As you already found out, the performance bottleneck in script components is piping data in and out. For security reasons, a script component always copies the data. Unless you have hundreds of thousands of objects, this is not a problem. But in this case you have, so you either write a plugin component where data is passed by reference, or you circumvent the copying with tricks and nasty hacks.
See this thread to find some inspiration for the second option:

1 Like

If you are after speed that’s part of the story (try 100K points).

Anyway … as I said I have absolutely no interest to play games with GH structures (and in general things related with GH SDK).

But even if you skip that … all pts vs all pts MAY cause GPU delays.

Grid

Distorted Grid

Classic prox connections (using Point3dList etc etc)

All pts VS all (what meaning has this for large collections? none I’m afraid)

Thanks all for your kind replies!

I didn´t know that there are C# things that work if compiled in VS but not in GH´s C# components.

So… lets see if I understood you well.

If I compile the same C# code in Visual Studio and use GH_Line or GH_Goo alternative it should run much faster avoiding bottlenecks? Am I right?

The GH_Structure is what a real component uses. It is basically a tree structure and it passes data itself by reference from component to component.

A script component works differently for multiple reasons. I’m not going into detail here and I’m not David Rutten, so I can’t tell you why he did it like this. But a script component working differently, and the main difference is that it copies data in and out. It also extracts the data from a wrapper type into the underlying Rhinocommon equivalent. So a ‘structure of GH_Points’ becomes a ‘DataTree of Point3d’.

This transformation is the bottleneck on larger collections. The hacks shown in the other thread for bypassing this transformation is basically exploiting the fact that you can go to the previous components connected to the script-components and extract the data directly. And of course you can directly create the GH compliant type, instead of outputting the Rhinocommon type. All this will improve the performance for a script component. But if you create a custom plugin component, you don’t need to bother at all. Just do what the documentation want you to do and you are fine.

But writing plugin components has also disadvantages. The distribution of a plugin is a bit harder. In any case, writing a script component and a plugin component is something different and you should not mix both!

1 Like

I´m sorry, but I´m a bit confused yet. I do not fully understand why, if GH components really work with GH_Structure stuff, I can´t output a GH_Structure into the next component (Line component) without an error.

From your explanations I understand the bottleneck at input, but I can´t understand the error that my code produces with “GH_Line” in the “Dump Mode” dropdown list (i.e. “case 2” in the code). Why a GH component does not understand a GH_Structure as input?

Do you mean that it is (badly) converted to something else that the next component cannot understand? (note the red Line component in the image above)

Why I can´t input a “GH_Structure<GH_Line>” tree in the next component without obtaining an error?

Because there is code in between a Grasshopper component and the script you are writing. If you are interested in how it works use a decompiler like ILSpy or DNSpy and see the implementation for the C# component for yourself. Any component uses a GH_Structure to pass data around, but before it enters the ScriptInstance with the Runscript method, any data is converted into a DataTree or a list or as a single item. If you return a GH_Structure it simply doesn’t expect that and fails. Or more precise you can return a GH_Structure but under the hood you return it as structure in a structure. This is also true for the other way around, if you instantiate a GH_Structure, you have GH_Structure in a GH_Structure. This is what you do in your example. The next component doesn’t know how to deal with something like this.

A script component wants you to work with a DataTree a List or a single item of any type. I remember that the author once said this was a design decision to simplify scripting for rather inexperienced programmers, like most GH users are.

1 Like

Ok, thanks for your explanations. I think now I more or less get the point.

Do you know any workaround for output like those you said in your post for input or it is just totally impossible?

In any case, thanks a lot for your help!!

As I said, I linked you the example of a similar problem. If you don’t want to write a custom plugin and stick to a C# script component, then the trick is to find the component before and after the script and directly inject/retreive the data. This code is not obvious and it can be done in multiple ways. But if you examine the LINQ query carefully you see one way of achieving this.

1 Like