From 78a8584adebcee17d34752dd4aa7648c50eb5646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Thor=C3=A9n?= Date: Mon, 4 May 2026 16:28:09 +0200 Subject: [PATCH 1/2] Add possibility to load multiple scenarios and change scenario without restarting ATOS Co-authored-by: Copilot --- .../EsminiAdapter/src/esminiadapter.cpp | 4 +- .../ObjectControl/inc/statemachine.hpp | 5 +- .../ObjectControl/src/objectcontrol.cpp | 47 +++++++++-- .../openscenariogateway.py | 82 ++++++++++++++----- .../tests/test_openscenariogateway.py | 4 +- .../atos_gui/controlpanel/controlpanel.py | 77 ++++++++++++++++- conf/conf/params.yaml | 2 +- 7 files changed, 184 insertions(+), 37 deletions(-) diff --git a/atos/modules/EsminiAdapter/src/esminiadapter.cpp b/atos/modules/EsminiAdapter/src/esminiadapter.cpp index cfa37f09d..862965e1d 100644 --- a/atos/modules/EsminiAdapter/src/esminiadapter.cpp +++ b/atos/modules/EsminiAdapter/src/esminiadapter.cpp @@ -50,7 +50,7 @@ EsminiAdapter::EsminiAdapter() : oscFilePathClient_cb_group_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant); objectIdsClient_cb_group_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant); oscFilePathClient_ = create_client( - ServiceNames::getOpenScenarioFilePath, rmw_qos_profile_services_default, oscFilePathClient_cb_group_); + ServiceNames::getOpenScenarioFilePath, rmw_qos_profile_services_default, oscFilePathClient_cb_group_); objectIdsClient_ = create_client( ServiceNames::getObjectIds, rmw_qos_profile_services_default, objectIdsClient_cb_group_); declare_parameter("timestep", 0.1); @@ -573,7 +573,7 @@ void EsminiAdapter::runEsminiSimulation() { void EsminiAdapter::onRequestObjectTrajectory(const std::shared_ptr req, std::shared_ptr res) { - res->id; + res->id = req->id; me->fetchOSCFilePath(); if (me->runSimulation || me->atosObjectIdToTraj.find(req->id) == me->atosObjectIdToTraj.end()) { me->runEsminiSimulation(); diff --git a/atos/modules/ObjectControl/inc/statemachine.hpp b/atos/modules/ObjectControl/inc/statemachine.hpp index ddce6c8d7..162e02ba3 100644 --- a/atos/modules/ObjectControl/inc/statemachine.hpp +++ b/atos/modules/ObjectControl/inc/statemachine.hpp @@ -176,8 +176,9 @@ class StateMachine { Connecting + sml::event = Ready, Ready + sml::on_entry / ready_on_entry(), - Ready + sml::event = Aborting, - Ready + sml::event = Armed, + Ready + sml::event = Aborting, + Ready + sml::event = Armed, + Ready + sml::event / ready_reload(), Ready + sml::event = Idle, Ready + sml::event / disconnected_from_object() = Connecting, Ready + sml::event = RemoteControl, diff --git a/atos/modules/ObjectControl/src/objectcontrol.cpp b/atos/modules/ObjectControl/src/objectcontrol.cpp index 2bf6fea29..afb005399 100644 --- a/atos/modules/ObjectControl/src/objectcontrol.cpp +++ b/atos/modules/ObjectControl/src/objectcontrol.cpp @@ -679,16 +679,45 @@ void ObjectControl::resetTestObjects() { * @param isResetting state-flag to indicate if the reset procedure is currently running */ void ObjectControl::reloadScenarioTrajectories() { - // Check if we already have loaded the scenario trajectories - if (!this->isResetting) { - RCLCPP_INFO(get_logger(), "Scenario trajectories already loaded"); - return; - } - this->isResetting = false; RCLCPP_INFO(get_logger(), "Reloading scenario trajectories"); - // Request the scenario trajectory for each object for (auto& id : getVehicleIDs()) { - this->setObjectTrajectory(id); + auto trajRequest = std::make_shared(); + trajRequest->id = id; + trajectoryClient->async_send_request( + trajRequest, + [this, id](const rclcpp::Client::SharedFuture future) { + auto response = future.get(); + if (!response->success) { + RCLCPP_ERROR(get_logger(), "Get trajectory service call failed for object ID %u", id); + return; + } + ATOS::Trajectory traj(get_logger()); + traj.initializeFromCartesianTrajectory(response->trajectory); + objects.at(id)->setTrajectory(traj); + + // Re-fetch origin (may reference a different ODR file after a scenario switch) + auto originRequest = std::make_shared(); + originClient->async_send_request( + originRequest, + [this, id, traj](const rclcpp::Client::SharedFuture originFuture) { + auto originResponse = originFuture.get(); + objects.at(id)->setOrigin({originResponse->origin.position.latitude, + originResponse->origin.position.longitude, + originResponse->origin.position.altitude, + true, + true, + true}); + RCLCPP_INFO(get_logger(), + "Updated origin for object ID %u: (%.6f, %.6f, %.3f)", + id, + originResponse->origin.position.latitude, + originResponse->origin.position.longitude, + originResponse->origin.position.altitude); + objects.at(id)->sendTrajectory(); + this->republishTrajectoryPaths(id); + RCLCPP_INFO(get_logger(), "Reloaded trajectory for object ID %u with %lu points", id, traj.size()); + }); + }); } } @@ -719,7 +748,7 @@ void ObjectControl::republishTrajectoryPaths(uint32_t id) { void ObjectControl::trajectoryCallback( const rclcpp::Client::SharedFuture future) { this->trajResponse = future.get(); - auto id = returnTrajResponse->id; + auto id = trajResponse->id; // Check if the return trajectory service call was successful if (!trajResponse->success) { RCLCPP_ERROR(get_logger(), "Get trajectory service call failed for object %u", id); diff --git a/atos/modules/OpenScenarioGateway/openscenariogateway.py b/atos/modules/OpenScenarioGateway/openscenariogateway.py index e4851e430..783188293 100755 --- a/atos/modules/OpenScenarioGateway/openscenariogateway.py +++ b/atos/modules/OpenScenarioGateway/openscenariogateway.py @@ -11,7 +11,7 @@ from rcl_interfaces.msg import SetParametersResult from rclpy.node import Node from scenariogeneration import xosc -from std_msgs.msg import Empty +from std_msgs.msg import Empty, String import atos_interfaces.msg import atos_interfaces.srv @@ -25,6 +25,12 @@ RUNNING = 2 +class ScenarioData: + def __init__(self, start_actions_to_obj_name: dict, custom_command_map: dict): + self.start_actions_to_obj_name = start_actions_to_obj_name + self.custom_command_map = custom_command_map + + class ScenarioObject: def __init__(self, name: str, catalog_ref: xosc.CatalogReference): self.name = name @@ -40,14 +46,16 @@ def __init__(self): # Class variables self.active_objects = {} self.vehicle_catalog = None - self.start_actions_to_obj_name = {} - self.custom_command_map = {} + self.scenarios: dict[str, ScenarioData] = {} + self._active_scenario_file: str = "" self.scenario_file_md5hash = None # ROS parameters self.declare_parameter(ROOT_FOLDER_PATH_PARAMETER, DEFAULT_FOLDER_PATH) - self.declare_parameter(SCENARIO_FILE_PARAMETER, "") + self.declare_parameter( + SCENARIO_FILE_PARAMETER, rclpy.Parameter.Type.STRING_ARRAY + ) self.declare_parameter( ACTIVE_OBJECT_NAME_PARAMETER, rclpy.Parameter.Type.STRING_ARRAY ) @@ -57,6 +65,9 @@ def __init__(self): # ROS subscriptions/publishers self.init_ = self.create_subscription(Empty, "init", self.init_callback, 10) self.arm_ = self.create_subscription(Empty, "arm", self.arm_callback, 10) + self.active_scenario_sub_ = self.create_subscription( + String, "active_scenario", self.active_scenario_callback, 10 + ) self.story_board_element_sub_ = self.create_subscription( atos_interfaces.msg.StoryBoardElementStateChange, @@ -90,7 +101,13 @@ def __init__(self): ) def init_callback(self, msg): - self.update_scenario(self.get_parameter(SCENARIO_FILE_PARAMETER).value) + self.scenarios = {} + for file_name in self.get_parameter(SCENARIO_FILE_PARAMETER).value: + self.update_scenario(file_name) + # Default to the first scenario after init + files = self.get_parameter(SCENARIO_FILE_PARAMETER).value + if not self._active_scenario_file and files: + self._active_scenario_file = files[0] self.update_active_scenario_objects( self.get_parameter(ACTIVE_OBJECT_NAME_PARAMETER).value ) @@ -101,13 +118,16 @@ def arm_callback(self, msg): self.active_objects[id].started = False def story_board_element_state_change_callback(self, story_board_element): + active = self.active_scenario + if active is None: + return if ( - story_board_element.name in self.start_actions_to_obj_name.keys() + story_board_element.name in active.start_actions_to_obj_name.keys() and story_board_element.state == RUNNING ): self.handle_start_actions(story_board_element) elif ( - story_board_element.full_path in self.custom_command_map + story_board_element.full_path in active.custom_command_map and story_board_element.state == RUNNING ): self.handle_custom_command_action(story_board_element) @@ -115,7 +135,7 @@ def story_board_element_state_change_callback(self, story_board_element): def handle_start_actions(self, story_board_element): # Iterate through active objects to send the start command for the target objects for object_id, object in self.active_objects.items(): - target_object_name = self.start_actions_to_obj_name[ + target_object_name = self.active_scenario.start_actions_to_obj_name[ story_board_element.name ] if object.name in target_object_name and not object.started: @@ -140,7 +160,9 @@ def handle_custom_command_action(self, story_board_element): story_board_element.full_path ) ) - custom_command = self.custom_command_map[story_board_element.full_path] + custom_command = self.active_scenario.custom_command_map[ + story_board_element.full_path + ] custom_command_msg = atos_interfaces.msg.CustomCommandAction() custom_command_msg.type = custom_command.type @@ -148,10 +170,21 @@ def handle_custom_command_action(self, story_board_element): self.custom_command_action.publish(custom_command_msg) + def active_scenario_callback(self, msg: String): + if msg.data in self.scenarios: + self._active_scenario_file = msg.data + self.get_logger().info(f"Active scenario set to: {msg.data}") + else: + self.get_logger().warn( + f"Received unknown scenario '{msg.data}'. Call init first." + ) + def parameter_callback(self, params): for param in params: if param.name == SCENARIO_FILE_PARAMETER and param.value: - self.update_scenario(file_name=param.value) + self.scenarios = {} + for file_name in param.value: + self.update_scenario(file_name=file_name) elif param.name == ACTIVE_OBJECT_NAME_PARAMETER: self.update_active_scenario_objects(active_objects_name=param.value) return SetParametersResult(successful=True) @@ -163,12 +196,14 @@ def update_scenario(self, file_name): self.get_logger().error("File does not exist: {}".format(scenario_file)) return self.get_logger().info("Loading scenario file: {}".format(scenario_file)) - self.start_actions_to_obj_name = StoryBoardHandler( - scenario_file - ).get_follow_trajectory_actions_to_actors_map() - self.custom_command_map = StoryBoardHandler( - self.getAbsoluteOSCPath(file_name) - ).get_custom_command_actions_map() + self.scenarios[file_name] = ScenarioData( + start_actions_to_obj_name=StoryBoardHandler( + scenario_file + ).get_follow_trajectory_actions_to_actors_map(), + custom_command_map=StoryBoardHandler( + scenario_file + ).get_custom_command_actions_map(), + ) def update_active_scenario_objects(self, active_objects_name: List[str]): if len(active_objects_name) != len(set(active_objects_name)): @@ -257,10 +292,19 @@ def srv_get_open_scenario_file_path(self, request, response): response.success = True return response + @property + def active_scenario_file_name(self) -> str: + if self._active_scenario_file and self._active_scenario_file in self.scenarios: + return self._active_scenario_file + files = self.get_parameter(SCENARIO_FILE_PARAMETER).value + return files[0] if files else "" + + @property + def active_scenario(self) -> ScenarioData: + return self.scenarios.get(self.active_scenario_file_name) + def getScenarioFilePath(self) -> str: - return self.getAbsoluteOSCPath( - self.get_parameter(SCENARIO_FILE_PARAMETER).value - ) + return self.getAbsoluteOSCPath(self.active_scenario_file_name) def getAbsoluteOSCPath(self, file_name: str) -> str: return path.join( diff --git a/atos/modules/OpenScenarioGateway/tests/test_openscenariogateway.py b/atos/modules/OpenScenarioGateway/tests/test_openscenariogateway.py index db914aeb1..61c972b4b 100644 --- a/atos/modules/OpenScenarioGateway/tests/test_openscenariogateway.py +++ b/atos/modules/OpenScenarioGateway/tests/test_openscenariogateway.py @@ -22,8 +22,8 @@ def open_scenario_gw(): ), rclpy.parameter.Parameter( "open_scenario_file", - rclpy.Parameter.Type.STRING, - "GaragePlanScenario.xosc", + rclpy.Parameter.Type.STRING_ARRAY, + ["GaragePlanScenario.xosc"], ), rclpy.parameter.Parameter( "active_object_names", rclpy.Parameter.Type.STRING_ARRAY, active_objects diff --git a/atos_gui/atos_gui/controlpanel/controlpanel.py b/atos_gui/atos_gui/controlpanel/controlpanel.py index 2fb6445ca..2159a81b6 100644 --- a/atos_gui/atos_gui/controlpanel/controlpanel.py +++ b/atos_gui/atos_gui/controlpanel/controlpanel.py @@ -5,8 +5,10 @@ import rclpy from nicegui import ui +from rcl_interfaces.msg import ParameterType +from rcl_interfaces.srv import GetParameters from rclpy.node import Node -from std_msgs.msg import Empty +from std_msgs.msg import Empty, String from atos_interfaces.srv import * @@ -53,6 +55,17 @@ def __init__(self) -> None: 0.5, self.get_object_control_state_callback ) + self.scenario_names = [] + self.selected_scenario = "" + self._scenario_select = None + self.active_scenario_pub = self.create_publisher( + String, "/atos/active_scenario", QOS + ) + self.get_scenario_names_client = self.create_client( + GetParameters, + "/atos/open_scenario_gateway/get_parameters", + ) + self.OBC_state = {"state": "UNDEFINED"} self.lost_connection = True @@ -68,7 +81,12 @@ def render_page(): ).props("size=large ") with ui.row(): ui.button( - "Init", on_click=lambda: self.initPub.publish(Empty()), color="blue" + "Init", + on_click=lambda: [ + self.initPub.publish(Empty()), + self.fetch_scenario_names(), + ], + color="blue", ) ui.button( "Connect", @@ -98,6 +116,25 @@ def render_page(): on_click=lambda: self.allClearPub.publish(Empty()), color="grey", ) + with ui.row().classes("items-center"): + self._scenario_select = ( + ui.select( + options=self.scenario_names, + label="Active scenario", + on_change=lambda e: [ + self.set_active_scenario(e.value), + self.connectPub.publish(Empty()), + ], + ) + .bind_value(self, "selected_scenario") + .bind_enabled_from( + self.OBC_state, "state", backward=lambda s: s == "CONNECTED" + ) + .props("outlined dense") + ) + ui.label( + "Changing scenario can ony be done in CONNECTED state" + ).classes("text-sm text-grey-7 italic") with ui.row(): ui.label().bind_text_from( self.OBC_state, "state", backward=lambda n: f"State: {n}" @@ -114,6 +151,42 @@ def render_page(): color="grey", ) + def fetch_scenario_names(self) -> None: + if not self.get_scenario_names_client.wait_for_service(timeout_sec=1.0): + self.get_logger().warn("open_scenario_gateway get_parameters not available") + return + req = GetParameters.Request() + req.names = ["open_scenario_file"] + self.get_scenario_names_client.call_async(req).add_done_callback( + self.on_scenario_names_fetched + ) + + def on_scenario_names_fetched(self, future) -> None: + try: + response = future.result() + except Exception as e: + self.get_logger().error(f"Failed to fetch scenario names: {e}") + return + if ( + response.values + and response.values[0].type == ParameterType.PARAMETER_STRING_ARRAY + ): + names = list(response.values[0].string_array_value) + self.scenario_names = names + if self._scenario_select is not None: + self._scenario_select.options = names + if not self.selected_scenario and names: + self.selected_scenario = names[0] + self._scenario_select.set_value(names[0]) + self._scenario_select.update() + + def set_active_scenario(self, scenario_name: str) -> None: + if not isinstance(scenario_name, str) or not scenario_name: + return + msg = String() + msg.data = scenario_name + self.active_scenario_pub.publish(msg) + def get_object_control_state_callback(self): # Call the service while not self.get_object_control_state_client.wait_for_service( diff --git a/conf/conf/params.yaml b/conf/conf/params.yaml index 8f3874f85..c65eb35f2 100644 --- a/conf/conf/params.yaml +++ b/conf/conf/params.yaml @@ -7,7 +7,7 @@ atos: timestep: 0.1 open_scenario_gateway: ros__parameters: - open_scenario_file: "GaragePlanScenario.xosc" + open_scenario_file: ["GaragePlanScenario.xosc"] active_object_names: ["1"] start_delay_ms: 0 object_control: From 28c6e3ae0b618b123c41a92750e7226fe53d2252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Thor=C3=A9n?= Date: Tue, 5 May 2026 15:54:45 +0200 Subject: [PATCH 2/2] Some fixes Co-authored-by: Copilot --- .../ObjectControl/src/objectcontrol.cpp | 4 +- .../openscenariogateway.py | 31 ++++++------ .../atos_gui/controlpanel/controlpanel.py | 49 ++++++++++--------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/atos/modules/ObjectControl/src/objectcontrol.cpp b/atos/modules/ObjectControl/src/objectcontrol.cpp index afb005399..0834f15a8 100644 --- a/atos/modules/ObjectControl/src/objectcontrol.cpp +++ b/atos/modules/ObjectControl/src/objectcontrol.cpp @@ -675,8 +675,6 @@ void ObjectControl::resetTestObjects() { /** * @brief Reloads the scenario trajectories for all objects - * - * @param isResetting state-flag to indicate if the reset procedure is currently running */ void ObjectControl::reloadScenarioTrajectories() { RCLCPP_INFO(get_logger(), "Reloading scenario trajectories"); @@ -695,7 +693,7 @@ void ObjectControl::reloadScenarioTrajectories() { traj.initializeFromCartesianTrajectory(response->trajectory); objects.at(id)->setTrajectory(traj); - // Re-fetch origin (may reference a different ODR file after a scenario switch) + // Update origin (might be different .xodr for different .xosc) auto originRequest = std::make_shared(); originClient->async_send_request( originRequest, diff --git a/atos/modules/OpenScenarioGateway/openscenariogateway.py b/atos/modules/OpenScenarioGateway/openscenariogateway.py index 783188293..35b3164ec 100755 --- a/atos/modules/OpenScenarioGateway/openscenariogateway.py +++ b/atos/modules/OpenScenarioGateway/openscenariogateway.py @@ -47,7 +47,7 @@ def __init__(self): self.active_objects = {} self.vehicle_catalog = None self.scenarios: dict[str, ScenarioData] = {} - self._active_scenario_file: str = "" + self.active_scenario_file: str = "" self.scenario_file_md5hash = None @@ -65,7 +65,7 @@ def __init__(self): # ROS subscriptions/publishers self.init_ = self.create_subscription(Empty, "init", self.init_callback, 10) self.arm_ = self.create_subscription(Empty, "arm", self.arm_callback, 10) - self.active_scenario_sub_ = self.create_subscription( + self.active_scenario_sub = self.create_subscription( String, "active_scenario", self.active_scenario_callback, 10 ) @@ -104,10 +104,10 @@ def init_callback(self, msg): self.scenarios = {} for file_name in self.get_parameter(SCENARIO_FILE_PARAMETER).value: self.update_scenario(file_name) - # Default to the first scenario after init + # Default to the first scenario in the list files = self.get_parameter(SCENARIO_FILE_PARAMETER).value - if not self._active_scenario_file and files: - self._active_scenario_file = files[0] + if not self.active_scenario_file and files: + self.active_scenario_file = files[0] self.update_active_scenario_objects( self.get_parameter(ACTIVE_OBJECT_NAME_PARAMETER).value ) @@ -118,16 +118,19 @@ def arm_callback(self, msg): self.active_objects[id].started = False def story_board_element_state_change_callback(self, story_board_element): - active = self.active_scenario - if active is None: + active_scenario = self.active_scenario + if active_scenario is None: + self.get_logger().error( + "Received story board element state change but no active scenario is set" + ) return if ( - story_board_element.name in active.start_actions_to_obj_name.keys() + story_board_element.name in active_scenario.start_actions_to_obj_name.keys() and story_board_element.state == RUNNING ): self.handle_start_actions(story_board_element) elif ( - story_board_element.full_path in active.custom_command_map + story_board_element.full_path in active_scenario.custom_command_map and story_board_element.state == RUNNING ): self.handle_custom_command_action(story_board_element) @@ -172,12 +175,10 @@ def handle_custom_command_action(self, story_board_element): def active_scenario_callback(self, msg: String): if msg.data in self.scenarios: - self._active_scenario_file = msg.data + self.active_scenario_file = msg.data self.get_logger().info(f"Active scenario set to: {msg.data}") else: - self.get_logger().warn( - f"Received unknown scenario '{msg.data}'. Call init first." - ) + self.get_logger().warn(f"Received unknown scenario '{msg.data}'") def parameter_callback(self, params): for param in params: @@ -294,8 +295,8 @@ def srv_get_open_scenario_file_path(self, request, response): @property def active_scenario_file_name(self) -> str: - if self._active_scenario_file and self._active_scenario_file in self.scenarios: - return self._active_scenario_file + if self.active_scenario_file and self.active_scenario_file in self.scenarios: + return self.active_scenario_file files = self.get_parameter(SCENARIO_FILE_PARAMETER).value return files[0] if files else "" diff --git a/atos_gui/atos_gui/controlpanel/controlpanel.py b/atos_gui/atos_gui/controlpanel/controlpanel.py index 2159a81b6..4b051793a 100644 --- a/atos_gui/atos_gui/controlpanel/controlpanel.py +++ b/atos_gui/atos_gui/controlpanel/controlpanel.py @@ -57,7 +57,7 @@ def __init__(self) -> None: self.scenario_names = [] self.selected_scenario = "" - self._scenario_select = None + self.scenario_select = None self.active_scenario_pub = self.create_publisher( String, "/atos/active_scenario", QOS ) @@ -117,24 +117,27 @@ def render_page(): color="grey", ) with ui.row().classes("items-center"): - self._scenario_select = ( - ui.select( - options=self.scenario_names, - label="Active scenario", - on_change=lambda e: [ - self.set_active_scenario(e.value), - self.connectPub.publish(Empty()), - ], + with ui.element("div"): + self.scenario_select = ( + ui.select( + options=self.scenario_names, + label="Active scenario", + on_change=lambda e: [ + self.set_active_scenario(e.value), + self.connectPub.publish(Empty()), + ], + ) + .bind_value(self, "selected_scenario") + .bind_enabled_from( + self.OBC_state, "state", backward=lambda s: s == "CONNECTED" + ) + .props("outlined dense") ) - .bind_value(self, "selected_scenario") - .bind_enabled_from( - self.OBC_state, "state", backward=lambda s: s == "CONNECTED" + ui.tooltip( + "Changing scenario can ony be done in CONNECTED state" + ).bind_visibility_from( + self.OBC_state, "state", backward=lambda s: s != "CONNECTED" ) - .props("outlined dense") - ) - ui.label( - "Changing scenario can ony be done in CONNECTED state" - ).classes("text-sm text-grey-7 italic") with ui.row(): ui.label().bind_text_from( self.OBC_state, "state", backward=lambda n: f"State: {n}" @@ -153,7 +156,9 @@ def render_page(): def fetch_scenario_names(self) -> None: if not self.get_scenario_names_client.wait_for_service(timeout_sec=1.0): - self.get_logger().warn("open_scenario_gateway get_parameters not available") + self.get_logger().warn( + "Failed to fetch scenario names: service not available" + ) return req = GetParameters.Request() req.names = ["open_scenario_file"] @@ -173,12 +178,12 @@ def on_scenario_names_fetched(self, future) -> None: ): names = list(response.values[0].string_array_value) self.scenario_names = names - if self._scenario_select is not None: - self._scenario_select.options = names + if self.scenario_select is not None: + self.scenario_select.options = names if not self.selected_scenario and names: self.selected_scenario = names[0] - self._scenario_select.set_value(names[0]) - self._scenario_select.update() + self.scenario_select.set_value(names[0]) + self.scenario_select.update() def set_active_scenario(self, scenario_name: str) -> None: if not isinstance(scenario_name, str) or not scenario_name: