Skip to content

Commit

Permalink
Added Python evaluator block
Browse files Browse the repository at this point in the history
* Closes #20
  • Loading branch information
ncorgan committed Jan 16, 2021
1 parent 5ae94e2 commit caed10c
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 2 deletions.
13 changes: 13 additions & 0 deletions Blocks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
########################################################################
## Build and install
########################################################################
POTHOS_PYTHON_UTIL(
TARGET PythonBlocks
SOURCES
__init__.py
Evaluator.py
FACTORIES
"/python/evaluator:Evaluator"
DESTINATION PothosBlocks
ENABLE_DOCS
)
167 changes: 167 additions & 0 deletions Blocks/Evaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright (c) 2020-2021 Nicholas Corgan
# SPDX-License-Identifier: BSL-1.0

from functools import partial
import importlib

import Pothos

"""
/***********************************************************************
* |PothosDoc Python Evaluator
*
* The Python evaluator block performs a user-specified expression evaluation
* on input slot(s) and produces the evaluation result on an output signal.
* The input slots are user-defined. The output signal is named "triggered".
* The arguments from the input slots must be primitive types.
*
* |category /Event
* |keywords signal slot eval expression
*
* |param imports[Imports] A list of Python modules to import before executing the expression.
* Example: ["math", "numpy"] will import the math and numpy modules.
* |default ["math"]
*
* |param args[Arguments] A list of named variables to use in the expression.
* Each variable corresponds to settings slot on the transform block.
* Example: ["foo", "bar"] will create the slots "setFoo" and "setBar".
* |default ["val"]
*
* |param expr[Expression] The expression to re-evaluate for each slot event.
* An expression is valid Python, comprised of combinations of variables, constants, and math functions.
* Example: math.log2(foo)/bar
*
* <p><b>Multi-argument input:</b> Upstream blocks may pass multiple arguments to a slot.
* Each argument will be available to the expression suffixed by its argument index.
* For example, suppose that the slot "setBaz" has two arguments,
* then the following expression would use both arguments: "baz0 + baz1"</p>
*
* |default "math.log2(val)"
* |widget StringEntry()
*
* |param localVars[LocalVars] A map of variable names to values.
* This allows you to use global variables from the topology in the expression.
*
* For example this mapping lets us use foo, bar, and baz in the expression
* to represent several different globals and combinations of expressions:
* {"foo": myGlobal, "bar": "test123", "baz": myNum+12345}
* |default {}
* |preview valid
*
* |factory /python/evaluator(args)
* |setter setExpression(expr)
* |setter setImports(imports)
* |setter setLocalVars(localVars)
**********************************************************************/
"""
class Evaluator(Pothos.Block):
def __init__(self, varNames):
Pothos.Block.__init__(self)
self.setName("/python/evaluator")

self.__checkIsStringList(varNames)

self.__expr = ""
self.__localVars = dict()
self.__varNames = varNames
self.__varValues = dict()
self.__imports = []
self.__varsReady = set()

# Add setters for user variables
for name in self.__varNames:
if not name:
continue

setterName = "set"+name[0].upper()+name[1:]
setattr(Evaluator, setterName, partial(self.__setter, name))
self.registerSlot(setterName)

self.registerSlot("setExpression")
self.registerSlot("setImports")
self.registerSlot("setLocalVars")
self.registerSignal("triggered")

def getExpression(self):
return self.__expr

def setExpression(self,expr):
self.__checkIsStr(expr)
self.__expr = expr

notReadyVars = [var for var in self.__varNames if var not in self.__varsReady]
if notReadyVars:
return

args = self.__performEval()
self.triggered(args)

def setImports(self,imports):
self.__checkIsStringOrStringList(imports)
self.__imports = imports if (type(imports) == list) else [imports]

def getImports(self):
return self.__imports

def setLocalVars(self,userLocalVars):
self.__checkIsDict(userLocalVars)
self.__localVars = userLocalVars

#
# Private utility functions
#

def __performEval(self):
for key,val in self.__varValues.items():
locals()[key] = val

for mod in self.__imports:
exec("import "+mod)

for key,val in self.__localVars.items():
locals()[key] = val

return eval(self.__expr)

def __setter(self,field,*args):
if len(args) > 1:
for i in range(len(args)):
self.__varValues[field+str(i)] = args[i]
else:
self.__varValues[field] = args[0]

self.__varsReady.add(field)

notReadyVars = [var for var in self.__varNames if var not in self.__varsReady]
if (not notReadyVars) and self.__expr:
args = self.__performEval()
self.triggered(args)

return None

def __checkIsStr(self,var):
if type(var) != str:
raise ValueError("The given value must be a str. Found {0}".format(type(var)))

def __checkIsDict(self,var):
if type(var) != dict:
raise ValueError("The given value must be a dict. Found {0}".format(type(var)))

def __checkIsList(self,var):
if type(var) != list:
raise ValueError("The given value must be a list. Found {0}".format(type(var)))

def __checkIsStringList(self,var):
self.__checkIsList(var)

nonStringVals = [x for x in var if type(x) != str]
if nonStringVals:
raise ValueError("All list values must be strings. Found {0}".format(type(nonStringVals[0])))

def __checkIsStringOrStringList(self,var):
if type(var) is str:
return
elif type(var) is list:
self.__checkIsStringList(var)
else:
raise ValueError("The given value must be a string or list. Found {0}".format(type(var)))
4 changes: 4 additions & 0 deletions Blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (c) 2020-2021 Nicholas Corgan
# SPDX-License-Identifier: BSL-1.0

from . Evaluator import Evaluator
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,5 @@ install(
# Enter the subdirectory configuration
########################################################################
add_subdirectory(Pothos)
add_subdirectory(Blocks)
add_subdirectory(TestBlocks)
2 changes: 1 addition & 1 deletion Pothos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright (c) 2014-2016 Josh Blum
# 2019 Nicholas Corgan
# 2019-2020 Nicholas Corgan
# SPDX-License-Identifier: BSL-1.0

from . PothosModule import *
Expand Down
96 changes: 95 additions & 1 deletion TestPythonBlock.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
// Copyright (c) 2014-2017 Josh Blum
// 2020-2021 Nicholas Corgan
// SPDX-License-Identifier: BSL-1.0

#include <Pothos/Framework.hpp>
#include <Pothos/Managed.hpp>
#include <Pothos/Testing.hpp>
#include <Pothos/Proxy.hpp>
#include <iostream>

#include <json.hpp>

#include <Poco/Path.h>
#include <Poco/TemporaryFile.h>
#include <Poco/Thread.h>

#include <complex>
#include <cmath>
#include <iostream>

using json = nlohmann::json;

POTHOS_TEST_BLOCK("/proxy/python/tests", python_module_import)
Expand Down Expand Up @@ -68,3 +77,88 @@ POTHOS_TEST_BLOCK("/proxy/python/tests", test_signals_and_slots)
std::string lastWord = acceptor.call("getLastWord");
POTHOS_TEST_EQUAL(lastWord, "hello");
}

//
// Test utility blocks
//

static Pothos::Object performEval(const Pothos::Proxy& evaluator, const std::string& expr)
{
auto periodicTrigger = Pothos::BlockRegistry::make("/blocks/periodic_trigger");
periodicTrigger.call("setRate", 5); // Triggers per second
periodicTrigger.call("setArgs", std::vector<std::string>{expr});

auto slotToMessage = Pothos::BlockRegistry::make(
"/blocks/slot_to_message",
"handleIt");
auto collectorSink = Pothos::BlockRegistry::make(
"/blocks/collector_sink",
""); // DType irrelevant

{
Pothos::Topology topology;

topology.connect(periodicTrigger, "triggered", evaluator, "setExpression");
topology.connect(evaluator, "triggered", slotToMessage, "handleIt");
topology.connect(slotToMessage, 0, collectorSink, 0);

// Since periodic_trigger will trigger 5 times/second, half a second
// should be enough to get at least one.
topology.commit();
Poco::Thread::sleep(500); // ms
}

auto collectorSinkObjs = collectorSink.call<Pothos::ObjectVector>("getMessages");
POTHOS_TEST_FALSE(collectorSinkObjs.empty());

return collectorSinkObjs[0];
}

POTHOS_TEST_BLOCK("/proxy/python/tests", test_evaluator)
{
constexpr auto jsonStr = "{\"outer\": [{\"inner\": [400,300,200,100]}, {\"inner\": [0.1,0.2,0.3,0.4]}]}";
constexpr auto inner0Index = 3;
constexpr auto inner1Index = 2;
const auto localDoubles = std::vector<double>{12.3456789, 0.987654321};
const std::complex<double> complexArg{1.351, 4.18};
constexpr double doubleArg0 = 1234.0;
constexpr double doubleArg1 = 5678.0;

auto cppJSON = json::parse(jsonStr);

const auto expectedResult = (cppJSON["outer"][0]["inner"][inner0Index].get<double>()
* cppJSON["outer"][1]["inner"][inner1Index].get<double>())
+ std::pow((localDoubles[0] - std::pow(localDoubles[1] + std::abs(complexArg), 2)), 3)
- doubleArg0
+ doubleArg1;

auto evaluator = Pothos::BlockRegistry::make(
"/python/evaluator",
std::vector<std::string>{"inner0Index", "inner1Index", "complexArg", "doubleArg"});
evaluator.call("setInner0Index", inner0Index);
evaluator.call("setInner1Index", inner1Index);
evaluator.call("setComplexArg", complexArg);
evaluator.call("setDoubleArg", doubleArg0, doubleArg1);

auto imports = std::vector<std::string>{"json","math","numpy"};
evaluator.call("setImports", imports);

auto env = Pothos::ProxyEnvironment::make("python");
auto localVars = Pothos::ProxyMap
{
{env->makeProxy("testJSON"), env->makeProxy(jsonStr)},
{env->makeProxy("localDoubles"), env->makeProxy(localDoubles)}
};
evaluator.call("setLocalVars", localVars);

const std::string jsonExpr0 = "json.loads(testJSON)['outer'][0]['inner'][inner0Index]";
const std::string jsonExpr1 = "json.loads(testJSON)['outer'][1]['inner'][inner1Index]";
const std::string powExpr = "numpy.power([localDoubles[0] - math.pow(localDoubles[1] + abs(complexArg), 2)], 3)[0]";
const auto expr = jsonExpr0 + " * " + jsonExpr1 + " + " + powExpr + "- doubleArg0 + doubleArg1";

auto result = performEval(evaluator, expr);
POTHOS_TEST_CLOSE(
expectedResult,
result.convert<double>(),
1e-3);
}

0 comments on commit caed10c

Please sign in to comment.