Run/Solve a Grasshopper file without starting Rhino CPython Rhino.Inside

Hello, how can I enable the solution of my grasshopper document?

Example from https://github.com/mcneel/rhino.inside-cpython.

The attributes of the document already state that the document is enabled, but it does not work as expected, even with ExpireSolution.

import rhinoinside
rhinoinside.load()

filePath =r"simple_def (1).gh"

Rhino = rhinoinside.Rhino
#Start grasshopper in "headless" mode
pluginObject = Rhino.RhinoApp.GetPlugInObject("Grasshopper")
if pluginObject:
    pluginObject.RunHeadless()

import Grasshopper
io = Grasshopper.Kernel.GH_DocumentIO()

if not io.Open(filePath):
    print("File loading failed.")
else:
    doc = io.Document
    doc.Enabled = True
    doc.ExpireSolution()
    for obj in doc.Objects:
        try:
            param = Grasshopper.Kernel.IGH_Param(obj)
        except:
            continue
            
        #look for param with nickname "CollectMe"
        if param.NickName == "CollectMe":
            param.CollectData()
            param.ComputeData()

            for item in param.VolatileData.AllData(True):
                try:
                    success, line = item.CastTo(Rhino.Geometry.Line())
                    if success: print("Got a Line: {}".format(line))
                except:
                    pass

Got a Line: -5.5574407422041,12.7579864904777,0,-3.39439210003612,-2.61104333545273,0

doc.Enabled, doc.EnableSolutions

(True, True)

The solution itself does not get executed, how can I do it?

I can only trigger the parameters by param.CollectData() and param.ComputeData(), but not the whole solution.

To load a GH definition with CPython Rhino.Inside you can do the following, without having to get hold of a plugin object like that. This is closer to how Grasshopper is loaded in Rhino Compute.

import rhinoinside
from pathlib import Path
import clr

sysdir = Path(r"C:\Program Files\Rhino 7 WIP\System")
plugdir = Path(sysdir, "..", "Plug-ins").resolve()
rhinoinside.load(f"{sysdir}")

GrasshopperDll = f'{Path(plugdir, "Grasshopper", "Grasshopper.dll").resolve()}'
GH_IODll= f'{Path(plugdir, "Grasshopper", "GH_IO.dll")}'
GH_UtilDll= f'{Path(plugdir, "Grasshopper", "GH_Util.dll")}'

clr.AddReference(GrasshopperDll)
clr.AddReference(GH_IODll)
clr.AddReference(GH_UtilDll)

# Set up ready, now do the actual Rhino usage

import System
import Rhino

import Grasshopper
from Grasshopper.Kernel import GH_Document
from GH_IO.Serialization import GH_Archive

definition = GH_Document()
archive = GH_Archive()
archive.ReadFromFile(r"N:\gh\times_viz.gh")

archive.ExtractObject(definition, "Definition")

for ob in definition.Objects:
    print(ob)

CollectData() and ComputeData() is also how Compute uses GH definitions:

1 Like

Nathan, thanks for your answer.

I think I still don’t understand how I could run a solution. For example it seems like python components are not collected and computed, because Grasshopper.Kernel.IGH_Param throws an error with these.
What I want / am looking for is executing the GH file as if it were open.

Thanks for your help and have a nice weekend!

Computing the data for parameters should result in solution being run AFAIK. @fraguada has been recently looking at that part of the code in compute, so maybe he can share some of his wisdom here.

1 Like

This is an example file with a python component that saves a file containing the current date and time to the temp directory.
You could use it for testing the behaviour. With GUI it shows up, without it does not.

example_file.gh (3.5 KB)

Used code

print("0")
import rhinoinside
from pathlib import Path
import clr
print("1")
sysdir = Path(r"C:\Program Files\Rhino 7 WIP\System")
plugdir = Path(sysdir, "..", "Plug-ins").resolve()
rhinoinside.load(f"{sysdir}")

GrasshopperDll = f'{Path(plugdir, "Grasshopper", "Grasshopper.dll").resolve()}'
GH_IODll= f'{Path(plugdir, "Grasshopper", "GH_IO.dll")}'
GH_UtilDll= f'{Path(plugdir, "Grasshopper", "GH_Util.dll")}'
print("2")
clr.AddReference(GrasshopperDll)
clr.AddReference(GH_IODll)
clr.AddReference(GH_UtilDll)

# Set up ready, now do the actual Rhino usage
print("3")
import System
import Rhino

import Grasshopper
from Grasshopper.Kernel import GH_Document
from GH_IO.Serialization import GH_Archive

definition = GH_Document()
archive = GH_Archive()
archive.ReadFromFile(r"path\example_file.gh")

archive.ExtractObject(definition, "Definition")

for obj in definition.Objects:
    try:
        param = Grasshopper.Kernel.IGH_Param(obj)
    except:
        continue
    param.CollectData()
    param.ComputeData()
    print(param.Name)

You’ll be needing a `definition.NewSolution(True, GH_SolutionMode.Silent). Here is a simple addition example where each run sets different inputs, then gets solution:

inside_addition.gh (1.5 KB)

The GH file should live next to the .py file.

#tested with python3.7
import rhinoinside
from pathlib import Path
import clr

sysdir = Path(r"C:\Program Files\Rhino 7 WIP\System")
plugdir = Path(sysdir, "..", "Plug-ins").resolve()
rhinoinside.load(f"{sysdir}")

GrasshopperDll = f'{Path(plugdir, "Grasshopper", "Grasshopper.dll").resolve()}'
GH_IODll= f'{Path(plugdir, "Grasshopper", "GH_IO.dll")}'
GH_UtilDll= f'{Path(plugdir, "Grasshopper", "GH_Util.dll")}'

clr.AddReference(GrasshopperDll)
clr.AddReference(GH_IODll)
clr.AddReference(GH_UtilDll)

# Set up ready, now do the actual Rhino usage

import System
import Rhino

import Grasshopper
from Grasshopper.Kernel import GH_Document, GH_SolutionMode
from GH_IO.Serialization import GH_Archive
from Grasshopper.Kernel.Data import GH_Path
from Grasshopper.Kernel.Types import GH_Number

import random

definition = GH_Document()
archive = GH_Archive()
archive.ReadFromFile(r"N:\gh\inside_addition.gh")

archive.ExtractObject(definition, "Definition")


for ob in definition.Objects:
    print(f"{ob.NickName}:")
    for inp in ob.Params.Input:
        inp.VolatileData.Clear()
        n = float(random.randint(0, 100))
        ghn = GH_Number(n)
        inp.AddVolatileData(GH_Path(0), 0, GH_Number(n))
        print(f"\tSet input {inp.NickName} to {ghn}")

definition.NewSolution(True, GH_SolutionMode.Silent)

for ob in definition.Objects:
    for output in ob.Params.Output:
        output.CollectData()
        output.ComputeData()

        pathcount = output.VolatileData.PathCount
        idx = 0
        while idx < pathcount:
            b = output.VolatileData.get_Branch(idx)
            for item in b:
                print(f"\toutput {output.NickName} -> {item.Value}")
            idx = idx + 1

Output of a test run:

D:\Dev\Rhino\rhinoinside_cpython_test> python .\test.py
InsideAddition:
        Set input A to 34
        Set input B to 100
        output Result -> 134
1 Like

Thanks for your example, maybe you should add it to the CPython example on Github (https://github.com/mcneel/rhino.inside-cpython).
But even with the line

definition.NewSolution(True, GH_SolutionMode.Silent)

the example with the python component I posted does not work :thinking:.
What am I doing wrong / what is missing?

No, I don’t know why the script component does not want to work

1 Like

@nathanletwory do you know anything new related to this topic?

I have no new information regarding py script components in a definition when used like this.

Hi @nathanletwory ,

I was trying a script which is a little larger than just addition. However, I’m not able to retreive the data.

Could you please help?

Best,

Rickinside_addition.gh (4.0 KB) testing2.py (1.6 KB)

Hi Nathan,
Thanks for the example. I’m trying to run it but I get two errors, which look like the object types I get are different.

for inp in ob.Params.Input:
AttributeError: 'IGH_DocumentObject' object has no attribute 'Params'

It looks like this can be fixed by replacing ob.Params with Grasshopper.Kernel.IGH_Component(ob).Params.

But then I still get another error in the penultimate line:

print(f"\toutput {output.NickName} -> {item.Value}")
AttributeError: 'IGH_Goo' object has no attribute 'Value'

I tried casting it to a number:
item.CastTo(Grasshopper.Kernel.Types.GH_Number()) - returns success False, and the value is 0.

What type was the ob and item when you ran your script?
Is there smth in the Grasshopper SDK that has changed since your post maybe? Do you or anyone happens how to fix it?

I use Rhino 7 for windows, python 3.7.12.

Many thanks :wink:

My Rhino.Inside with CPython is very rusty, perhaps @fraguada or @AndyPayne can help here.

Hi Nathan,
thanks for you prompt reply.

Hello Luis @fraguada and Andy @AndyPayne,
I’m trying to set and get values to/from a headless Grasshopper with rhino.inside and cpython. Here is my current code, adapted from the examples above (here and here). It’s a dummy example - the GH file contains some simple components (Addition, Line, Integer, Number) for which I try to set or retrieve data (internalized or computed).

Setting inputs in “Addition” component seems to work (no errors). But to retrieve data, I struggle to get values and geometry out of the IGH_Goo objects: e.g. CastTo(Grasshopper.Kernel.Types.GH_Number() and CastTo(Rhino.Geometry.NurbsCurve()) return success: False and values are zero.
What is the proper way of doing this?

Also, as a newbie to GH SDK, I am not sure if the GH solution has run properly at all (whether I should expected it to run correctly with this code).

The files to reproduce: inside_addition_mix.gh inside_addition_mix.py
image

#WIP with python3.7, Rhino 7 on Windows10

import os
import random

import rhinoinside
rhinoinside.load()
import Rhino #any difference to "Rhino = rhinoinside.Rhino" ?

#Start grasshopper in "headless" mode
pluginObject = Rhino.RhinoApp.GetPlugInObject("Grasshopper")
if pluginObject: pluginObject.RunHeadless()

import Grasshopper
from Grasshopper.Kernel import GH_SolutionMode
from Grasshopper.Kernel.Data import GH_Path
from Grasshopper.Kernel.Types import GH_Number

currentpath = os.path.dirname(__file__)
filename = "inside_addition_mix.gh"
ghfile = os.path.join(currentpath, filename)
if not os.path.exists(ghfile): print("This file does not exists:", ghfile)

ghdocIO = Grasshopper.Kernel.GH_DocumentIO() 
ghdocIO.Open(ghfile)
ghdoc = ghdocIO.Document
ghdoc.Enabled = True
#ghdoc.ExpireSolution() #needed?

for obj in ghdoc.Objects:

    #Example 1: Set input values A and B to an "Addition" component and retrieve the Result.
    if obj.NickName == "Addition": 
        print("* GH component: ", obj.NickName, obj.Name, obj.Category, type(obj))
        component = Grasshopper.Kernel.IGH_Component(obj)
        for inp in component.Params.Input:
            inp.VolatileData.Clear()
            n = float(random.randint(0, 100))
            ghn = GH_Number(n)
            inp.AddVolatileData(GH_Path(0), 0, GH_Number(n))
            print(f"\tSet input {inp.NickName} to {ghn}")
        
        component.ComputeData() #needed?
        #ghdoc.NewSolution(True, GH_SolutionMode.Silent) #needed?
        
        for outp in component.Params.Output: 
            outp.CollectData() #needed?
            outp.ComputeData() #needed?
            print(f"\tGet output {outp.NickName}")
            pathcount = outp.VolatileData.PathCount
            for idx in range(pathcount):
                b = outp.VolatileData.get_Branch(idx)
                for item in b:
                    #type(item): Grasshopper.Kernel.Types.IGH_Goo
                    success, number = item.CastTo(Grasshopper.Kernel.Types.GH_Number())
                    print("\t\titem:", type(item), "-->", success, number, number.Value)

    # Example 2: Get a value stored in a component of a Category "Params"
    if obj.NickName in ("InternalizedInteger" , "AdditionResult"):
        print("* GH component: ", obj.NickName, obj.Name, obj.Category, type(obj))
        param = Grasshopper.Kernel.IGH_Param(obj)
        param.CollectData()
        param.ComputeData()
        for item in param.VolatileData.AllData(True): 
            success, number = item.CastTo(Grasshopper.Kernel.Types.GH_Number())
            print("\t\titem:", type(item), "-->", success, number, number.Value)

    # Example 3: Retrieve a geometric object (a line):
    if obj.NickName == "InternalizedLine":
        print("* GH component: ", obj.NickName, obj.Name, obj.Category, type(obj))
        param = Grasshopper.Kernel.IGH_Param(obj)
        param.CollectData()
        param.ComputeData()
        for item in param.VolatileData.AllData(True): 
            #success, line = item.CastTo(Rhino.Geometry.Line()) #!!! TypeError: No method matches given arguments for Line..ctor: ()
            success, line = item.CastTo(Rhino.Geometry.NurbsCurve())
            print("\t\titem:", type(item), "-->", success, line)
            print("\t\t",line.PointAtStart, line.PointAtEnd) #should be [1,1,0] and [2,2,0]

Many thanks for any nudge or hint :wink:

Hi folks,

Meanwhile I have arrived at a code that is reasonably functional and allows to control a Grasshopper file in a headless mode from an external cpython process using rhinoinside, to:

  • set values to “primitive” parameters such as bool, num, int etc., as well as to ghpython component’s inputs
  • read values from a component (flattened).

It is available here on Github - contributions are welcome! :wink:

1 Like

Hey!
Thanks for this discussion. I can’t manage to make it work though. I tried a piece of your code, but when i try to use component = gh.Kernel.IGH_Component(obj), i get TypeError: object does not implement IGH_Component error. I tried multiple things but nothing seems to solve the issue.
I work on Rhino8, did you try your code on this?