Tutorial - Integrate Dynamo
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:
- Setup VIKTOR app
- Create a method to update Dynamo file
- Create a mocked GeometryView
- Create a data processing method
- Create a Data View with the mocked output files
- Create a geometry processing method
- Add the logic to execute the Dynamo script with a worker
- Setting up worker
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:
You can find the complete code below
Pre-requisites
- 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:
- 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'.
- Select 'Editor' as app type and click 'Next'.
- 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 platform...
INFO : Connection is established: https://cloud.viktor.ai <---- here you can see your app
INFO : The connection can be closed using Ctrl+C
INFO : App is ready
- 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:
- Retrieve the input files for the analysis, in this case the
dynamo_model_sample_app.dyn
file, and create aDynamoFile
object instantiated from thedynamo_model_sample_app.dyn
file. You can download the sample dynamo model here. - With the
update
method, the value of input nodes can be updated. - When all inputs have been updated as desired, the
generate
method can be used to generate an updatedFile
object.
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:
- Update the Dynamo file with the
update_model
method(see section 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.
- With the helper function
convert_geometry_to_glb
, you can convert the json file to a GLB file, which can directly be visualized in aGeometryView
. - 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:
- Get the
node_id
, which corresponds to the same node id as the input file. - Collect the numerical results.
- Create a
DataGroup
from the collected numerical results using theDataGroup
andDataItem
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:
- Update the Dynamo file with the
update_model
method (see section 1) - For now we are using a mocked file, instead of running the analysis in Dynamo. The mocked file can be downloaded here.
- With the static method
convert_dynamo_file_to_data_items
, you can convert the .xml file to aDataGroup
, which can be directly visualized in aDataView
. - 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),
]
generic_analysis = vkt.external.GenericAnalysis(files=files, executable_key="dynamo",
output_filenames=["output.xml", "geometry.json"])
generic_analysis.execute(timeout=60)
# Step 3: Processing geometry
geometry_file = generic_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 = generic_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:
- Update the Dynamo file with the
update_model
method (see section 2) - We use the
GenericAnalysis
to run the Dynamo script. Theexecutable_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. - Processing the geometry, the .json file can be obtained with the
get_ouput_file
method. With the helper functionconvert_geometry_to_glb
, you can convert it to a GLB type file, which can directly be visualized in aGeometryAndDataView
. - Processing geometry, the .xml file can be obtained with the
get_ouput_file
. method. With the static methodconvert_dynamo_file_to_data_items
, you can convert the .xml file to aDatagroup
, which can be directly visualized inGeometryAndDataView
.
8. Setting up a worker
A worker is a program that connects VIKTOR with third-party software to execute tasks and retrieve results through the platform. The worker communicates with the VIKTOR cloud via an encrypted connection, eliminating the need to open public ports on the network. For the Dynamo integration, the generic worker must be installed.
Installation
Follow these steps to install the worker:
- Development
- Published App
-
Navigate to the "My Integrations" tab in your personal settings
-
Click "Add integration"
-
Follow the steps provided in the modal
3.1. Select Other
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
You need to be an environment administrator in order to install a worker for a published app.
-
Navigate to the "Integrations" tab in the Administrator panel
-
Click "Add integration"
-
Follow the steps provided in the modal
3.1. Select Other
3.2. Select the workspace(s) the integration should be available to
3.3. Download the worker .msi (Microsoft Installer) and run it on the machine of choice
3.4. Copy the generated connection key and paste it when the installer asks for it
The generated connection key should be copied immediately as VIKTOR will not preserve this data for security reasons.
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 (see last section). 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 sucessfully 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),
]
generic_analysis = vkt.external.GenericAnalysis(files=files, executable_key="dynamo",
output_filenames=["output.xml", "geometry.json"])
generic_analysis.execute(timeout=60)
# Step 3: Processing geometry
geometry_file = generic_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 = generic_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 Generic 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!