Skip to content

Feature: Combined Parameter Substitution #517

@DLu

Description

@DLu

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions