diff --git a/onair/src/data_driven_components/__init__.py b/onair/src/ai_components/__init__.py similarity index 100% rename from onair/src/data_driven_components/__init__.py rename to onair/src/ai_components/__init__.py diff --git a/onair/src/data_driven_components/ai_plugin_abstract/__init__.py b/onair/src/ai_components/ai_plugin_abstract/__init__.py similarity index 100% rename from onair/src/data_driven_components/ai_plugin_abstract/__init__.py rename to onair/src/ai_components/ai_plugin_abstract/__init__.py diff --git a/onair/src/data_driven_components/ai_plugin_abstract/core.py b/onair/src/ai_components/ai_plugin_abstract/core.py similarity index 100% rename from onair/src/data_driven_components/ai_plugin_abstract/core.py rename to onair/src/ai_components/ai_plugin_abstract/core.py diff --git a/onair/src/data_driven_components/data_driven_learning.py b/onair/src/ai_components/learners_interface.py similarity index 95% rename from onair/src/data_driven_components/data_driven_learning.py rename to onair/src/ai_components/learners_interface.py index 4a37d491..b07c2327 100644 --- a/onair/src/data_driven_components/data_driven_learning.py +++ b/onair/src/ai_components/learners_interface.py @@ -11,11 +11,10 @@ Data driven learning class for managing all data driven AI components """ import importlib.util -import importlib.util from ..util.data_conversion import * -class DataDrivenLearning: +class LearnersInterface: def __init__(self, headers, _ai_plugins={}): assert(len(headers)>0), 'Headers are required' self.headers = headers @@ -26,21 +25,21 @@ def __init__(self, headers, _ai_plugins={}): spec.loader.exec_module(module) self.ai_constructs.append(module.Plugin(module_name,headers)) + def apriori_training(self, batch_data): + for plugin in self.ai_constructs: + plugin.apriori_training(batch_data) + def update(self, curr_data, status): input_data = curr_data output_data = status_to_oneHot(status) for plugin in self.ai_constructs: plugin.update(input_data) - def apriori_training(self, batch_data): - for plugin in self.ai_constructs: - plugin.apriori_training(batch_data) + def check_for_salient_event(self): + pass def render_reasoning(self): diagnoses = {} for plugin in self.ai_constructs: diagnoses[plugin.component_name] = plugin.render_reasoning() return diagnoses - - - diff --git a/onair/src/ai_components/planners_interface.py b/onair/src/ai_components/planners_interface.py new file mode 100644 index 00000000..f8339b86 --- /dev/null +++ b/onair/src/ai_components/planners_interface.py @@ -0,0 +1,37 @@ +# 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" + +""" +Planners interface class for managing all planning-based AI components +""" +import importlib.util + +from ..util.data_conversion import * + +class PlannersInterface: + def __init__(self, headers, _ai_plugins={}): + assert(len(headers)>0), 'Headers are required' + self.headers = headers + 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.ai_constructs.append(module.Plugin(module_name,headers)) + + def update(self, curr_raw_tlm, status): + # Raw TLM should be transformed into high-leve state representation here + # Can store something as stale unless a planning thread is launched + pass + + def check_for_salient_event(self): + pass + + def render_reasoning(self): + pass diff --git a/onair/src/reasoning/agent.py b/onair/src/reasoning/agent.py index 6a28a16d..9b83a5ee 100644 --- a/onair/src/reasoning/agent.py +++ b/onair/src/reasoning/agent.py @@ -11,21 +11,31 @@ Agent Class Deals with supervised learning for diagnosing statuses """ -from ..data_driven_components.data_driven_learning import DataDrivenLearning +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 = DataDrivenLearning(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): + # Update with new telemetry 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) + + # Check for a salient event, needing acionable outcome + self.learning_systems.check_for_salient_event() + self.planning_systems.check_for_salient_event() def diagnose(self, time_step): """ Grab the mnemonics from the """ diff --git a/plugins/generic/generic_plugin.py b/plugins/generic/generic_plugin.py index 1b0da3ae..6a8a9acd 100644 --- a/plugins/generic/generic_plugin.py +++ b/plugins/generic/generic_plugin.py @@ -8,7 +8,7 @@ # See "NOSA GSC-19165-1 OnAIR.pdf" import numpy as np -from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn +from onair.src.ai_components.ai_plugin_abstract.core import AIPlugIn class Plugin(AIPlugIn): def apriori_training(self,batch_data=[]): diff --git a/plugins/kalman_plugin/kalman_plugin.py b/plugins/kalman_plugin/kalman_plugin.py index 7a557361..f97bb62a 100644 --- a/plugins/kalman_plugin/kalman_plugin.py +++ b/plugins/kalman_plugin/kalman_plugin.py @@ -9,7 +9,7 @@ import simdkalman import numpy as np -from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn +from onair.src.ai_components.ai_plugin_abstract.core import AIPlugIn class Plugin(AIPlugIn): def __init__(self, name, headers, window_size=3): diff --git a/test/onair/src/data_driven_components/ai_plugin_abstract/test_core.py b/test/onair/src/ai_components/ai_plugin_abstract/test_core.py similarity index 96% rename from test/onair/src/data_driven_components/ai_plugin_abstract/test_core.py rename to test/onair/src/ai_components/ai_plugin_abstract/test_core.py index 7667b5fb..ec7eb6ea 100644 --- a/test/onair/src/data_driven_components/ai_plugin_abstract/test_core.py +++ b/test/onair/src/ai_components/ai_plugin_abstract/test_core.py @@ -11,8 +11,8 @@ import pytest from mock import MagicMock -import onair.src.data_driven_components.ai_plugin_abstract.core as core -from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn +import onair.src.ai_components.ai_plugin_abstract.core as core +from onair.src.ai_components.ai_plugin_abstract.core import AIPlugIn class FakeAIPlugIn(AIPlugIn): def __init__(self, _name, _headers): diff --git a/test/onair/src/data_driven_components/test_data_driven_learning.py b/test/onair/src/ai_components/test_learners_interface.py similarity index 69% rename from test/onair/src/data_driven_components/test_data_driven_learning.py rename to test/onair/src/ai_components/test_learners_interface.py index d0054cc3..4826b75b 100644 --- a/test/onair/src/data_driven_components/test_data_driven_learning.py +++ b/test/onair/src/ai_components/test_learners_interface.py @@ -7,18 +7,18 @@ # Licensed under the NASA Open Source Agreement version 1.3 # See "NOSA GSC-19165-1 OnAIR.pdf" -""" Test DataDrivenLearning Functionality """ +""" Test LearnersInterface Functionality """ import pytest from mock import MagicMock -import onair.src.data_driven_components.data_driven_learning as data_driven_learning -from onair.src.data_driven_components.data_driven_learning import DataDrivenLearning +import onair.src.ai_components.learners_interface as learners_interface +from onair.src.ai_components.learners_interface import LearnersInterface import importlib from typing import Dict # __init__ tests -def test_DataDrivenLearning__init__sets_instance_headers_to_given_headers_and_does_nothing_else_when_given__ai_plugins_is_empty(mocker): +def test_LearnersInterface__init__sets_instance_headers_to_given_headers_and_does_nothing_else_when_given__ai_plugins_is_empty(mocker): # Arrange arg_headers = [] arg__ai_plugins = {} @@ -27,7 +27,7 @@ def test_DataDrivenLearning__init__sets_instance_headers_to_given_headers_and_do for i in range(num_fake_headers): arg_headers.append(MagicMock()) - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) # Act cut.__init__(arg_headers, arg__ai_plugins) @@ -35,7 +35,7 @@ def test_DataDrivenLearning__init__sets_instance_headers_to_given_headers_and_do # Assert assert cut.headers == arg_headers -def test_DataDrivenLearning__init__throws_AttributeError_when_given_module_file_has_no_attribute_Plugin(mocker): +def test_LearnersInterface__init__throws_AttributeError_when_given_module_file_has_no_attribute_Plugin(mocker): # Arrange fake_module_name = MagicMock() arg_headers = [] @@ -48,7 +48,7 @@ def test_DataDrivenLearning__init__throws_AttributeError_when_given_module_file_ # Assert -def test_DataDrivenLearning__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): +def test_LearnersInterface__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 = {} @@ -77,7 +77,7 @@ def test_DataDrivenLearning__init__sets_instance_ai_constructs_to_a_list_of_the_ for i, module in enumerate(fake_module_list): mocker.patch.object(module,'Plugin',return_value=expected_ai_constructs[i]) - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) # Act cut.__init__(arg_headers, arg__ai_plugins) @@ -99,32 +99,32 @@ def test_DataDrivenLearning__init__sets_instance_ai_constructs_to_a_list_of_the_ assert cut.ai_constructs == expected_ai_constructs # update tests -def test_DataDrivenLearning_update_only_calls_flotify_input_with_given_curr_data_and_status_to_oneHot_with_given_status_when_instance_ai_constructs_is_empty(mocker): +def test_LearnersInterface_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(data_driven_learning.__name__ + '.status_to_oneHot') + mocker.patch(learners_interface.__name__ + '.status_to_oneHot') - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] # Act result = cut.update(arg_curr_data, arg_status) # Assert - assert data_driven_learning.status_to_oneHot.call_count == 1 - assert data_driven_learning.status_to_oneHot.call_args_list[0].args == (arg_status,) + assert learners_interface.status_to_oneHot.call_count == 1 + assert learners_interface.status_to_oneHot.call_args_list[0].args == (arg_status,) assert result == None -def test_DataDrivenLearning_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): +def test_LearnersInterface_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(data_driven_learning.__name__ + '.status_to_oneHot') + mocker.patch(learners_interface.__name__ + '.status_to_oneHot') - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] num_fake_ai_constructs = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) @@ -137,19 +137,19 @@ def test_DataDrivenLearning_update_calls_flotify_input_with_given_curr_data_and_ result = cut.update(arg_curr_data, arg_status) # Assert - assert data_driven_learning.status_to_oneHot.call_count == 1 - assert data_driven_learning.status_to_oneHot.call_args_list[0].args == (arg_status,) + assert learners_interface.status_to_oneHot.call_count == 1 + assert learners_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_DataDrivenLearning_apriori_training_does_nothing_when_instance_ai_constructs_is_empty(): +def test_LearnersInterface_apriori_training_does_nothing_when_instance_ai_constructs_is_empty(): # Arrange arg_batch_data = MagicMock() - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] # Act @@ -158,11 +158,11 @@ def test_DataDrivenLearning_apriori_training_does_nothing_when_instance_ai_const # Assert assert result == None -def test_DataDrivenLearning_apriori_training_calls_apriori_training_on_each_ai_constructs_item(mocker): +def test_LearnersInterface_apriori_training_calls_apriori_training_on_each_ai_constructs_item(mocker): # Arrange arg_batch_data = MagicMock() - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] num_fake_ai_constructs = pytest.gen.randint(1, 10) # arbitrary, from 1 to 10 (0 has own test) @@ -178,10 +178,21 @@ def test_DataDrivenLearning_apriori_training_calls_apriori_training_on_each_ai_c assert cut.ai_constructs[i].apriori_training.call_args_list[0].args == (arg_batch_data, ) assert result == None +# check_for_salient_event +def test_salient_event_does_nothing(): + # Arrange + cut = LearnersInterface.__new__(LearnersInterface) + + # Act + result = cut.check_for_salient_event() + + # Assert + assert result == None + # render_reasoning tests -def test_DataDrivenLearning_render_reasoning_returns_empty_dict_when_instance_ai_constructs_is_empty(mocker): +def test_LearnersInterface_render_reasoning_returns_empty_dict_when_instance_ai_constructs_is_empty(mocker): # Arrange - cut = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] # Act @@ -190,9 +201,9 @@ def test_DataDrivenLearning_render_reasoning_returns_empty_dict_when_instance_ai # Assert assert result == {} -def test_DataDrivenLearning_render_reasoning_returns_dict_of_each_ai_construct_as_key_to_the_result_of_its_render_reasoning_when_instance_ai_constructs_is_occupied(mocker): +def test_LearnersInterface_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 = DataDrivenLearning.__new__(DataDrivenLearning) + cut = LearnersInterface.__new__(LearnersInterface) cut.ai_constructs = [] expected_result = {} 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..6de25e24 --- /dev/null +++ b/test/onair/src/ai_components/test_planners_interface.py @@ -0,0 +1,118 @@ +# 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__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(i)] = 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 + +def test_update_does_nothing(): + # Arrange + arg_curr_raw_tlm = MagicMock() + arg_status = MagicMock() + + cut = PlannersInterface.__new__(PlannersInterface) + + # Act + result = cut.update(arg_curr_raw_tlm, arg_status) + + # Assert + assert result == None + +def test_check_for_salient_event_does_nothing(): + # Arrange + cut = PlannersInterface.__new__(PlannersInterface) + + # Act + result = cut.check_for_salient_event() + + # Assert + assert result == None + +def test_render_reasoning_does_nothing(): + # Arrange + cut = PlannersInterface.__new__(PlannersInterface) + + # Act + result = cut.render_reasoning() + + # Assert + assert result == None \ 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 5713ea06..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__ + '.DataDrivenLearning', return_value=fake_learning_systems) + 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.DataDrivenLearning.call_count == 1 - assert agent.DataDrivenLearning.call_args_list[0].args == (fake_headers, fake_plugin_list) + 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()