Skip to main content

Tutorial - Integrate Dynamo

note

Estimated time: 45 minutes
Difficulty level: Intermediate

  • Introduction

    Welcome to this tutorial on creating an app that integrates with Dynamo! In this tutorial, we will explore how to render a basic house with Dynamo and visualize it in your app. As a starting point, the user will provide parameters such as the number of houses, width, depth, number of floors, and heights for floors and roofs. These parameters are sent to Dynamo Sandbox using a worker, the program that establishes the connection between VIKTOR and the third-party software, and are used to generate the geometry of the house. The geometry is then converted to a mesh, which is rendered and visualized in VIKTOR. In this tutorial, we will cover the step-by-step process of setting up a VIKTOR app with a Dynamo integration:

    By the end of this tutorial, you will have created a simple VIKTOR application that generates the geometry of a house and calculates some basic properties. For the final result see the GIF below:

    tip

    You can find the complete code below

    Pre-requisites

    Prerequisites
    • You have some experience with reading Python code

    In the tutorial, we added some links to additional information; but don't let them distract you too much. Stay focused on completing the tutorial. After this, you will know everything you need to create an app which includes an integration with Dynamo (Sandbox).

    1. Setup a VIKTOR app

    Create an empty app

    Let's create, install and start an empty app. This will be the starting point for the rest of the tutorial. But before we start, make sure to shut down any app that is running (like the demo app) by closing the command-line shell (for example Powershell) or end the process using Ctrl + C.

    Follow these steps to create, install and start an empty app:

    1. Go to the App store in your VIKTOR environment to create a new app. After clicking 'Create app' choose the option 'Create blank app' and enter a name and description of your choice. Submit the form by clicking 'Create and setup'.

    1. Select 'Editor' as app type and click 'Next'.
    2. Now follow the instructions to run the quickstart command to download the empty app template. After entering the command click 'I have run the command' to continue. The CLI will ask you to select your code editor of choice. Use the arrows and press enter to select a code editor. The app will now open in your code editor of choice.

    If all went well, your empty app is installed and connected to your development workspace. Do not close the terminal as this will break the connection with your app. The terminal in your code editor should show something like this:

    INFO    : Connecting to cloud.viktor.ai...
    INFO : Connection is established:
    INFO :
    INFO : https://cloud.viktor.ai/workspaces/XXX/app <--- navigate here to find your app
    INFO :
    INFO : The connection can be closed using Ctrl+C
    INFO : App is ready
    Re-starting your app
    • You only need create an app template and install it once for each new app you want to make.
    • The app will update automatically once you start adding code in app.py, as long as you don't close the terminal or your code editor.
    • Did you close your code editor? Use viktor-cli start to start the app again. No need to install, clear, etc.

    Add input fields

    We will add 6 numeric input fields to our app: number_of_houses, number_of_floors, depth, width, height_floor and height_roof. For these inputs a NumberField will be used. Open app.py, and add the relevant fields to the Parametrization class. If you like you can accompany the fields with a descriptive text. After adding the fields your app.py file should look like this:

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    Refresh the development workspace in your browser and you should the input fields appear.

    2. Create a method to update Dynamo file

    In this section, we will define a function that updates the Dynamo file with the parameters provided by the user. This will be done by defining a staticmethod on the Controller class, similar to what is described in the Dynamo section of the VIKTOR documentation.

    Before you continue, download the sample dynamo file and save it the app folder next to app.py. The following code will update the nodes of the Dynamo file and generate an input file with the parameters defined in your app:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    Let us go through the above mentioned logic:

    1. Retrieve the input files for the analysis, in this case the dynamo_model_sample_app.dyn file, and create a DynamoFile object instantiated from the dynamo_model_sample_app.dyn file. You can download the sample dynamo model here.
    2. With the update method, the value of input nodes can be updated.
    3. When all inputs have been updated as desired, the generate method can be used to generate an updated File object.
    note

    Don't forget to import Path from the pathlib module

    3. Create a GeometryView with mocked output files

    In this section we will add a GeometryView to display the house's geometry. However, for now, we will use a mocked output file. In section 7 we will create the json file using the Dynamo script.

    Before you continue, download the mocked output file here and save it in your app folder next to app.py. To visualize the mocked output add a GeometryView method to your Controller class, see code below:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @vkt.GeometryView("Mocked 3d model", x_axis_to_right=True)
    def mocked_geometry_view(self, params, **kwargs):
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    file_path = Path(__file__).parent / "Mocked_3d_model.json"
    threed_file = vkt.File.from_path(file_path)

    # Step 3: Processing geometry
    glb_file = vkt.dynamo.convert_geometry_to_glb(threed_file)

    return vkt.GeometryResult(geometry=glb_file)

    Let us go through the above mentioned logic:

    1. Update the Dynamo file with the update_model method(see section 2)
    2. For now we are using a mocked file, instead of running the analysis in Dynamo. The mocked file can be downloaded here. In section 7 it is explained how to create the json file using the Dynamo script.
    3. With the helper function convert_geometry_to_glb, you can convert the json file to a GLB file, which can directly be visualized in a GeometryView.
    4. Refresh your app in the browser, and you should see a 3D model of a house.

    4. Create a data processing method

    In this section, we will define code to extract data from the dynamo files. The dynamo file is used to get the node ID, and the output.xml file is used to get the result values. To do this we will add another staticmethod on the Controller class. See code below:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @vkt.GeometryView("Mocked 3d model", x_axis_to_right=True)
    def mocked_geometry_view(self, params, **kwargs):
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    file_path = Path(__file__).parent / "Mocked_3d_model.json"
    threed_file = vkt.File.from_path(file_path)

    # Step 3: Processing geometry
    glb_file = vkt.dynamo.convert_geometry_to_glb(threed_file)

    return vkt.GeometryResult(geometry=glb_file)

    @staticmethod
    def convert_dynamo_file_to_data_items(input_file: vkt.dynamo.DynamoFile, output_file: vkt.File) -> vkt.DataGroup:
    """Extracts the output of the Dynamo results by using the input and output files."""
    # Collect ids for the computational output from the Dynamo file
    output_id_floor_area = input_file.get_node_id("(OUTPUT) Floor area per house")
    output_id_total_cost = input_file.get_node_id("(OUTPUT) Total cost")
    output_id_mki = input_file.get_node_id("(OUTPUT) MKI")
    output_id_co2 = input_file.get_node_id("(OUTPUT) CO2")

    # Collect the numerical results from the output file using the collected ids
    with output_file.open_binary() as f:
    floor_area = vkt.dynamo.get_dynamo_result(f, id_=output_id_floor_area)
    total_cost = vkt.dynamo.get_dynamo_result(f, id_=output_id_total_cost)
    mki = vkt.dynamo.get_dynamo_result(f, id_=output_id_mki)
    co2 = vkt.dynamo.get_dynamo_result(f, id_=output_id_co2)

    # Add values to a DataGroup to visualize them in the app
    data_group = vkt.DataGroup(
    vkt.DataItem(label="Floor area", value=round(float(floor_area), 2), suffix="m²"),
    vkt.DataItem(label="Total cost", value=round(float(total_cost), 2), suffix="€"),
    vkt.DataItem(label="MKI", value=round(float(mki), 2)),
    vkt.DataItem(label="CO₂ emission", value=round(float(co2), 2), suffix="ton CO₂"),
    )
    return data_group

    Let us go through the above mentioned logic:

    1. Get the node_id, which corresponds to the same node id as the input file.
    2. Collect the numerical results.
    3. Create a DataGroup from the collected numerical results using the DataGroup and DataItem classes.

    5. Create a DataView with mocked output files

    In this section we will create a DataView to display the data. However, for now, we will use a mocked output file. In section 7 we will generate the xml file using the Dynamo script.

    Before you continue, download the result file here and save it in your app folder next to app.py. To visualize the mocked output add a DataView method to your Controller class, see code below:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @vkt.GeometryView("Mocked 3d model", x_axis_to_right=True)
    def mocked_geometry_view(self, params, **kwargs):
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    file_path = Path(__file__).parent / "Mocked_3d_model.json"
    threed_file = vkt.File.from_path(file_path)

    # Step 3: Processing geometry
    glb_file = vkt.dynamo.convert_geometry_to_glb(threed_file)

    return vkt.GeometryResult(geometry=glb_file)

    @staticmethod
    def convert_dynamo_file_to_data_items(input_file: vkt.dynamo.DynamoFile, output_file: vkt.File) -> vkt.DataGroup:
    """Extracts the output of the Dynamo results by using the input and output files."""
    # Collect ids for the computational output from the Dynamo file
    output_id_floor_area = input_file.get_node_id("(OUTPUT) Floor area per house")
    output_id_total_cost = input_file.get_node_id("(OUTPUT) Total cost")
    output_id_mki = input_file.get_node_id("(OUTPUT) MKI")
    output_id_co2 = input_file.get_node_id("(OUTPUT) CO2")

    # Collect the numerical results from the output file using the collected ids
    with output_file.open_binary() as f:
    floor_area = vkt.dynamo.get_dynamo_result(f, id_=output_id_floor_area)
    total_cost = vkt.dynamo.get_dynamo_result(f, id_=output_id_total_cost)
    mki = vkt.dynamo.get_dynamo_result(f, id_=output_id_mki)
    co2 = vkt.dynamo.get_dynamo_result(f, id_=output_id_co2)

    # Add values to a DataGroup to visualize them in the app
    data_group = vkt.DataGroup(
    vkt.DataItem(label="Floor area", value=round(float(floor_area), 2), suffix="m²"),
    vkt.DataItem(label="Total cost", value=round(float(total_cost), 2), suffix="€"),
    vkt.DataItem(label="MKI", value=round(float(mki), 2)),
    vkt.DataItem(label="CO₂ emission", value=round(float(co2), 2), suffix="ton CO₂"),
    )
    return data_group

    @vkt.DataView("Mocked data results")
    def mocked_data_view(self, params, **kwargs):
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    file_path = Path(__file__).parent / "Mocked_data_results.xml"
    data_file = vkt.File.from_path(file_path)

    # Step 3: Process numerical output
    data_group = self.convert_dynamo_file_to_data_items(dynamo_file, data_file)

    return vkt.DataResult(data=data_group)

    Let us go through the above mentioned logic:

    1. Update the Dynamo file with the update_model method (see section 1)
    2. For now we are using a mocked file, instead of running the analysis in Dynamo. The mocked file can be downloaded here.
    3. With the static method convert_dynamo_file_to_data_items, you can convert the .xml file to a DataGroup, which can be directly visualized in a DataView.
    4. Refresh your app, and you should see a data view with the data.

    6. Create a GeometryAndDataView with the mocked output files

    In this section we will combine the geometry view and data view in one view using the GeometryAndDataView class. We will use the mocked files. To this end, replace the mocked_data_view and the mocked_geometry_view methods with a single geometry_and_data_view method:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @staticmethod
    def convert_dynamo_file_to_data_items(input_file: vkt.dynamo.DynamoFile, output_file: vkt.File) -> vkt.DataGroup:
    """Extracts the output of the Dynamo results by using the input and output files."""
    # Collect ids for the computational output from the Dynamo file
    output_id_floor_area = input_file.get_node_id("(OUTPUT) Floor area per house")
    output_id_total_cost = input_file.get_node_id("(OUTPUT) Total cost")
    output_id_mki = input_file.get_node_id("(OUTPUT) MKI")
    output_id_co2 = input_file.get_node_id("(OUTPUT) CO2")

    # Collect the numerical results from the output file using the collected ids
    with output_file.open_binary() as f:
    floor_area = vkt.dynamo.get_dynamo_result(f, id_=output_id_floor_area)
    total_cost = vkt.dynamo.get_dynamo_result(f, id_=output_id_total_cost)
    mki = vkt.dynamo.get_dynamo_result(f, id_=output_id_mki)
    co2 = vkt.dynamo.get_dynamo_result(f, id_=output_id_co2)

    # Add values to a DataGroup to visualize them in the app
    data_group = vkt.DataGroup(
    vkt.DataItem(label="Floor area", value=round(float(floor_area), 2), suffix="m²"),
    vkt.DataItem(label="Total cost", value=round(float(total_cost), 2), suffix="€"),
    vkt.DataItem(label="MKI", value=round(float(mki), 2)),
    vkt.DataItem(label="CO₂ emission", value=round(float(co2), 2), suffix="ton CO₂"),
    )
    return data_group

    @vkt.GeometryAndDataView("Mocked 3d/data", x_axis_to_right=True)
    def geometry_and_data_view(self, params, **kwargs):
    """The endpoint that initiates the logic to visualize the geometry and data executed
    and retrieved from a Dynamo script."""
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    file_path = Path(__file__).parent / "Mocked_3d_model.json"
    threed_file = vkt.File.from_path(file_path)

    file_path = Path(__file__).parent / "Mocked_data_results.xml"
    data_file = vkt.File.from_path(file_path)

    # Step 3: Processing geometry
    glb_file = vkt.dynamo.convert_geometry_to_glb(threed_file)

    # Step 4: Process numerical output
    data_group = self.convert_dynamo_file_to_data_items(dynamo_file, data_file)

    return vkt.GeometryAndDataResult(geometry=glb_file, data=data_group)

    7. Integrate Dynamo

    In this section, we will implement the logic to trigger the Dynamo calculation from your app. We will update step 2 of the geometry_and_data_view to run the Dynamo calculation. Please refer to the code snippet below:

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @staticmethod
    def convert_dynamo_file_to_data_items(input_file: vkt.dynamo.DynamoFile, output_file: vkt.File) -> vkt.DataGroup:
    """Extracts the output of the Dynamo results by using the input and output files."""
    # Collect ids for the computational output from the Dynamo file
    output_id_floor_area = input_file.get_node_id("(OUTPUT) Floor area per house")
    output_id_total_cost = input_file.get_node_id("(OUTPUT) Total cost")
    output_id_mki = input_file.get_node_id("(OUTPUT) MKI")
    output_id_co2 = input_file.get_node_id("(OUTPUT) CO2")

    # Collect the numerical results from the output file using the collected ids
    with output_file.open_binary() as f:
    floor_area = vkt.dynamo.get_dynamo_result(f, id_=output_id_floor_area)
    total_cost = vkt.dynamo.get_dynamo_result(f, id_=output_id_total_cost)
    mki = vkt.dynamo.get_dynamo_result(f, id_=output_id_mki)
    co2 = vkt.dynamo.get_dynamo_result(f, id_=output_id_co2)

    # Add values to a DataGroup to visualize them in the app
    data_group = vkt.DataGroup(
    vkt.DataItem(label="Floor area", value=round(float(floor_area), 2), suffix="m²"),
    vkt.DataItem(label="Total cost", value=round(float(total_cost), 2), suffix="€"),
    vkt.DataItem(label="MKI", value=round(float(mki), 2)),
    vkt.DataItem(label="CO₂ emission", value=round(float(co2), 2), suffix="ton CO₂"),
    )
    return data_group

    @vkt.GeometryAndDataView("Building 3D", duration_guess=5, x_axis_to_right=True)
    def geometry_and_data_view(self, params, **kwargs):
    """Run an analysis using Dynamo and visualize the geometry and data"""
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    files = [
    ('input.dyn', input_file),
    ]

    analysis = vkt.dynamo.DynamoAnalysis(files=files, output_filenames=["output.xml", "geometry.json"])
    analysis.execute(timeout=60)

    # Step 3: Processing geometry
    geometry_file = analysis.get_output_file('geometry.json', as_file=True)
    glb_file = vkt.dynamo.convert_geometry_to_glb(geometry_file)

    # Step 4: Process numerical output
    output_file = analysis.get_output_file('output.xml', as_file=True)
    data_group = self.convert_dynamo_file_to_data_items(dynamo_file, output_file)

    return vkt.GeometryAndDataResult(geometry=glb_file, data=data_group)

    The steps in geometry_and_data_view explained:

    1. Update the Dynamo file with the update_model method (see section 2)
    2. We use the DynamoAnalysis to run the Dynamo script. The executable_key in the example above refers to the "dynamo" command in the configuration file of the worker. For more information on how to configure the worker, refer to next section.
    3. Processing the geometry, the .json file can be obtained with the get_ouput_file method. With the helper function convert_geometry_to_glb, you can convert it to a GLB type file, which can directly be visualized in a GeometryAndDataView.
    4. Processing geometry, the .xml file can be obtained with the get_ouput_file. method. With the static method convert_dynamo_file_to_data_items, you can convert the .xml file to a Datagroup, which can be directly visualized in GeometryAndDataView.

    8. Setting up a worker

    A worker is a program that connects your app with third-party software to execute tasks and send results. The worker communicates with the VIKTOR cloud via an encrypted connection. For the Dynamo integration, the Dynamo worker must be installed.

    Installation

    Follow these steps to install the worker:

    1. Navigate to the "My Integrations" tab in your personal settings

    2. Click "Add integration"

    3. Follow the steps provided in the modal

      3.1. Select Dynamo

      3.2. Download the worker .msi (Microsoft Installer) and run it on the machine of choice

      3.3. Copy the generated connection key and paste it when the installer asks for it. In the browser, you can now click Finish. Continue the installation in the installer wizard.

      Connection Key

      The generated connection key should be copied immediately as VIKTOR will not preserve this data for security reasons.

    4. In the installer wizard, select the DynamoWPFCLI executable

    5. Make sure to launch the worker once the installation is finished. If you closed the integration, you can restart it through the desktop shortcut.

    Configuration

    To configure the worker, you first need to install FormIt, which can be downloaded from here. After it is installed, you can set up the worker to execute the logic when the input files have been sent by the VIKTOR app. The configuration of the worker is defined in the config.yaml file, which can be found in the same folder where the worker is installed (C:\Users\{username}\AppData\Local\Viktor\ for development workers and C:\Program Files\Viktor\ for production workers). Edit the config.yaml file as follows:

    executables:
    dynamo:
    path: 'C:\Program Files\Autodesk\FormIt\DynamoSandbox\DynamoWPFCLI.exe'
    arguments:
    - '-o'
    - 'input.dyn'
    - '-v'
    - 'output.xml'
    - '-gp'
    - 'C:\Program Files\Autodesk\FormIt'
    - '-g'
    - 'geometry.json'
    maxParallelProcesses: 1
    • path: Here we define the path of the program to be executed.
    • arguments: Under this key we can list all arguments that can be added to the executable. This works similar to command-line arguments.
    • -o Open the dynamo script(input.dyn)
    • -v Ouput geometry file (name = output.xml)
    • -gp Path to local installation of Autodesk FormIt or Revit
    • -g Ouptut geometry file (name = geometry.json)

    For more information about the Dynamo CLI see the Dynamo CLI documentation

    Connection

    Once you have saved your config.yaml file, you can start the worker by double-clicking the shortcut on the desktop. If all went well, you will be presented with windon in which the message: "Successfully connected to the server" is displayed. Also in the top right corner in your VIKTOR environment you should see a green indicator in your worker overview, see the figure below.

    All code together

    Now that the worker is successfully connected you can open your development workspace and generate new housing configurations based on the supplied input parameters!

    from pathlib import Path

    import viktor as vkt


    class Parametrization(vkt.Parametrization):
    intro = vkt.Text("# 3D Dynamo app \n This app parametrically generates and visualises a 3D model of a house using a Dynamo script. \n\n Please fill in the following parameters:")
    number_of_houses = vkt.NumberField("Number of houses", max=8.0, min=1.0, variant='slider', step=1.0, default=3.0)
    number_of_floors = vkt.NumberField("Number of floors", max=5.0, min=1.0, variant='slider', step=1.0, default=2.0)
    depth = vkt.NumberField("Depth", max=10.0, min=5.0, variant='slider', step=1.0, default=8.0, suffix="m")
    width = vkt.NumberField("Width", max=6.0, min=4.0, variant='slider', step=1.0, default=5.0, suffix="m")
    height_floor = vkt.NumberField("Height floor", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')
    height_roof = vkt.NumberField("Height roof", max=3.0, min=2.0, variant='slider', step=0.1, default=2.5, suffix='m')


    class Controller(vkt.Controller):
    parametrization = Parametrization

    @staticmethod
    def update_model(params) -> tuple[vkt.File, vkt.dynamo.DynamoFile]:
    """This method updates the nodes of the Dynamo file with the parameters
    from the parametrization class."""

    # Create a DynamoFile object using the sample file
    file_path = Path(__file__).parent / "dynamo_model_sample_app.dyn"
    dyn_file = vkt.dynamo.DynamoFile(vkt.File.from_path(file_path))

    # Update the Dynamo file with parameters from user input
    dyn_file.update("Number of houses", params.number_of_houses)
    dyn_file.update("Number of floors", params.number_of_floors)
    dyn_file.update("Depth", params.depth)
    dyn_file.update("Width", params.width)
    dyn_file.update("Height floor", params.height_floor)
    dyn_file.update("Height roof", params.height_roof)

    # Generate updated file
    input_file = dyn_file.generate()

    return input_file, dyn_file

    @staticmethod
    def convert_dynamo_file_to_data_items(input_file: vkt.dynamo.DynamoFile, output_file: vkt.File) -> vkt.DataGroup:
    """Extracts the output of the Dynamo results by using the input and output files."""
    # Collect ids for the computational output from the Dynamo file
    output_id_floor_area = input_file.get_node_id("(OUTPUT) Floor area per house")
    output_id_total_cost = input_file.get_node_id("(OUTPUT) Total cost")
    output_id_mki = input_file.get_node_id("(OUTPUT) MKI")
    output_id_co2 = input_file.get_node_id("(OUTPUT) CO2")

    # Collect the numerical results from the output file using the collected ids
    with output_file.open_binary() as f:
    floor_area = vkt.dynamo.get_dynamo_result(f, id_=output_id_floor_area)
    total_cost = vkt.dynamo.get_dynamo_result(f, id_=output_id_total_cost)
    mki = vkt.dynamo.get_dynamo_result(f, id_=output_id_mki)
    co2 = vkt.dynamo.get_dynamo_result(f, id_=output_id_co2)

    # Add values to a DataGroup to visualize them in the app
    data_group = vkt.DataGroup(
    vkt.DataItem(label="Floor area", value=round(float(floor_area), 2), suffix="m²"),
    vkt.DataItem(label="Total cost", value=round(float(total_cost), 2), suffix="€"),
    vkt.DataItem(label="MKI", value=round(float(mki), 2)),
    vkt.DataItem(label="CO₂ emission", value=round(float(co2), 2), suffix="ton CO₂"),
    )
    return data_group

    @vkt.GeometryAndDataView("Building 3D", duration_guess=5, x_axis_to_right=True)
    def geometry_and_data_view(self, params, **kwargs):
    """Run an analysis using Dynamo and visualize the geometry and data"""
    # Step 1: Update model
    input_file, dynamo_file = self.update_model(params)

    # Step 2: Running analysis
    files = [('input.dyn', input_file)]

    analysis = vkt.dynamo.DynamoAnalysis(files=files, output_filenames=["output.xml", "geometry.json"])
    analysis.execute(timeout=60)

    # Step 3: Processing geometry
    geometry_file = analysis.get_output_file('geometry.json', as_file=True)
    glb_file = vkt.dynamo.convert_geometry_to_glb(geometry_file)

    # Step 4: Process numerical output
    output_file = analysis.get_output_file('output.xml', as_file=True)
    data_group = self.convert_dynamo_file_to_data_items(dynamo_file, output_file)

    return vkt.GeometryAndDataResult(geometry=glb_file, data=data_group)

    To infinity and beyond

    Nice! You are now able to create an app that can integrate with Dynamo (Sandbox) using a Dynamo Worker!

    Of course, the journey doesn't end here. Check out some of our other tutorials or try to replace Dynamo script from this tutorial with one of your own!