ARTIST Tutorial: Generating a Scenario HDF5 File
Note
You can find the corresponding Python scripts for this tutorial here:
In this tutorial, we will walk through the process of generating simple ARTIST HDF5 scenario files. Before
starting, please make sure you have read the scenario documentation describing the structure of an
ARTIST scenario file.
As outlined in the scenario overview, an ARTIST scenario consists of five main elements:
One power plant location,
at least one (but possibly more) target areas,
at least one (but possibly more) light sources,
at least one (but usually more) heliostats, and
a prototype used whenever a heliostat does not define individual parameters.
In this tutorial, we generate minimal example scenarios that include:
A power plant location,
one or more target areas,
a single
Sunlight source,one or more heliostats, and
a corresponding prototype defining the heliostat properties.
Defining the Scenario Path
Before defining the scenario content, we must specify where the generated HDF5 file will be stored. This is done by
setting the scenario_path variable. If the specified directory does not exist, the scenario generation will fail.
# Specify the path to your scenario file.
scenario_path = pathlib.Path("please/insert/the/path/to/the/scenario/here/name")
Choosing A Data Source
The subsequent steps depend on the data source used to construct the scenario. Data with different input formats from
different sources requires different preprocessing steps before it can be converted into an ARTIST scenario.
This tutorial shows how to convert STRAL and PAINT data to create usable scenarios for ARTIST. The solar tower ray
tracing laboratory STRAL is a ray-tracing software, and
PAINT is the first FAIR database for operational data of concentrating solar power
plants.
We will first cover the workflow for PAINT data. If you are only interested in STRAL, you can jump directly to Generating a Scenario with STRAL Data.
Generating a Scenario With PAINT Data
To generate an ARTIST scenario from PAINT data, you must provide the following files:
One
tower-measurement.jsonfileOne or more
heliostat-properties.jsonfilesOne or more
deflectometry.h5files
If you want to include multiple heliostats in your scenario, simply add one set of heliostat-specific files, i.e., properties and deflectometry, per heliostat.
# Specify the path to your tower-measurements.json file.
tower_file = pathlib.Path(
"please/insert/the/path/to/the/tower/measurements/here/tower-measurements.json"
)
# Specify the following data for each heliostat that you want to include in the scenario:
# A tuple of: (heliostat-name, heliostat-properties.json, deflectometry.h5)
heliostat_files_list = [
(
"name1",
pathlib.Path(
"please/insert/the/path/to/the/heliostat/properties/here/heliostat_properties.json"
),
pathlib.Path(
"please/insert/the/path/to/the/deflectometry/data/here/deflectometry.h5"
),
),
(
"name2",
pathlib.Path(
"please/insert/the/path/to/the/heliostat/properties/here/heliostat_properties.json"
),
pathlib.Path(
"please/insert/the/path/to/the/deflectometry/data/here/deflectometry.h5"
),
),
# ... Include as many heliostats as you want, but at least one!
]
With the required input files defined, we can now proceed to building the scenario.
Power Plant and Target Areas
The power plant location and the associated target areas (i.e., the receiver or calibration targets) are loaded
simultaneously from the tower-measurement.json file. We can extract this information using functions from the
paint_scenario_parser module. The function shown below will return
an instance of
PowerPlantConfigandan instance of
TargetAreaPlanarListConfig, containing a list of viable planar target areas.an instance of
TargetAreaCylindricalListConfig, containing a list of viable cylindrical target areas.
# Include the power plant and target area configurations.
(
power_plant_config,
target_area_list_planar_config,
target_area_list_cylindrical_config,
) = paint_scenario_parser.extract_paint_tower_measurements(
tower_measurements_path=tower_file, device=device
)
The PowerPlantConfig object provides the power plant’s geographic location via the power_plant_position
attribute. The TargetAreaPlanarListConfig contains a list of multiple TargetAreaPlanarConfig objects.
Each defines the following attributes:
target_area_keyAn identifier used to reference the target area when loading the
ARTISTscenario – in this case, a receiver.centerThe target area’s middle position. Since this is a position tensor, its final element in the 4D representation is a 1 – for more information, see our docs page on coordinates.
normalThe target area plane’s normal vector. Since this is a direction tensor, its final element in the 4D representation is a 0 – for more information, see our docs page on coordinates.
plane_eThe direction vector defining the target area plane’s east direction.
plane_uThe direction vector defining the target area plane’s up direction.
The TargetAreaCylindricalListConfig contains a list of multiple TargetAreaCylindricalConfig objects.
Each defines the following attributes:
target_area_keyAn identifier used to reference the target area when loading the
ARTISTscenario – in this case, a receiver.radiusThe cylinder radius.
heightThe cylinder height.
axisThe cylinder axis. Since this is a direction tensor, its final element in the 4D representation is a 0 – for more information, see our docs page on coordinates.
normalThe target area plane’s normal vector. Since this is a direction tensor, its final element in the 4D representation is a 0 – for more information, see our docs page on coordinates.
opening_angleThe cylinder opening angle. Cylindrical target areas can either be full cylinders or cylinder sectors. For a full cylinder the opening angle is \(2\pi\).
Light Source
The light source provides the radiation that is reflected by the heliostats. In most scenarios, this light source
represents the sun. However, in certain applications, such as calibration setups, it may be useful to model multiple
artificial light sources. Light source information is not read from external files and must be defined manually.
We define the light source by creating a LightSourceConfig object as shown below:
# Include the light source configuration.
light_source_config = LightSourceConfig(
light_source_key="sun",
light_source_type=constants.sun_key,
number_of_rays=10,
distribution_type=constants.light_source_distribution_is_normal,
mean=0.0,
covariance=4.3681e-06,
)
This configuration specifies the following light source properties:
light_source_keyUsed to identify the light source when loading the
ARTISTscenario.light_source_typeThe type of light source used – in this case, a
Sun.number_of_raysThe number of rays to be sampled from the light source for ray tracing.
distribution_typeThe type of distribution used to model the light source – in this case, a normal distribution.
meanThe mean parameter of the selected normal distribution.
covarianceThe covariance parameter of the selected normal distribution.
Although this example uses only a single light source, ARTIST scenarios are designed to support multiple sources.
Therefore, the light source configuration must be wrapped in a list and passed to a LightSourceListConfig object:
# Create a list of light source configs - in this case only one.
light_source_list = [light_source_config]
# Include the configuration for the list of light sources.
light_source_list_config = LightSourceListConfig(light_source_list=light_source_list)
Prototypes and Heliostats
Every ARTIST scenario requires both prototypes and heliostats (see our tutorial here for more
information).
The prototypes and list of heliostats can be easily extracted using the paint_scenario_parser. Before doing so, we
must define a default aim point by selecting one target area from the previously loaded list – typically the
receiver:
target_area = [
target_area
for target_area in target_area_list_config.target_area_list
if target_area.target_area_key == constants.target_area_receiver
]
Before loading the heliostats, we need to do some configuration. ARTIST internally models all heliostat surfaces
using NURBS, which are learned when loading the data. Therefore, we need to specify parameters
controlling the fitting process, such as:
the number of NURBS control points,
the fitting method
the tolerance and number of epochs to train, and
an optimizer and learning rate scheduler for the training process.
This is shown below:
number_of_nurbs_control_points = torch.tensor([20, 20], device=device)
nurbs_fit_method = constants.fit_nurbs_from_normals
nurbs_deflectometry_step_size = 100
nurbs_fit_tolerance = 1e-10
nurbs_fit_max_epoch = 400
# Leave the optimizable parameters empty, they will automatically be added for the surface fit.
nurbs_fit_optimizer = torch.optim.Adam([torch.empty(1, requires_grad=True)], lr=1e-3)
nurbs_fit_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
nurbs_fit_optimizer,
mode="min",
factor=0.2,
patience=50,
threshold=1e-7,
threshold_mode="abs",
)
With the configuration defined, a single function call :
loads the heliostat list configuration,
learn the NURBS surfaces, and
generates the prototype configuration.
heliostat_list_config, prototype_config = (
paint_scenario_parser.extract_paint_heliostats_fitted_surface(
paths=heliostat_files_list,
power_plant_position=power_plant_config.power_plant_position,
number_of_nurbs_control_points=number_of_nurbs_control_points,
deflectometry_step_size=nurbs_deflectometry_step_size,
nurbs_fit_method=nurbs_fit_method,
nurbs_fit_tolerance=nurbs_fit_tolerance,
nurbs_fit_max_epoch=nurbs_fit_max_epoch,
nurbs_fit_optimizer=nurbs_fit_optimizer,
nurbs_fit_scheduler=nurbs_fit_scheduler,
device=device,
)
)
heliostat_list_configA list of
HeliostatConfigobjects, where each object contains:The
nameused to identify the heliostatThe numerical
idof the heliostatThe heliostat
positionThe
surfaceconfiguration of the heliostat (seeartist.scenario.configuration_classes.SurfaceConfig).The
kinematicsconfiguration of the heliostat (seeartist.scenario.configuration_classes.KinematicsConfig).A list of configurations for the
actuatorsrequired by the heliostat (seeartist.scenario.configuration_classes.ActuatorConfig).
prototype_configA
PrototypeConfigobject defines fallback configurations used when heliostats do not provide individual parameters. It contains:The
surface_prototype(seeartist.scenario.configuration_classes.SurfacePrototypeConfig)The
kinematics_prototype(seeartist.scenario.configuration_classes.KinematicsPrototypeConfig)A list of
actuators_prototype(seeartist.scenario.configuration_classes.ActuatorPrototypeConfig)
Different Surface Options
ARTIST does not require deflectometry data to generate a scenario. Instead, scenarios can also be created with
ideal heliostat surfaces. The true surface can later be learned via ray tracing (see
the NURBS surface reconstructor). If no information about the true surface
is available, the ideal surface can simply be used as is. To generate heliostats with ideal surfaces, call:
heliostat_list_config, prototype_config = (
paint_scenario_parser.extract_paint_heliostats_ideal_surface(
paths=heliostat_files_list,
power_plant_position=power_plant_config.power_plant_position,
device=device,
)
)
In this case, no optimizer or NURBS fitting parameters need to be defined.
It is also possible to generate mixed-surface scenarios containing both fitted and ideal surfaces using the function
extract_paint_heliostats_mixed_surface(). In this case, the surface type is determined automatically from the
provided input mapping. If you provide a path to a deflectometry file, the surface will be fitted; if not, an ideal
surface will be generated.
For example, for the following mapping:
heliostat_files_list = [
(
"heliostat_1",
pathlib.Path(
"please/insert/the/path/to/the/heliostat/properties/here/heliostat_properties.json"
),
pathlib.Path(
"please/insert/the/path/to/the/deflectometry/data/here/deflectometry.h5"
),
),
(
"heliostat_2",
pathlib.Path(
"please/insert/the/path/to/the/heliostat/properties/here/heliostat_properties.json"
),
),
]
Calling the function:
heliostat_list_config, prototype_config = (
paint_scenario_parser.extract_paint_heliostats_mixed_surface(
paths=heliostat_files_list,
power_plant_position=power_plant_config.power_plant_position,
number_of_nurbs_control_points=number_of_nurbs_control_points,
deflectometry_step_size=nurbs_deflectometry_step_size,
nurbs_fit_method=nurbs_fit_method,
nurbs_fit_tolerance=nurbs_fit_tolerance,
nurbs_fit_max_epoch=nurbs_fit_max_epoch,
nurbs_fit_optimizer=nurbs_fit_optimizer,
nurbs_fit_scheduler=nurbs_fit_scheduler,
device=device,
)
)
will generate a scenario in which heliostat_1 has a fitted surface and heliostat_2 has an ideal surface.
NOTE: In mixed-surface scenarios, the prototype surface will always be an ideal surface.
Creating the HDF5 File
At this point, we have all the information needed to generate the HDF5 file and complete the scenario. We can create the
scenario by running the main function shown below:
if __name__ == "__main__":
# Generate the scenario given the defined parameters.
scenario_generator = H5ScenarioGenerator(
file_path=scenario_path,
power_plant_config=power_plant_config,
target_area_list_planar_config=target_area_list_planar_config,
target_area_list_cylindrical_config=target_area_list_cylindrical_config,
light_source_list_config=light_source_list_config,
prototype_config=prototype_config,
heliostat_list_config=heliostats_list_config,
)
scenario_generator.generate_scenario()
Based on the previously defined scenario_path and our configurations for the receiver(s), light source(s),
prototype, and heliostat(s), an H5ScenarioGenerator object is instantiated. This object is then used to generate the
actual HDF5 file.
After running this script, a new HDF5 file will appear at the location you specified at the very beginning – and that is
all it takes to generate a scenario in ARTIST!
Generating a Scenario with STRAL Data
To generate a scenario from STRAL, you only need a single .binp file.
# Specify the path to your stral_data.binp file.
stral_file_path = pathlib.Path(
"please/insert/the/path/to/the/stral/data/here/stral_data.binp"
)
Many of the steps for generating a scenario are very similar to those for PAINT data, but a few differences specific to STRAL need to be taken into account.
Power Plant
STRAL data does not include the power plant location, so you must enter the coordinates manually:
# Include the power plant configuration.
power_plant_config = PowerPlantConfig(
power_plant_position=torch.tensor([0.0, 0.0, 0.0], device=device)
)
More details on the PowerPlantConfig class are provided above (see Power Plant and Target Areas).
Target Areas
When using STRAL data, we also need to manually define the TargetAreaPlanarListConfig
and the TargetAreaCylindricalListConfig:
# STRAL
# Include a single planar tower target area.
target_area_list_planar_config = TargetAreaPlanarConfig(
target_area_key="planar",
center=torch.tensor([0.0, -50.0, 0.0, 1.0], device=device),
normal_vector=torch.tensor([0.0, 1.0, 0.0, 0.0], device=device),
plane_e=8.629666667,
plane_u=7.0,
)
target_area_planar_list_config = TargetAreaPlanarListConfig(
[target_area_list_planar_config]
)
# Include a single cylindrical tower target area.
target_area_list_cylindrical_config = TargetAreaCylindricalConfig(
target_area_key="cylinder",
radius=4.14,
center=torch.tensor([0.0, 0.0, 0.0, 1.0], device=device),
height=6.0,
axis=torch.tensor([0.0, 0.0, 1.0, 0.0], device=device),
normal=torch.tensor([0.0, 1.0, 0.0, 0.0], device=device),
opening_angle=60,
)
target_area_cylindrical_list_config = TargetAreaCylindricalListConfig(
[target_area_list_cylindrical_config]
)
More details on the TargetAreaPlanarConfig and TargetAreaCylindricalListConfig classes are provided above (see Power Plant and Target Areas).
Light Source
Generating a light source using STRAL data is identical to PAINT data, please see Light Source.
Prototypes
With STRAL data, prototypes must be defined manually. A prototype always consists of a surface prototype, a kinematics prototype, and an actuator prototype.
We start with the surface prototype. First, we need to extract information regarding the facet translation vectors, the canting, and the surface points and normals from STRAL:
(
facet_translation_vectors,
canting,
surface_points_with_facets_list,
surface_normals_with_facets_list,
) = stral_scenario_parser.extract_stral_deflectometry_data(
stral_file_path=stral_file_path, device=device
)
Before we can generate a NURBS surface based on the surface normals and points from STRAL, we need to define the surface generator and the optimizer and scheduler to fit the surface:
surface_generator = SurfaceGenerator(device=device)
# Leave the optimizable parameters empty, they will automatically be added for the surface fit.
nurbs_fit_optimizer = torch.optim.Adam([torch.empty(1, requires_grad=True)], lr=1e-3)
nurbs_fit_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
nurbs_fit_optimizer,
mode="min",
factor=0.2,
patience=50,
threshold=1e-7,
threshold_mode="abs",
)
Finally, we can use the configuration to generate a fitted surface:
surface_config = surface_generator.generate_fitted_surface_config(
heliostat_name="heliostat_1",
facet_translation_vectors=facet_translation_vectors,
canting=canting,
surface_points_with_facets_list=surface_points_with_facets_list,
surface_normals_with_facets_list=surface_normals_with_facets_list,
optimizer=nurbs_fit_optimizer,
scheduler=nurbs_fit_scheduler,
device=device,
)
Alternatively, we can generate an ideal surface that is not fitted based on deflectometry data. To generate such a surface, we do not need to define an optimizer or scheduler but can simply call:
surface_config = surface_generator.generate_ideal_surface_config(
facet_translation_vectors=facet_translation_vectors,
canting=canting,
device=device,
)
To create the surface configuration, we define a surface configuration prototype based on the list of facets contained in the SurfaceConfig from above:
surface_prototype_config = SurfacePrototypeConfig(facet_list=surface_config.facet_list)
Next, we consider the kinematics prototype. The kinematics in ARTIST assume that all heliostats initially
point in the south direction; however, depending on the CSP considered, the heliostats may be orientated differently. In
our scenario, we orient the heliostats upwards, i.e., they point directly at the sky. A further element of a kinematics
configuration is KinematicsDeviations which are small disturbance parameters representing offsets caused by the
two-joint kinematics modeled in ARTIST. In this tutorial, we ignore these deviations. Therefore, we can
create the kinematics prototype by generating a KinematicsPrototypeConfig object as:
kinematics_prototype_config = KinematicsPrototypeConfig(
type=constants.rigid_body_key,
initial_orientation=torch.tensor([0.0, 0.0, 1.0, 0.0], device=device),
)
This object defines:
typeThe type used in the scenario – in this case, rigid-body kinematics.
initial_orientationThe initial heliostat orientation which is the direction we defined above.
KinematicsDeviationsThe offsets (ignored here).
With the kinematics prototype defined, the final prototype required is the actuator prototype. For the rigid-body kinematics, we need exactly two actuators. Since STRAL data does not include motor position limits, we have to define them manually. Here, we use the minimum and maximum motor positions for the Jülich plant:
min_max_motor_positions_actuator_1 = [0.0, 60000.0]
min_max_motor_positions_actuator_2 = [0.0, 80000.0]
We can now define the actuators using ActuatorConfig objects as shown below:
# Include two ideal actuators.
actuator1_prototype = ActuatorConfig(
key="actuator_1",
type=constants.ideal_actuator_key,
clockwise_axis_movement=False,
min_max_motor_positions=min_max_motor_positions_actuator_1,
)
actuator2_prototype = ActuatorConfig(
key="actuator_2",
type=constants.ideal_actuator_key,
clockwise_axis_movement=True,
min_max_motor_positions=min_max_motor_positions_actuator_2,
)
These configurations define:
keyThe key used when loading the actuator from an
ARTISTscenario.typeThe actuator type – in this case, an ideal actuator for both actuators.
clockwise_axis_movementDefines whether the actuator operates in a clockwise or counter-clockwise direction.
For different types of actuators, e.g., a linear actuator, we would also have to define specific actuator parameters.
However, we will stick to a simple configuration for this tutorial. To complete the actuator prototype, we wrap both
actuators in a list and generate an ActuatorPrototypeConfig object:
# Create a list of actuators.
actuator_prototype_list = [actuator1_prototype, actuator2_prototype]
# Include the actuator prototype config.
actuator_prototype_config = ActuatorPrototypeConfig(
actuator_list=actuator_prototype_list
)
With all prototypes defined, we can combine them into the final PrototypeConfig object as shown below:
# Include the final prototype config.
prototype_config = PrototypeConfig(
surface_prototype=surface_prototype_config,
kinematics_prototype=kinematics_prototype_config,
actuator_prototype=actuator_prototype_config,
)
Heliostat from STRAL
Having defined the prototype, we can now define our heliostat by creating a HeliostatConfig object:
# Include the configuration for a heliostat.
heliostat1 = HeliostatConfig(
name="heliostat_1",
id=1,
position=torch.tensor([0.0, 5.0, 0.0, 1.0], device=device),
)
This heliostat configuration specifies:
nameA name used to identify the heliostat when loading the
ARTISTscenario.idA unique identifier that can be used to quickly identify the heliostat within the scenario.
positionThe heliostat’s position in the field. Note the one in the fourth dimension according to the previously discussed coordinate convention.
Since no individual surface, kinematics, or actuator parameters are provided, this heliostat will automatically use the
prototype configurations. Since ARTIST is designed to support multiple heliostats, we need to wrap our heliostat
configuration in a list and create a HeliostatListConfig object:
heliostat_list = [heliostat1]
# Create the configuration for all heliostats.
heliostats_list_config = HeliostatListConfig(heliostat_list=heliostat_list)
If individual measurements are available, it is also possible to define custom surface, kinematics, and actuator configurations for each heliostat.
Creating the HDF5 File
Creating the HDF5 based on STRAL data follows the same procedure as with PAINT data (see Creating the HDF5 File).
Warning
When generating a scenario, the logger reports the version of the scenario generator being used. Scenario files
generated with a different version may be incompatible with the current ARTIST version.