diff --git a/onair/src/ai_components/planners_interface.py b/onair/src/ai_components/planners_interface.py index 80019ee4..78a1666e 100644 --- a/onair/src/ai_components/planners_interface.py +++ b/onair/src/ai_components/planners_interface.py @@ -19,26 +19,26 @@ class PlannersInterface: def __init__(self, headers, _ai_plugins={}): assert(len(headers)>0), 'Headers are required' self.headers = headers - self.planning_constructs = [] - for module_name in list(_planning_plugins.keys()): - spec = importlib.util.spec_from_file_location(module_name, _planning_plugins[module_name]) + self.ai_constructs = [] + for module_name in list(_ai_plugins.keys()): + spec = importlib.util.spec_from_file_location(module_name, _ai_plugins[module_name]) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - self.planning_constructs.append(module.Plugin(module_name,headers)) + self.ai_constructs.append(module.Plugin(module_name,headers)) def update(self, curr_data, status): input_data = curr_data output_data = status_to_oneHot(status) - for plugin in self.planning_constructs: + for plugin in self.ai_constructs: plugin.update(input_data) def apriori_training(self, batch_data): - for plugin in self.planning_constructs: + for plugin in self.ai_constructs: plugin.apriori_training(batch_data) def render_reasoning(self): diagnoses = {} - for plugin in self.planning_constructs: + for plugin in self.ai_constructs: diagnoses[plugin.component_name] = plugin.render_reasoning() return diagnoses diff --git a/onair/src/reasoning/agent.py b/onair/src/reasoning/agent.py index 83dcde33..2b827ced 100644 --- a/onair/src/reasoning/agent.py +++ b/onair/src/reasoning/agent.py @@ -12,20 +12,25 @@ Deals with supervised learning for diagnosing statuses """ from ..ai_components.learners_interface import LearnersInterface +from ..ai_components.planners_interface import PlannersInterface from ..reasoning.diagnosis import Diagnosis class Agent: def __init__(self, vehicle, plugin_list): self.vehicle_rep = vehicle - self.learning_systems = LearnersInterface(self.vehicle_rep.get_headers(),plugin_list) self.mission_status = self.vehicle_rep.get_status() self.bayesian_status = self.vehicle_rep.get_bayesian_status() + # AI Interfaces + self.learning_systems = LearnersInterface(self.vehicle_rep.get_headers(),plugin_list) + self.planning_systems = PlannersInterface(self.vehicle_rep.get_headers(),plugin_list) + # Markov Assumption holds def reason(self, frame): self.vehicle_rep.update(frame) self.mission_status = self.vehicle_rep.get_status() self.learning_systems.update(frame, self.mission_status) + self.planning_systems.update(frame, self.mission_status) def diagnose(self, time_step): """ Grab the mnemonics from the """ diff --git a/test/onair/src/ai_components/test_planners_interface.py b/test/onair/src/ai_components/test_planners_interface.py new file mode 100644 index 00000000..bae6fc7d --- /dev/null +++ b/test/onair/src/ai_components/test_planners_interface.py @@ -0,0 +1,214 @@ +# GSC-19165-1, "The On-Board Artificial Intelligence Research (OnAIR) Platform" +# +# Copyright © 2023 United States Government as represented by the Administrator of +# the National Aeronautics and Space Administration. No copyright is claimed in the +# United States under Title 17, U.S. Code. All Other Rights Reserved. +# +# Licensed under the NASA Open Source Agreement version 1.3 +# See "NOSA GSC-19165-1 OnAIR.pdf" + +""" Test PlannersInterface Functionality """ +import pytest +from mock import MagicMock + +import onair.src.ai_components.planners_interface as planners_interface +from onair.src.ai_components.planners_interface import PlannersInterface + +import importlib +from typing import Dict + +# __init__ tests +def test_PlannersInterface__init__sets_instance_headers_to_given_headers_and_does_nothing_else_when_given__ai_plugins_is_empty(mocker): + # Arrange + arg_headers = [] + arg__ai_plugins = {} + + num_fake_headers = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 headers (0 has own test) + for i in range(num_fake_headers): + arg_headers.append(MagicMock()) + + cut = PlannersInterface.__new__(PlannersInterface) + + # Act + cut.__init__(arg_headers, arg__ai_plugins) + + # Assert + assert cut.headers == arg_headers + +def test_PlannersInterface__init__throws_AttributeError_when_given_module_file_has_no_attribute_Plugin(mocker): + # Arrange + fake_module_name = MagicMock() + arg_headers = [] + + arg__ai_plugins = {MagicMock()} + + # Act + + # Assert + +def test_PlannersInterfacee__init__sets_instance_ai_constructs_to_a_list_of_the_calls_AIPlugIn_with_plugin_and_given_headers_for_each_item_in_given__ai_plugins_when_given__ai_plugins_is_occupied(mocker): + # Arrange + arg_headers = [] + arg__ai_plugins = {} + fake_spec_list = [] + fake_module_list = [] + + num_fake_headers = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 headers (0 has own test) + for i in range(num_fake_headers): + arg_headers.append(MagicMock()) + num_fake_ai_plugins = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) + for i in range(num_fake_ai_plugins): + arg__ai_plugins[str(MagicMock())] = str(MagicMock()) + fake_spec_list.append(MagicMock()) + fake_module_list.append(MagicMock()) + + + expected_ai_constructs = [] + for i in range(num_fake_ai_plugins): + expected_ai_constructs.append(MagicMock()) + + # mocker.patch('importlib.import_module', return_value=fake_imported_module) + mocker.patch('importlib.util.spec_from_file_location',side_effect=fake_spec_list) + mocker.patch('importlib.util.module_from_spec',side_effect=fake_module_list) + for spec in fake_spec_list: + mocker.patch.object(spec,'loader.exec_module') + for i, module in enumerate(fake_module_list): + mocker.patch.object(module,'Plugin',return_value=expected_ai_constructs[i]) + + cut = PlannersInterface.__new__(PlannersInterface) + + # Act + cut.__init__(arg_headers, arg__ai_plugins) + + # Assert + assert importlib.util.spec_from_file_location.call_count == len(arg__ai_plugins) + assert importlib.util.module_from_spec.call_count == len(fake_spec_list) + + for i in range(num_fake_ai_plugins): + fake_name = list(arg__ai_plugins.keys())[i] + fake_path = arg__ai_plugins[fake_name] + assert importlib.util.spec_from_file_location.call_args_list[i].args == (fake_name,fake_path) + assert importlib.util.module_from_spec.call_args_list[i].args == (fake_spec_list[i],) + assert fake_spec_list[i].loader.exec_module.call_count == 1 + assert fake_spec_list[i].loader.exec_module.call_args_list[0].args == (fake_module_list[i],) + assert fake_module_list[i].Plugin.call_count == 1 + assert fake_module_list[i].Plugin.call_args_list[0].args == (fake_name,arg_headers) + + assert cut.ai_constructs == expected_ai_constructs + +# update tests +def test_PlannersInterface_update_only_calls_flotify_input_with_given_curr_data_and_status_to_oneHot_with_given_status_when_instance_ai_constructs_is_empty(mocker): + # Arrange + arg_curr_data = MagicMock() + arg_status = MagicMock() + + mocker.patch(planners_interface.__name__ + '.status_to_oneHot') + + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + # Act + result = cut.update(arg_curr_data, arg_status) + + # Assert + assert planners_interface.status_to_oneHot.call_count == 1 + assert planners_interface.status_to_oneHot.call_args_list[0].args == (arg_status,) + assert result == None + +def test_PlannersInterface_update_calls_flotify_input_with_given_curr_data_and_status_to_oneHot_with_given_status_and_calls_update_on_each_ai_construct_with_input_data_when_instance_ai_constructs_is_occupied(mocker): + # Arrange + arg_curr_data = MagicMock() + arg_status = MagicMock() + + mocker.patch(planners_interface.__name__ + '.status_to_oneHot') + + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + num_fake_ai_constructs = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) + for i in range(num_fake_ai_constructs): + fake_ai_construct = MagicMock() + cut.ai_constructs.append(fake_ai_construct) + mocker.patch.object(fake_ai_construct, 'update') + + # Act + result = cut.update(arg_curr_data, arg_status) + + # Assert + assert planners_interface.status_to_oneHot.call_count == 1 + assert planners_interface.status_to_oneHot.call_args_list[0].args == (arg_status,) + for i in range(num_fake_ai_constructs): + assert cut.ai_constructs[i].update.call_count == 1 + assert cut.ai_constructs[i].update.call_args_list[0].args == (arg_curr_data,) + assert result == None + +# apriori_training tests +def test_PlannersInterface_apriori_training_does_nothing_when_instance_ai_constructs_is_empty(): + # Arrange + arg_batch_data = MagicMock() + + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + # Act + result = cut.apriori_training(arg_batch_data) + + # Assert + assert result == None + +def test_PlannersInterface_apriori_training_calls_apriori_training_on_each_ai_constructs_item(mocker): + # Arrange + arg_batch_data = MagicMock() + + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + num_fake_ai_constructs = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) + for i in range(num_fake_ai_constructs): + cut.ai_constructs.append(MagicMock()) + + # Act + result = cut.apriori_training(arg_batch_data) + + # Assert + for i in range(num_fake_ai_constructs): + assert cut.ai_constructs[i].apriori_training.call_count == 1 + assert cut.ai_constructs[i].apriori_training.call_args_list[0].args == (arg_batch_data, ) + assert result == None + +# render_reasoning tests +def test_PlannersInterface_render_reasoning_returns_empty_dict_when_instance_ai_constructs_is_empty(mocker): + # Arrange + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + # Act + result = cut.render_reasoning() + + # Assert + assert result == {} + +def test_PlannersInterface_render_reasoning_returns_dict_of_each_ai_construct_as_key_to_the_result_of_its_render_reasoning_when_instance_ai_constructs_is_occupied(mocker): + # Arrange + cut = PlannersInterface.__new__(PlannersInterface) + cut.ai_constructs = [] + + expected_result = {} + + num_fake_ai_constructs = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) + for i in range(num_fake_ai_constructs): + fake_ai_construct = MagicMock() + forced_return_ai_construct_render_reasoning = MagicMock() + cut.ai_constructs.append(fake_ai_construct) + mocker.patch.object(fake_ai_construct, 'render_reasoning', return_value=forced_return_ai_construct_render_reasoning) + fake_ai_construct.component_name = MagicMock() + expected_result[fake_ai_construct.component_name] = forced_return_ai_construct_render_reasoning + + # Act + result = cut.render_reasoning() + + # Assert + for i in range(num_fake_ai_constructs): + assert cut.ai_constructs[i].render_reasoning.call_count == 1 + assert cut.ai_constructs[i].render_reasoning.call_args_list[0].args == () + assert result == expected_result \ No newline at end of file diff --git a/test/onair/src/reasoning/test_agent.py b/test/onair/src/reasoning/test_agent.py index cef204ce..65c37aa2 100644 --- a/test/onair/src/reasoning/test_agent.py +++ b/test/onair/src/reasoning/test_agent.py @@ -21,12 +21,14 @@ def test_Agent__init__sets_vehicle_rep_to_given_vehicle_and_learning_systems_and fake_headers = MagicMock() fake_learning_systems = MagicMock() + fake_planning_systems = MagicMock() fake_mission_status = MagicMock() fake_bayesian_status = MagicMock() fake_plugin_list = MagicMock() mocker.patch.object(arg_vehicle, 'get_headers', return_value=fake_headers) mocker.patch(agent.__name__ + '.LearnersInterface', return_value=fake_learning_systems) + mocker.patch(agent.__name__ + '.PlannersInterface', return_value=fake_planning_systems) mocker.patch.object(arg_vehicle, 'get_status', return_value=fake_mission_status) mocker.patch.object(arg_vehicle, 'get_bayesian_status', return_value=fake_bayesian_status) @@ -37,11 +39,14 @@ def test_Agent__init__sets_vehicle_rep_to_given_vehicle_and_learning_systems_and # Assert assert cut.vehicle_rep == arg_vehicle - assert arg_vehicle.get_headers.call_count == 1 + assert arg_vehicle.get_headers.call_count == 2 assert arg_vehicle.get_headers.call_args_list[0].args == () assert agent.LearnersInterface.call_count == 1 assert agent.LearnersInterface.call_args_list[0].args == (fake_headers, fake_plugin_list) assert cut.learning_systems == fake_learning_systems + assert agent.PlannersInterface.call_count == 1 + assert agent.PlannersInterface.call_args_list[0].args == (fake_headers, fake_plugin_list) + assert cut.planning_systems == fake_planning_systems assert arg_vehicle.get_status.call_count == 1 assert arg_vehicle.get_status.call_args_list[0].args == () assert cut.mission_status == fake_mission_status @@ -59,6 +64,7 @@ def test_Agent_reason_updates_vehicle_rep_with_given_frame_and_sets_new_vehicle_ cut = Agent.__new__(Agent) cut.vehicle_rep = MagicMock() cut.learning_systems = MagicMock() + cut.planning_systems = MagicMock() mocker.patch.object(cut.vehicle_rep, 'update') mocker.patch.object(cut.vehicle_rep, 'get_status', return_value=fake_mission_status) @@ -74,6 +80,8 @@ def test_Agent_reason_updates_vehicle_rep_with_given_frame_and_sets_new_vehicle_ assert cut.vehicle_rep.get_status.call_args_list[0].args == () assert cut.learning_systems.update.call_count == 1 assert cut.learning_systems.update.call_args_list[0].args == (arg_frame, fake_mission_status) + assert cut.planning_systems.update.call_count == 1 + assert cut.planning_systems.update.call_args_list[0].args == (arg_frame, fake_mission_status) # diagnose tests def test_Agent_diagnose_returns_empty_Dict(): @@ -82,6 +90,7 @@ def test_Agent_diagnose_returns_empty_Dict(): cut = Agent.__new__(Agent) cut.learning_systems = MagicMock() + cut.planning_systems = MagicMock() cut.bayesian_status = MagicMock() cut.vehicle_rep = MagicMock()