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

Added Python evaluator block #25

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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);
}