Skip to content

Commit caed10c

Browse files
committed
Added Python evaluator block
* Closes #20
1 parent 5ae94e2 commit caed10c

File tree

6 files changed

+281
-2
lines changed

6 files changed

+281
-2
lines changed

Blocks/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
########################################################################
2+
## Build and install
3+
########################################################################
4+
POTHOS_PYTHON_UTIL(
5+
TARGET PythonBlocks
6+
SOURCES
7+
__init__.py
8+
Evaluator.py
9+
FACTORIES
10+
"/python/evaluator:Evaluator"
11+
DESTINATION PothosBlocks
12+
ENABLE_DOCS
13+
)

Blocks/Evaluator.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright (c) 2020-2021 Nicholas Corgan
2+
# SPDX-License-Identifier: BSL-1.0
3+
4+
from functools import partial
5+
import importlib
6+
7+
import Pothos
8+
9+
"""
10+
/***********************************************************************
11+
* |PothosDoc Python Evaluator
12+
*
13+
* The Python evaluator block performs a user-specified expression evaluation
14+
* on input slot(s) and produces the evaluation result on an output signal.
15+
* The input slots are user-defined. The output signal is named "triggered".
16+
* The arguments from the input slots must be primitive types.
17+
*
18+
* |category /Event
19+
* |keywords signal slot eval expression
20+
*
21+
* |param imports[Imports] A list of Python modules to import before executing the expression.
22+
* Example: ["math", "numpy"] will import the math and numpy modules.
23+
* |default ["math"]
24+
*
25+
* |param args[Arguments] A list of named variables to use in the expression.
26+
* Each variable corresponds to settings slot on the transform block.
27+
* Example: ["foo", "bar"] will create the slots "setFoo" and "setBar".
28+
* |default ["val"]
29+
*
30+
* |param expr[Expression] The expression to re-evaluate for each slot event.
31+
* An expression is valid Python, comprised of combinations of variables, constants, and math functions.
32+
* Example: math.log2(foo)/bar
33+
*
34+
* <p><b>Multi-argument input:</b> Upstream blocks may pass multiple arguments to a slot.
35+
* Each argument will be available to the expression suffixed by its argument index.
36+
* For example, suppose that the slot "setBaz" has two arguments,
37+
* then the following expression would use both arguments: "baz0 + baz1"</p>
38+
*
39+
* |default "math.log2(val)"
40+
* |widget StringEntry()
41+
*
42+
* |param localVars[LocalVars] A map of variable names to values.
43+
* This allows you to use global variables from the topology in the expression.
44+
*
45+
* For example this mapping lets us use foo, bar, and baz in the expression
46+
* to represent several different globals and combinations of expressions:
47+
* {"foo": myGlobal, "bar": "test123", "baz": myNum+12345}
48+
* |default {}
49+
* |preview valid
50+
*
51+
* |factory /python/evaluator(args)
52+
* |setter setExpression(expr)
53+
* |setter setImports(imports)
54+
* |setter setLocalVars(localVars)
55+
**********************************************************************/
56+
"""
57+
class Evaluator(Pothos.Block):
58+
def __init__(self, varNames):
59+
Pothos.Block.__init__(self)
60+
self.setName("/python/evaluator")
61+
62+
self.__checkIsStringList(varNames)
63+
64+
self.__expr = ""
65+
self.__localVars = dict()
66+
self.__varNames = varNames
67+
self.__varValues = dict()
68+
self.__imports = []
69+
self.__varsReady = set()
70+
71+
# Add setters for user variables
72+
for name in self.__varNames:
73+
if not name:
74+
continue
75+
76+
setterName = "set"+name[0].upper()+name[1:]
77+
setattr(Evaluator, setterName, partial(self.__setter, name))
78+
self.registerSlot(setterName)
79+
80+
self.registerSlot("setExpression")
81+
self.registerSlot("setImports")
82+
self.registerSlot("setLocalVars")
83+
self.registerSignal("triggered")
84+
85+
def getExpression(self):
86+
return self.__expr
87+
88+
def setExpression(self,expr):
89+
self.__checkIsStr(expr)
90+
self.__expr = expr
91+
92+
notReadyVars = [var for var in self.__varNames if var not in self.__varsReady]
93+
if notReadyVars:
94+
return
95+
96+
args = self.__performEval()
97+
self.triggered(args)
98+
99+
def setImports(self,imports):
100+
self.__checkIsStringOrStringList(imports)
101+
self.__imports = imports if (type(imports) == list) else [imports]
102+
103+
def getImports(self):
104+
return self.__imports
105+
106+
def setLocalVars(self,userLocalVars):
107+
self.__checkIsDict(userLocalVars)
108+
self.__localVars = userLocalVars
109+
110+
#
111+
# Private utility functions
112+
#
113+
114+
def __performEval(self):
115+
for key,val in self.__varValues.items():
116+
locals()[key] = val
117+
118+
for mod in self.__imports:
119+
exec("import "+mod)
120+
121+
for key,val in self.__localVars.items():
122+
locals()[key] = val
123+
124+
return eval(self.__expr)
125+
126+
def __setter(self,field,*args):
127+
if len(args) > 1:
128+
for i in range(len(args)):
129+
self.__varValues[field+str(i)] = args[i]
130+
else:
131+
self.__varValues[field] = args[0]
132+
133+
self.__varsReady.add(field)
134+
135+
notReadyVars = [var for var in self.__varNames if var not in self.__varsReady]
136+
if (not notReadyVars) and self.__expr:
137+
args = self.__performEval()
138+
self.triggered(args)
139+
140+
return None
141+
142+
def __checkIsStr(self,var):
143+
if type(var) != str:
144+
raise ValueError("The given value must be a str. Found {0}".format(type(var)))
145+
146+
def __checkIsDict(self,var):
147+
if type(var) != dict:
148+
raise ValueError("The given value must be a dict. Found {0}".format(type(var)))
149+
150+
def __checkIsList(self,var):
151+
if type(var) != list:
152+
raise ValueError("The given value must be a list. Found {0}".format(type(var)))
153+
154+
def __checkIsStringList(self,var):
155+
self.__checkIsList(var)
156+
157+
nonStringVals = [x for x in var if type(x) != str]
158+
if nonStringVals:
159+
raise ValueError("All list values must be strings. Found {0}".format(type(nonStringVals[0])))
160+
161+
def __checkIsStringOrStringList(self,var):
162+
if type(var) is str:
163+
return
164+
elif type(var) is list:
165+
self.__checkIsStringList(var)
166+
else:
167+
raise ValueError("The given value must be a string or list. Found {0}".format(type(var)))

Blocks/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (c) 2020-2021 Nicholas Corgan
2+
# SPDX-License-Identifier: BSL-1.0
3+
4+
from . Evaluator import Evaluator

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,5 @@ install(
141141
# Enter the subdirectory configuration
142142
########################################################################
143143
add_subdirectory(Pothos)
144+
add_subdirectory(Blocks)
144145
add_subdirectory(TestBlocks)

Pothos/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Copyright (c) 2014-2016 Josh Blum
2-
# 2019 Nicholas Corgan
2+
# 2019-2020 Nicholas Corgan
33
# SPDX-License-Identifier: BSL-1.0
44

55
from . PothosModule import *

TestPythonBlock.cpp

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
// Copyright (c) 2014-2017 Josh Blum
2+
// 2020-2021 Nicholas Corgan
23
// SPDX-License-Identifier: BSL-1.0
34

45
#include <Pothos/Framework.hpp>
56
#include <Pothos/Managed.hpp>
67
#include <Pothos/Testing.hpp>
78
#include <Pothos/Proxy.hpp>
8-
#include <iostream>
9+
910
#include <json.hpp>
1011

12+
#include <Poco/Path.h>
13+
#include <Poco/TemporaryFile.h>
14+
#include <Poco/Thread.h>
15+
16+
#include <complex>
17+
#include <cmath>
18+
#include <iostream>
19+
1120
using json = nlohmann::json;
1221

1322
POTHOS_TEST_BLOCK("/proxy/python/tests", python_module_import)
@@ -68,3 +77,88 @@ POTHOS_TEST_BLOCK("/proxy/python/tests", test_signals_and_slots)
6877
std::string lastWord = acceptor.call("getLastWord");
6978
POTHOS_TEST_EQUAL(lastWord, "hello");
7079
}
80+
81+
//
82+
// Test utility blocks
83+
//
84+
85+
static Pothos::Object performEval(const Pothos::Proxy& evaluator, const std::string& expr)
86+
{
87+
auto periodicTrigger = Pothos::BlockRegistry::make("/blocks/periodic_trigger");
88+
periodicTrigger.call("setRate", 5); // Triggers per second
89+
periodicTrigger.call("setArgs", std::vector<std::string>{expr});
90+
91+
auto slotToMessage = Pothos::BlockRegistry::make(
92+
"/blocks/slot_to_message",
93+
"handleIt");
94+
auto collectorSink = Pothos::BlockRegistry::make(
95+
"/blocks/collector_sink",
96+
""); // DType irrelevant
97+
98+
{
99+
Pothos::Topology topology;
100+
101+
topology.connect(periodicTrigger, "triggered", evaluator, "setExpression");
102+
topology.connect(evaluator, "triggered", slotToMessage, "handleIt");
103+
topology.connect(slotToMessage, 0, collectorSink, 0);
104+
105+
// Since periodic_trigger will trigger 5 times/second, half a second
106+
// should be enough to get at least one.
107+
topology.commit();
108+
Poco::Thread::sleep(500); // ms
109+
}
110+
111+
auto collectorSinkObjs = collectorSink.call<Pothos::ObjectVector>("getMessages");
112+
POTHOS_TEST_FALSE(collectorSinkObjs.empty());
113+
114+
return collectorSinkObjs[0];
115+
}
116+
117+
POTHOS_TEST_BLOCK("/proxy/python/tests", test_evaluator)
118+
{
119+
constexpr auto jsonStr = "{\"outer\": [{\"inner\": [400,300,200,100]}, {\"inner\": [0.1,0.2,0.3,0.4]}]}";
120+
constexpr auto inner0Index = 3;
121+
constexpr auto inner1Index = 2;
122+
const auto localDoubles = std::vector<double>{12.3456789, 0.987654321};
123+
const std::complex<double> complexArg{1.351, 4.18};
124+
constexpr double doubleArg0 = 1234.0;
125+
constexpr double doubleArg1 = 5678.0;
126+
127+
auto cppJSON = json::parse(jsonStr);
128+
129+
const auto expectedResult = (cppJSON["outer"][0]["inner"][inner0Index].get<double>()
130+
* cppJSON["outer"][1]["inner"][inner1Index].get<double>())
131+
+ std::pow((localDoubles[0] - std::pow(localDoubles[1] + std::abs(complexArg), 2)), 3)
132+
- doubleArg0
133+
+ doubleArg1;
134+
135+
auto evaluator = Pothos::BlockRegistry::make(
136+
"/python/evaluator",
137+
std::vector<std::string>{"inner0Index", "inner1Index", "complexArg", "doubleArg"});
138+
evaluator.call("setInner0Index", inner0Index);
139+
evaluator.call("setInner1Index", inner1Index);
140+
evaluator.call("setComplexArg", complexArg);
141+
evaluator.call("setDoubleArg", doubleArg0, doubleArg1);
142+
143+
auto imports = std::vector<std::string>{"json","math","numpy"};
144+
evaluator.call("setImports", imports);
145+
146+
auto env = Pothos::ProxyEnvironment::make("python");
147+
auto localVars = Pothos::ProxyMap
148+
{
149+
{env->makeProxy("testJSON"), env->makeProxy(jsonStr)},
150+
{env->makeProxy("localDoubles"), env->makeProxy(localDoubles)}
151+
};
152+
evaluator.call("setLocalVars", localVars);
153+
154+
const std::string jsonExpr0 = "json.loads(testJSON)['outer'][0]['inner'][inner0Index]";
155+
const std::string jsonExpr1 = "json.loads(testJSON)['outer'][1]['inner'][inner1Index]";
156+
const std::string powExpr = "numpy.power([localDoubles[0] - math.pow(localDoubles[1] + abs(complexArg), 2)], 3)[0]";
157+
const auto expr = jsonExpr0 + " * " + jsonExpr1 + " + " + powExpr + "- doubleArg0 + doubleArg1";
158+
159+
auto result = performEval(evaluator, expr);
160+
POTHOS_TEST_CLOSE(
161+
expectedResult,
162+
result.convert<double>(),
163+
1e-3);
164+
}

0 commit comments

Comments
 (0)