Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

24 allow for dynamic plugins #36

Merged
merged 15 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import pytest
import coverage
# coverage started early to see all lines in all files (def and imports were being missed with programmatic runs)
cov = coverage.Coverage(source=['onair'], branch=True)
cov = coverage.Coverage(source=['onair','plugins'], branch=True)
cov.start()

import os
Expand Down
1 change: 1 addition & 0 deletions onair/config/default_config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ TelemetryFiles = ['700_crash_to_earth_1.csv']
ParserFileName = csv_parser
ParserName = CSV
SimName = CSV
PluginList = {'generic_plugin':'plugins/generic/generic_plugin.py'} # format: {plugin_name : plugin_path}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an area where we can put an assertion statement?


[RUN_FLAGS]
IO_Flag = true
Expand Down
15 changes: 10 additions & 5 deletions onair/src/data_driven_components/data_driven_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@
"""
Data driven learning class for managing all data driven AI components
"""
import importlib
import importlib.util

from ..util.data_conversion import *

class DataDrivenLearning:
def __init__(self, headers, _ai_plugins:list=[]):
def __init__(self, headers, _ai_plugins={}):
assert(len(headers)>0)
self.headers = headers
self.ai_constructs = [
importlib.import_module('onair.src.data_driven_components.' + plugin_name + '.' + f'{plugin_name}_plugin').Plugin(plugin_name, headers) for plugin_name in _ai_plugins
]
self.ai_constructs = []

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertion here, or earlier. Also, I'd remove this empty space.

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_data, status):
input_data = curr_data
Expand Down
4 changes: 2 additions & 2 deletions onair/src/reasoning/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from ..reasoning.diagnosis import Diagnosis

class Agent:
def __init__(self, vehicle):
def __init__(self, plugin_list, vehicle):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd swap the order (most significant to least significant)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, follows the instantiation order

self.vehicle_rep = vehicle
self.learning_systems = DataDrivenLearning(self.vehicle_rep.get_headers())
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()

Expand Down
8 changes: 7 additions & 1 deletion onair/src/run_scripts/execution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import shutil
from distutils.dir_util import copy_tree
from time import gmtime, strftime
import ast

from ...data_handling.time_synchronizer import TimeSynchronizer
from ..run_scripts.sim import Simulator
Expand Down Expand Up @@ -49,6 +50,9 @@ def __init__(self, config_file='', run_name='', save_flag=False):
self.sim_name = ''
self.processedSimData = None
self.sim = None

# Init plugins
self.plugin_list = ['']

self.save_flag = save_flag
self.save_name = run_name
Expand Down Expand Up @@ -94,6 +98,8 @@ def parse_configs(self, config_filepath):
self.benchmarkIndices = config['DEFAULT']['BenchmarkIndices']
except:
pass
## Sort Data: Plugins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is sorting happening? I think this function turns a string into list (with extra processing of tokens , beyond pythons built in "list()").

self.plugin_list = ast.literal_eval(config['DEFAULT']['PluginList'])

def parse_data(self, parser_name, parser_file_name, dataFilePath, metadataFilePath, subsystems_breakdown=False):
parser = importlib.import_module('onair.data_handling.parsers.' + parser_file_name)
Expand All @@ -104,7 +110,7 @@ def parse_data(self, parser_name, parser_file_name, dataFilePath, metadataFilePa
self.processedSimData = TimeSynchronizer(*parsed_data.get_sim_data())

def setup_sim(self):
self.sim = Simulator(self.sim_name, self.processedSimData, self.SBN_Flag)
self.sim = Simulator(self.sim_name, self.processedSimData, self.plugin_list, self.SBN_Flag)
try:
fls = ast.literal_eval(self.benchmarkFiles)
fp = os.path.dirname(os.path.realpath(__file__)) + '/../..' + self.benchmarkFilePath
Expand Down
4 changes: 2 additions & 2 deletions onair/src/run_scripts/sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DIAGNOSIS_INTERVAL = 100

class Simulator:
def __init__(self, simType, parsedData, SBN_Flag):
def __init__(self, simType, parsedData, plugin_list, SBN_Flag):
self.simulator = simType
vehicle = VehicleRepresentation(*parsedData.get_vehicle_metadata())

Expand All @@ -39,7 +39,7 @@ def __init__(self, simType, parsedData, SBN_Flag):

else:
self.simData = DataSource(parsedData.get_sim_data())
self.agent = Agent(vehicle)
self.agent = Agent(plugin_list,vehicle)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swap


def run_sim(self, IO_Flag=False, dev_flag=False, viz_flag = True):
if IO_Flag == True: print_sim_header()
Expand Down
1 change: 1 addition & 0 deletions plugins/generic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .generic_plugin import Plugin
30 changes: 30 additions & 0 deletions plugins/generic/generic_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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"

import numpy as np
from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn

class Plugin(AIPlugIn):
def apriori_training(self,batch_data=[]):
"""
Given data, system should learn any priors necessary for realtime diagnosis.
"""
pass

def update(self,frame=[]):
"""
Given streamed data point, system should update internally
"""
pass

def render_reasoning(self):
"""
System should return its diagnosis
"""
pass
1 change: 1 addition & 0 deletions plugins/kalman_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .kalman_plugin import Plugin
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import simdkalman
import numpy as np
from ...data_driven_components.generic_component import AIPlugIn
from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn

class Plugin(AIPlugIn):
def __init__(self, name, headers, window_size=3):
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import pytest
from mock import MagicMock

import onair.src.data_driven_components.generic_component.core as core
from onair.src.data_driven_components.generic_component.core import AIPlugIn
import onair.src.data_driven_components.ai_plugin_abstract.core as core
from onair.src.data_driven_components.ai_plugin_abstract.core import AIPlugIn

class FakeAIPlugIn(AIPlugIn):
def __init__(self, _name, _headers):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
from onair.src.data_driven_components.data_driven_learning import DataDrivenLearning

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):
# Arrange
arg_headers = []
arg__ai_plugins = []
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):
Expand All @@ -34,64 +35,54 @@ def test_DataDrivenLearning__init__sets_instance_headers_to_given_headers_and_do
# Assert
assert cut.headers == arg_headers

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(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())
fake_imported_module = 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.append(str(MagicMock()))

mocker.patch('importlib.import_module', return_value=fake_imported_module)

cut = DataDrivenLearning.__new__(DataDrivenLearning)

# Act
cut.__init__(arg_headers, arg__ai_plugins)

# Assert
assert importlib.import_module.call_count == num_fake_ai_plugins
for i in range(num_fake_ai_plugins):
assert importlib.import_module.call_args_list[i].args == ('onair.src.data_driven_components.' + arg__ai_plugins[i] + '.' + arg__ai_plugins[i] + '_plugin',)

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):
# Arrange
arg_headers = []
arg__ai_plugins = []
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())
fake_imported_module = 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.append(str(MagicMock()))
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.object(fake_imported_module, 'Plugin', side_effect=expected_ai_constructs)
# 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 = DataDrivenLearning.__new__(DataDrivenLearning)

# Act
cut.__init__(arg_headers, arg__ai_plugins)

# Assert
assert importlib.import_module.call_count == num_fake_ai_plugins
for i in range(num_fake_ai_plugins):
assert importlib.import_module.call_args_list[i].args == (f'onair.src.data_driven_components.{arg__ai_plugins[i]}.{arg__ai_plugins[i]}_plugin',)
assert fake_imported_module.Plugin.call_count == num_fake_ai_plugins
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):
assert fake_imported_module.Plugin.call_args_list[i].args == (arg__ai_plugins[i], arg_headers)

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_Agent__init__sets_vehicle_rep_to_given_vehicle_and_learning_systems_and
fake_learning_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)
Expand All @@ -32,14 +33,14 @@ def test_Agent__init__sets_vehicle_rep_to_given_vehicle_and_learning_systems_and
cut = Agent.__new__(Agent)

# Act
result = cut.__init__(arg_vehicle)
result = cut.__init__(fake_plugin_list, arg_vehicle)

# Assert
assert cut.vehicle_rep == arg_vehicle
assert arg_vehicle.get_headers.call_count == 1
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, )
assert agent.DataDrivenLearning.call_args_list[0].args == (fake_headers, fake_plugin_list)
assert cut.learning_systems == fake_learning_systems
assert arg_vehicle.get_status.call_count == 1
assert arg_vehicle.get_status.call_args_list[0].args == ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
'PluginList':dict(MagicMock())
}
fake_run_flags = MagicMock()
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
Expand All @@ -144,12 +145,15 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
fake_Dev_flags = MagicMock()
fake_SBN_flags = MagicMock()
fake_Viz_flags = MagicMock()
fake_plugin_list = dict(MagicMock())


cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch.object(fake_run_flags, 'getboolean', side_effect=[fake_IO_flags, fake_Dev_flags, fake_SBN_flags, fake_Viz_flags])
mocker.patch('ast.literal_eval',return_value=fake_plugin_list)

# Act
cut.parse_configs(arg_config_filepath)
Expand All @@ -167,6 +171,7 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
assert cut.parser_file_name == fake_default['ParserFileName']
assert cut.parser_name == fake_default['ParserName']
assert cut.sim_name == fake_default['SimName']
assert cut.plugin_list == fake_default['PluginList']
assert fake_run_flags.getboolean.call_count == 4
assert fake_run_flags.getboolean.call_args_list[0].args == ('IO_Flag', )
assert cut.IO_Flag == fake_IO_flags
Expand All @@ -189,6 +194,7 @@ def test_ExecutionEngine_parse_configs_bypasses_benchmarks_when_access_raises_er
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
'PluginList':MagicMock()
}
fake_run_flags = MagicMock()
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
Expand All @@ -200,12 +206,14 @@ def test_ExecutionEngine_parse_configs_bypasses_benchmarks_when_access_raises_er
fake_Dev_flags = MagicMock()
fake_SBN_flags = MagicMock()
fake_Viz_flags = MagicMock()
fake_plugin_dict = dict(MagicMock())

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch.object(fake_run_flags, 'getboolean', side_effect=[fake_IO_flags, fake_Dev_flags, fake_SBN_flags, fake_Viz_flags])
mocker.patch('ast.literal_eval',return_value=fake_plugin_dict)

# Act
cut.parse_configs(arg_config_filepath)
Expand Down Expand Up @@ -367,6 +375,7 @@ def test_ExecutionEngine_setup_sim_sets_self_sim_to_new_Simulator_and_sets_bench
cut.benchmarkFiles = MagicMock()
cut.benchmarkFilePath = MagicMock()
cut.benchmarkIndices = MagicMock()
cut.plugin_list = MagicMock()

fake_sim = MagicMock()
fake_fls = MagicMock()
Expand All @@ -388,7 +397,7 @@ def test_ExecutionEngine_setup_sim_sets_self_sim_to_new_Simulator_and_sets_bench

# Assert
assert execution_engine.Simulator.call_count == 1
assert execution_engine.Simulator.call_args_list[0].args == (cut.sim_name, cut.processedSimData, cut.SBN_Flag, )
assert execution_engine.Simulator.call_args_list[0].args == (cut.sim_name, cut.processedSimData, cut.plugin_list, cut.SBN_Flag)
assert cut.sim == fake_sim
assert execution_engine.ast.literal_eval.call_count == 2
assert execution_engine.ast.literal_eval.call_args_list[0].args == (cut.benchmarkFiles, )
Expand All @@ -407,6 +416,7 @@ def test_ExecutionEngine_setup_sim_sets_self_sim_to_new_Simulator_but_does_not_s
cut.benchmarkFiles = MagicMock()
cut.benchmarkFilePath = MagicMock()
cut.benchmarkIndices = MagicMock()
cut.plugin_list = MagicMock()

fake_sim = MagicMock()
fake_fls = MagicMock()
Expand All @@ -427,7 +437,7 @@ def test_ExecutionEngine_setup_sim_sets_self_sim_to_new_Simulator_but_does_not_s

# Assert
assert execution_engine.Simulator.call_count == 1
assert execution_engine.Simulator.call_args_list[0].args == (cut.sim_name, cut.processedSimData, cut.SBN_Flag, )
assert execution_engine.Simulator.call_args_list[0].args == (cut.sim_name, cut.processedSimData, cut.plugin_list, cut.SBN_Flag)
assert cut.sim == fake_sim
assert execution_engine.ast.literal_eval.call_count == 1
assert execution_engine.ast.literal_eval.call_args_list[0].args == (cut.benchmarkFiles, )
Expand Down
Loading