Change the parameters in step 2

Hello Rick,

We have now released the app. Unfortunately, we have noticed that when we change the input values in Step 2, the output data does not change. What could be the reason for this? When we change the inputs, the Power Shell shows us that the input values have been changed but the output has not.
In this example, we have reduced the “Eigenverbrauch” from 25% to 5%:

‘step_2’: Munch({‘Modultyp’: 0, ‘Eigenverbrauch’: 25, ‘Investitionskosten’: 2, ‘Instandhaltungskosten’: 0.02, ‘Foerderbeitrag’: 6, ‘Strompreis’: 0.3, ‘Einspeiseverguetung’: 0.12, ‘Zeitraum’: 25})})
2024-10-08 19:56:44.684 INFO : External analysis not yet finished, waiting 1 seconds to retry

2024-10-08 19:56:45.870 INFO : External analysis not yet finished, waiting 2 seconds to retry

2024-10-08 19:56:47.977 INFO : External analysis not yet finished, waiting 3 seconds to retry

2024-10-08 19:56:51.078 INFO : External analysis not yet finished, waiting 4 seconds to retry

2024-10-08 19:56:55.368 INFO : External analysis completed successfully, returning results
{‘Einspeiseverguetung’: ‘1920 CHF’, ‘EinnahmeJahr’: ‘6719 CHF’, ‘Stromkosteneinsparung’: ‘4800 CHF’, ‘Einnahmen’: ‘167975 CHF / Einnahmen in X Jahren’, ‘Unterhaltskosten’: ‘2894 CHF’, ‘KostenproJahr’: ‘2894 CHF’, ‘Investitionskosten’: ‘144710 CHF’, ‘KostenGesamt’: ‘72355 CHF’, ‘Rendite’: ‘5 %’, ‘Amortisationsdauer’: ‘20 Jahre’, ‘ProduktionFassade’: ‘31997 kWh/a’}
2024-10-08 19:56:55.551 INFO : Job (uid: 1022775) completed in 20826ms

, ‘step_2’: Munch({‘Modultyp’: 0, ‘Eigenverbrauch’: 5, ‘Investitionskosten’: 300, ‘Instandhaltungskosten’: 0.02, ‘Foerderbeitrag’: 6, ‘Strompreis’: 0.3, ‘Einspeiseverguetung’: 0.12, ‘Zeitraum’: 25})})
2024-10-08 19:58:44.373 INFO : External analysis not yet finished, waiting 1 seconds to retry

2024-10-08 19:58:45.566 INFO : External analysis not yet finished, waiting 2 seconds to retry

2024-10-08 19:58:47.751 INFO : External analysis not yet finished, waiting 3 seconds to retry

2024-10-08 19:58:50.848 INFO : External analysis not yet finished, waiting 4 seconds to retry

2024-10-08 19:58:55.153 INFO : External analysis completed successfully, returning results
{‘Einspeiseverguetung’: ‘1920 CHF’, ‘EinnahmeJahr’: ‘6719 CHF’, ‘Stromkosteneinsparung’: ‘4800 CHF’, ‘Einnahmen’: ‘167975 CHF / Einnahmen in X Jahren’, ‘Unterhaltskosten’: ‘2894 CHF’, ‘KostenproJahr’: ‘2894 CHF’, ‘Investitionskosten’: ‘144710 CHF’, ‘KostenGesamt’: ‘72355 CHF’, ‘Rendite’: ‘5 %’, ‘Amortisationsdauer’: ‘20 Jahre’, ‘ProduktionFassade’: ‘31997 kWh/a’}
2024-10-08 19:58:55.342 INFO : Job (uid: 1022781) completed in 20744ms

this is our app.py code:

from io import BytesIO
import json
import rhino3dm
from viktor import ViktorController, File
from viktor.parametrization import NumberField, FileField, Text, ViktorParametrization, Step, OptionField, \
    OptionListElement, GeometrySelectField, GeometryMultiSelectField
from viktor.external.generic import GenericAnalysis
from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem, GeometryView, GeometryResult


class Parametrization(ViktorParametrization):
    step_1 = Step('Modell_Modultyp', views='choose_geom')
    step_1.intro = Text(
        "## PV Machbarkeitsanalyse [PARAMETRICZONE](https://www.parametriczone.ch/) \n Dieses Script berechnet die ersten Kennzahlen welche fĂŒr die Realisierung einer PV-Anlage an einer Fassade benötigt werden. Dabei stĂŒtzt sich die Berechnung auf angenommene Standardwerte. Im zweiten Tab können die Input Werte beliebig verĂ€ndert werden, zu jedem Wert ist fĂŒr die Nachvollziehbarkeit eine Beschreibung hinterlegt. \n\n In naher Zukunft wird eine neue App verfĂŒgbar sein, die eine prĂ€zisere Berechnung der Leistungswerte von PV-Fassaden ermöglicht. Sie wird das optimal geeignete PV-Modul fĂŒr jede Fassade identifizieren, die Anordnung der Module automatisch planen und optimieren sowie die ideale Stringverschaltung bestimmen. Dadurch wird der Einsatz kostenintensiver und störanfĂ€lliger Leistungsoptimierer ĂŒberflĂŒssig, was zu einer effizienteren und zuverlĂ€ssigeren Nutzung von PV-Fassaden fĂŒhrt. \n\n\n Laden sie Ihre FassadenflĂ€che und Kontextgeometrien als 3dm Datei hoch. Wichtig ist, dass die Nordrichtung in der hochgeladenen Datei richtig eingestellt ist und alle Geometien mĂŒssen einen Namen enthalten, damit sie vom Script erkannt werden können. Falls sie Probleme mit ihrem hochgeladenen Modell haben melden sie sich hier: Mail: pvplanningsyst@gmail.com / [Vorlage Modell](https://www.parametriczone.ch/) \n\n\n\n Geben sie die vorgegebenen Parameter an:")

    # Input fields

    step_1.selected_geometry = GeometrySelectField("WÀhle die zu berechnende FassadenflÀche aus")
    step_1.selected_geometries = GeometryMultiSelectField("WĂ€hle die Umgebungsgeometrie aus")
    step_1.Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)

    step_2 = Step('Angaben', views='run_grasshopper')
    step_2.intro = Text(
        "## Angaben der Inputparameter \n Zum Nachvollziehen werden die Inputparameter untenstehend beschrieben: \n\n Modultyp: 4 Standard-Modultypen sind vorgegeben \n\n\n Eigenverbrauch: Liegt durchschnittlich bei ca. 20 bis 35 Prozent \n\n\n\n Investitionskosten: Durchschnittlich 380 CHF/m2 (inkl. Installation) unter folgendem Link finden sie Richtwerte der Schweiz:[Investitionskosten Richtwerte](https://www.photovoltaikmeister.ch/arten/photovoltaik-fassade/) \n\n\n\n\n Instandhaltungskosten: Erfahrungswert 2 Prozent der Investitionskosten  \n\n\n\n\n\n Foerderbeitrag: [FörderbeitrĂ€ge Beschrieb](https://www.swissolar.ch/de/wissen/wirtschaftlichkeit/foerderung/pv-foerderung) [ZĂŒrich](https://www.stadt-zuerich.ch/energie/de/index/foerderung/alle-foerderprogramme/photovoltaik.html) [Luzern](https://www.stadtluzern.ch/dienstleistungeninformation/6253) [Zug]( (in Prozent der Investitionskosten (falls die Investitionskosten in CHF angegeben sind muss der Wert berechnet werden und als Prozent Angabe eingegeben werden)) \n\n\n\n\n\n\n Strompreis: [Strompreise pro Gemeinde](https://www.nzz.ch/wirtschaft/die-strompreise-sinken-wieder-in-welchen-gemeinden-die-tarife-naechstes-jahr-besonders-guenstig-sind-und-wo-sie-entgegen-dem-nationalen-trend-steigen-ld.1846692) (in CHF/kwh) \n\n\n\n\n\n\n\n EinspeisevergĂŒtung: [EinspeisevergĂŒtungĂŒbersicht Regionen](https://www.vese.ch/pvtarif/) (in CHF/kwh) \n\n\n\n\n\n\n\n\n Zeitraum: Zeitraum der Berechnung (Durchschnittliche Lebensdauer PV-Anlage = 20-25 Jahre)"
    )

    step_2.Modultyp = OptionField('Modultyp', options=[OptionListElement('0', 'L 1300x875'),
                                                       OptionListElement('1', 'Q 1300x720'),
                                                       OptionListElement('2', 'M 985x875'),
                                                       OptionListElement('3', 'S 985x720')], default=0)
    step_2.Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=25)
    step_2.Investitionskosten = NumberField( "Investitionskosten(Fr./m2)", default=380 )
    step_2.Instandhaltungskosten = NumberField( "Instandhaltungskosten in % der Investitionskosten", default=0.02 )
    step_2.Foerderbeitrag = NumberField("Förderbeitrag %", default=6)
    step_2.Strompreis = NumberField("Strompreis(Fr./kWh)", default=0.3)
    step_2.Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=0.12)
    step_2.Zeitraum = NumberField("Zeitraum(Jahre)", default=25)


class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization(width=30)

    @GeometryView("Geometry", duration_guess=0, x_axis_to_right=True)
    def choose_geom(self, params, **kwargs):
        return GeometryResult(geometry=params.step_1.Modell.file, geometry_type="3dm")

    @GeometryAndDataView("Geometry", duration_guess=0, update_label='Run Grasshopper')
    def run_grasshopper(self, params, **kwargs):


        rhino_upload = params.step_1['Modell'].file

        # Read the rhino file
        gh_file = rhino3dm.File3dm.FromByteArray(rhino_upload.getvalue_binary())

        encoded_facade = []
        encoded_context = []

        # Durchlaufen aller Objekte und Kodieren der zum gewĂŒnschten Layer gehörenden
        for obj in gh_file.Objects:
            if params.step_1.selected_geometry == obj.Attributes.Name:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data = json.dumps(geom.Encode())
                encoded_facade.append(encoded_data)

        for obj in gh_file.Objects:
            if obj.Attributes.Name in params.step_1.selected_geometries:
            #if params.step_1.selected_geometries == obj.Attributes.Name:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data2 = json.dumps(geom.Encode())
                encoded_context.append(encoded_data2)

        # Erstellen einer JSON-Datei aus den Eingabeparametern
        # new dictinary
        params.step_1.pop('Modell')
        params.step_1['encoded_geometry'] = encoded_facade
        params.step_1['encoded_geometry2'] = encoded_context
        input_json = json.dumps(params.step_1)  # Konvertieren aller Parameter inklusive der Geometrie in JSON
        print(params)
        # Erstellen der Eingabedateien fĂŒr die Analyse
        files = [
            ('input.json', BytesIO(bytes(input_json, 'utf8'))),
        ]

        # Run the Grasshopper analysis and obtain the output files
        generic_analysis = GenericAnalysis(files=files, executable_key="run_grasshopper", output_filenames=[
            "geometry.3dm", "output.json"
        ])
        generic_analysis.execute(timeout=120)
        params.step_1.rhino_3dm_file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
        params.step_2.output_values: File = generic_analysis.get_output_file("output.json", as_file=True)

        # Create a DataGroup object to display output data
        params.step_2.output_dict = json.loads(params.step_2.output_values.getvalue())
        print(params.step_2.output_dict)
        params.step_2.data_group = DataGroup(
            *[DataItem(key.replace("_", " "), val) for key, val in params.step_2.output_dict.items()]
        )

        return GeometryAndDataResult(geometry=params.step_1.rhino_3dm_file, geometry_type="3dm", data=params.step_2.data_group)

and this is our run grasshopper.py script:

# Pip install required packages
import os
import json
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm

# Set the compute_rhino3d.Util.url, default URL is http://localhost:6500/
compute_rhino3d.Util.url = 'http://localhost:6500/'

# Define path to local working directory
workdir = os.getcwd() + '\\'

# Read input parameters from JSON file
with open(workdir + 'input.json') as f:
    input_params = json.load(f)

# Create the input DataTree
input_trees = []
for key, value in input_params.items():
    tree = gh.DataTree(key)
    if isinstance(value,list) :
        tree.Append([{0}], value)
    else:
        tree.Append([{0}], [str(value)])
    input_trees.append(tree)

# Evaluate the Grasshopper definition
output = gh.EvaluateDefinition(
    workdir + 'sample_box_grasshopper.gh',
    input_trees
)


def get_value_from_tree(datatree: dict, param_name: str):
    """Get first value in datatree that matches given param_name"""
    for val in datatree['values']:
        if val["ParamName"] == param_name:
            return val['InnerTree']['{0}'][0]['data']


# Create a new rhino3dm file and save resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = get_value_from_tree(output, "Geometry")
obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)

# Save Rhino file to working directory
file.Write(workdir + 'geometry.3dm', 7)

units = {
    "Einspeiseverguetung": "CHF",
    "EinnahmeJahr": "CHF",
    "Stromkosteneinsparung": "CHF",
    "Einnahmen": "CHF / Einnahmen in X Jahren",
    "Unterhaltskosten": "CHF",
    "KostenproJahr": "CHF",
    "Investitionskosten": "CHF",
    "KostenGesamt": "CHF",
    "Rendite": "%",
    "Amortisationsdauer": "Jahre",
    "ProduktionFassade": "kWh/a"
}

# Parsing und Einheiten hinzufĂŒgen
output_values = {}
for key in ["Einspeiseverguetung", "EinnahmeJahr", "Stromkosteneinsparung", "Einnahmen", "Unterhaltskosten", "KostenproJahr", "Investitionskosten", "KostenGesamt", "Rendite", "Amortisationsdauer", "ProduktionFassade"]:
    val = get_value_from_tree(output, key)
    val = val.replace("\"", "")
    # Einheit hinzufĂŒgen
    if key in units:
        output_values[key] = f"{val} {units[key]}"
    else:
        output_values[key] = val

# Speichern der JSON-Datei im Arbeitsverzeichnis
with open(workdir + 'output.json', 'w') as f:
    json.dump(output_values, f, ensure_ascii=False, indent=4)

thanks alot
cheers Samuel & Marco

Hi Samuel and Marco,

I don’t see something right off the bat. Some things to check:

  • Does the hops input match exactly with the name? Otherwise the default will be used in Grasshopper. Easy way to check your grasshopper script is working is by using Hops.
  • Print the input_params in your run_grasshopper.py

Also, I would advise on using your own variables in stead of overriding params.

        generic_analysis.execute(timeout=120)
        file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
        output_values: File = generic_analysis.get_output_file("output.json", as_file=True)

        # Create a DataGroup object to display output data
        output_dict = json.loads(output_values.getvalue())
        print(output_dict)
        data_group = DataGroup(
            *[DataItem(key.replace("_", " "), val) for key, val in output_dict.items()]
        )
       return GeometryAndDataResult(geometry=file, geometry_type="3dm", data=data_group)

Hi Rick,
Thanks for your help.

We had an error with the input parameters, one was different in the Grasshopper than in the app.py script. Nevertheless, the output does not change according to the input.
We have now tested the functionality of the script with Hops. In the Grasshopper script we get one value per output. If we test the whole thing with Hops, we get several values per output. The input areas of encoded_geometry2 “Surrounding area” are calculated instead of the area of encoded_geometry.

output grasshopper script:

output with hops:

Hi @PVSyst,

Your json.dumps(params.step_1) is only targeting the variables in step_1.

To fully have control over your parameters, I would advise in creating your own input_parameters dictionary. Like:

input_parameters = {
    'encoded_geometry': encoded_facade,
    'encoded_geometry2': encoded_context,
    'Eigenverbrach': params.step_2.Eigenverbrach,
    ...
    'Zeitraum': params.step_2.Zeitraum
}

This way you have full control over the keys (=grasshopper inputs) and the values (the actual value).

1 Like