Description
I am proposing a new launch_ros.Substitution that takes multiple yaml paths as a parameter, and returns one unified (temporary) yaml path as the result.
Motivation
YAML files are getting mighty unwieldy these days. Check out all 600 lines of this navigation config. It is easy enough to load multiple parameter files into a Node when you are writing the launch file directly, i.e.
Node(...
parameters=[yaml_path1, yaml_path2],
)
However, there are scenarios where you might want to load those yaml files into one single LaunchConfiguration. Consider nav2_bringup / navigation_launch.py. The above navigation configuration is declared as a launch argument
declare_params_file_cmd = DeclareLaunchArgument(
'params_file',
default_value=os.path.join(bringup_dir, 'params', 'nav2_params.yaml'),
description='Full path to the ROS2 parameters file to use for all launched nodes',
)
and then passed to multiple nodes (after some processing).
I want to be able to specify multiple yaml files to replace the single nav2_params.yaml WITHOUT having to rewrite the entire launch file.
Design / Implementation Considerations
One option is to just copy/paste the launch file and rewrite the parameters as needed, which is valid, and many people have done just that.
I have implemented a proof-of-concept approach (that I call MultiYaml) wherein multiple yaml files are passed in, and a new temporary yaml file is returned as the result of the substitution. The contents of the new temporary yaml file are merged into one large dictionary.
nav2_common / RewrittenYaml and nav2_common / ReplaceString already read a yaml file and write a temporary yaml file as a result. #258 exists to port that behavior to launch_ros.
I see three potential paths forward.
- Implement
MultiYaml, RewrittenYaml and ReplaceString as independent substitutions.
- Implement all three with some common inherited infrastructure for "Substitutions that write to a temporary yaml file"
- Implement a
CombinedConfiguration substitution that writes to a temporary yaml but operates in a more extensible way, i.e.
- Starts with a single base yaml path
- Takes a list of "Yaml Operations" in as a parameter that could include
- Combine with another yaml (for
MultiYaml)
- Perform a string replacement (for
ReplaceString natch)
- Perform param, key, value rewrites (for
RewrittenYaml)
- Change the root key (for
RewrittenYaml)
- Returns the resulting dictionary as a temporary yaml path
I am open to discussion of the proper level of over-engineering for this problem. For reference, here is my initial unpolished MultiYaml implementation.
from launch import Substitution, LaunchContext, SomeSubstitutionsType
from launch.utilities import normalize_to_list_of_substitutions, perform_substitutions
from collections.abc import Mapping
import tempfile
from typing import List, Text
import yaml
def recursive_update(d, u):
for k, v in u.items():
if isinstance(v, Mapping):
d[k] = recursive_update(d.get(k, {}), v)
else:
d[k] = v
return d
class MultiYaml(Substitution):
def __init__(
self,
source_files: List[SomeSubstitutionsType],
) -> None:
super().__init__()
self.__source_files = source_files
def perform(self, context: LaunchContext) -> Text:
output_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
d = {}
for raw_source_file in self.__source_files:
source_list = normalize_to_list_of_substitutions(raw_source_file)
source_file = perform_substitutions(context, source_list)
new_d = yaml.safe_load(open(source_file))
recursive_update(d, new_d)
yaml.dump(d, output_file)
return output_file.name
and its usage
navigation_launch = IncludeLaunchDescription(
PathJoinSubstitution([nav_config_path, 'launch', 'nav_core.launch.py']),
launch_arguments={
'params_file': MultiYaml([
PathJoinSubstitution([nav_config_path, 'config', 'nav2_params_core.yaml']),
PathJoinSubstitution([nav_config_path, 'config', 'nav2_params_mppi.yaml']),
]),
}.items(),
)
Other approach: I briefly considered trying to return an array of yaml files from a substitution but that would have required more work from the evaluate_parameters code to theoretically get to work.
Additional Information
I'd wager @SteveMacenski has feelings on this, and maybe @adamsj-ros @leander-dsouza
Description
I am proposing a new
launch_ros.Substitutionthat takes multiple yaml paths as a parameter, and returns one unified (temporary) yaml path as the result.Motivation
YAML files are getting mighty unwieldy these days. Check out all 600 lines of this navigation config. It is easy enough to load multiple parameter files into a Node when you are writing the launch file directly, i.e.
However, there are scenarios where you might want to load those yaml files into one single
LaunchConfiguration. Considernav2_bringup / navigation_launch.py. The above navigation configuration is declared as a launch argumentand then passed to multiple nodes (after some processing).
I want to be able to specify multiple yaml files to replace the single
nav2_params.yamlWITHOUT having to rewrite the entire launch file.Design / Implementation Considerations
One option is to just copy/paste the launch file and rewrite the parameters as needed, which is valid, and many people have done just that.
I have implemented a proof-of-concept approach (that I call
MultiYaml) wherein multiple yaml files are passed in, and a new temporary yaml file is returned as the result of the substitution. The contents of the new temporary yaml file are merged into one large dictionary.nav2_common / RewrittenYamlandnav2_common / ReplaceStringalready read a yaml file and write a temporary yaml file as a result. #258 exists to port that behavior tolaunch_ros.I see three potential paths forward.
MultiYaml,RewrittenYamlandReplaceStringas independent substitutions.CombinedConfigurationsubstitution that writes to a temporary yaml but operates in a more extensible way, i.e.MultiYaml)ReplaceStringnatch)RewrittenYaml)RewrittenYaml)I am open to discussion of the proper level of over-engineering for this problem. For reference, here is my initial unpolished
MultiYamlimplementation.and its usage
Other approach: I briefly considered trying to return an array of yaml files from a substitution but that would have required more work from the
evaluate_parameterscode to theoretically get to work.Additional Information
I'd wager @SteveMacenski has feelings on this, and maybe @adamsj-ros @leander-dsouza