Skip to content

Commit

Permalink
Change function1D & functionLocal signature to return outputs
Browse files Browse the repository at this point in the history
The speed impact seems to be not as great as envisaged and this
signature makes it much simpler to write understandable functions.
Refs #970
  • Loading branch information
martyngigg committed Apr 17, 2013
1 parent afe05e3 commit b0c2197
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ namespace Mantid
/// Base-class method
void function1D(double* out, const double* xValues, const size_t nData) const;
/// Python-type signature
void function1D(const boost::python::object & xvals, boost::python::object & out) const;
boost::python::object function1D(const boost::python::object & xvals) const;
///@}

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ namespace Mantid
/// Implemented Base-class method
void functionLocal(double* out, const double* xValues, const size_t nData) const;
/// Python-type signature for above method
void functionLocal(const boost::python::object & xvals, boost::python::object & out) const;
boost::python::object functionLocal(const boost::python::object & xvals) const;
/// Implemented base-class method
void functionDerivLocal(API::Jacobian* out, const double* xValues, const size_t nData);
/// Python signature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ void export_IFunction1D()
*/
class_<IFunction1D,bases<IFunction>,boost::shared_ptr<IFunction1DAdapter>,
boost::noncopyable>("IFunction1D", "Base class for 1D Fit functions")
.def("function1D", (void (IFunction1DAdapter::*)(const object &,object&)const)&IFunction1DAdapter::function1D,
"Calculate the values of the function for the given x values. The output should be stored in the out array")
.def("function1D", (object (IFunction1DAdapter::*)(const object &)const)&IFunction1DAdapter::function1D,
"Calculate the values of the function for the given x values and returns them")
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ void export_IPeakFunction()
{
class_<IPeakFunction, bases<IFunction1D>, boost::shared_ptr<IPeakFunctionAdapter>,
boost::noncopyable>("IPeakFunction")
.def("functionLocal", (void (IPeakFunctionAdapter::*)(const object &,object&)const)&IPeakFunction::functionLocal,
.def("functionLocal", (object (IPeakFunctionAdapter::*)(const object &)const)&IPeakFunction::functionLocal,
"Calculate the values of the function for the given x values. The output should be stored in the out array")
;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include "MantidPythonInterface/kernel/Environment/WrapperHelpers.h"

#include <boost/python/class.hpp>
#define PY_ARRAY_UNIQUE_SYMBOL KERNEL_ARRAY_API
#define NO_IMPORT_ARRAY
#include <numpy/arrayobject.h>

//-----------------------------------------------------------------------------
// IFunction1D definition
Expand All @@ -13,7 +16,7 @@ namespace Mantid
{
namespace PythonInterface
{
using Environment::CallMethod2;
using Environment::CallMethod1;
using namespace boost::python;

/**
Expand All @@ -33,27 +36,43 @@ namespace Mantid
*/
void IFunction1DAdapter::function1D(double* out, const double* xValues, const size_t nData) const
{
using namespace Converters;
// GIL must be held while numpy wrappers are destroyed as they access Python
// state information
Environment::GlobalInterpreterLock gil;

Py_intptr_t dims[1] = { static_cast<Py_intptr_t>(nData) } ;
object xvals = object(handle<>(Converters::WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims)));
object outnp = object(handle<>(Converters::WrapReadWrite::apply<double>::createFromArray(out, 1,dims)));
Py_intptr_t dims[1] = { static_cast<Py_intptr_t>(nData) };
PyObject *xvals = WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims);

// Deliberately avoids using the CallMethod wrappers. They lock the GIL again and
// will check for each function call whether the wrapped method exists.
boost::python::call_method<void,object,object>(getSelf(), "function1D", xvals, outnp);
// will check for each function call whether the wrapped method exists. It also avoid unnecessary construction of
// boost::python::objects whn using boost::python::call_method

PyObject *result = PyEval_CallMethod(getSelf(), "function1D", "(O)", xvals);
PyArrayObject *nparray = (PyArrayObject *)(result);

if(PyArray_TYPE(nparray) == NPY_DOUBLE) // dtype matches so use memcpy for speed
{
std::memcpy(static_cast<void*>(out), PyArray_DATA(nparray), nData*sizeof(npy_double));
}
else
{
PyArray_Descr *dtype=PyArray_DESCR(nparray);
PyObject *name = PyList_GetItem(dtype->names, 0);
std::ostringstream os;
os << "Unsupported numpy data type: '" << PyString_AsString(name) << "'. Currently only numpy.float64 is supported";
throw std::runtime_error(os.str());
}
}

/**
* Python-type signature version of above to be called directly from Python
* @param xvals The input X values in read-only numpy array
* @param out A read/write numpy array of doubles to store the results
*/
void IFunction1DAdapter::function1D(const boost::python::object & xvals, boost::python::object & out) const
object IFunction1DAdapter::function1D(const object & xvals) const
{
CallMethod2<void,object,object>::dispatchWithException(getSelf(), "function1D", xvals, out);
return CallMethod1<object,object>::dispatchWithException(getSelf(), "function1D", xvals);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
#include "MantidPythonInterface/kernel/Converters/WrapWithNumpy.h"
#include "MantidPythonInterface/kernel/Environment/CallMethod.h"

#include <boost/python/class.hpp>
#include <boost/python/object.hpp>
#define PY_ARRAY_UNIQUE_SYMBOL KERNEL_ARRAY_API
#define NO_IMPORT_ARRAY
#include <numpy/arrayobject.h>


//-----------------------------------------------------------------------------
// IPeakFunction definition
Expand Down Expand Up @@ -88,13 +92,28 @@ namespace Mantid
// state information
Environment::GlobalInterpreterLock gil;

Py_intptr_t dims[1] = { static_cast<Py_intptr_t>(nData) } ;
object xvals = object(handle<>(WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims)));
object outnp = object(handle<>(WrapReadWrite::apply<double>::createFromArray(out, 1,dims)));
Py_intptr_t dims[1] = { static_cast<Py_intptr_t>(nData) };
PyObject *xvals = WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims);

// Deliberately avoids using the CallMethod wrappers. They lock the GIL again and
// will check for each function call whether the wrapped method exists.
boost::python::call_method<void,object,object>(getSelf(), "functionLocal", xvals,outnp);
// will check for each function call whether the wrapped method exists. It also avoid unnecessary construction of
// boost::python::objects whn using boost::python::call_method

PyObject *result = PyEval_CallMethod(getSelf(), "functionLocal", "(O)", xvals);
PyArrayObject *nparray = (PyArrayObject *)(result);

if(PyArray_TYPE(nparray) == NPY_DOUBLE) // dtype matches so use memcpy for speed
{
std::memcpy(static_cast<void*>(out), PyArray_DATA(nparray), nData*sizeof(npy_double));
}
else
{
PyArray_Descr *dtype=PyArray_DESCR(nparray);
PyObject *name = PyList_GetItem(dtype->names, 0);
std::ostringstream os;
os << "Unsupported numpy data type: '" << PyString_AsString(name) << "'. Currently only numpy.float64 is supported";
throw std::runtime_error(os.str());
}
}

/**
Expand All @@ -103,9 +122,9 @@ namespace Mantid
* @param xvals The input X values in read-only numpy array
* @param out A read/write numpy array of doubles to store the results
*/
void IPeakFunctionAdapter::functionLocal(const boost::python::object & xvals, boost::python::object & out) const
object IPeakFunctionAdapter::functionLocal(const boost::python::object & xvals) const
{
CallMethod2<void,object,object>::dispatchWithException(getSelf(), "functionLocal", xvals, out);
return CallMethod1<object,object>::dispatchWithException(getSelf(), "functionLocal", xvals);
}

/**
Expand All @@ -122,16 +141,13 @@ namespace Mantid
Environment::GlobalInterpreterLock gil;

Py_intptr_t dims[1] = { static_cast<Py_intptr_t>(nData) } ;
object xvals = object(handle<>(Converters::WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims)));

// For some reason passing the Jacobian through as a C++ type does not work. There is a runtime error:
// No to_python (by-value) converter found for C++ type: Mantid::API::Jacobian
// So we'll do the work of the wrapper for it
object jacobian = object(handle<>(boost::python::to_python_value<API::Jacobian*>()(out)));
PyObject *xvals = WrapReadOnly::apply<double>::createFromArray(xValues, 1,dims);
PyObject *jacobian = boost::python::to_python_value<API::Jacobian*>()(out);

// Deliberately avoids using the CallMethod wrappers. They lock the GIL again and
// will check for each function call whether the wrapped method exists.
boost::python::call_method<void,object,object>(getSelf(), "functionDerivLocal", xvals,jacobian);
// will check for each function call whether the wrapped method exists. It also avoid unnecessary construction of
// boost::python::objects when using boost::python::call_method
PyEval_CallMethod(getSelf(), "functionDerivLocal", "(OO)", xvals,jacobian);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import unittest
from mantid.api import IFunction1D, IFunction, FunctionFactory
import numpy as np

class PyLinear(IFunction1D):
class Times2(IFunction1D):

def init(self):
self.declareAttribute("IntAtt", 1)
Expand All @@ -13,30 +14,30 @@ def init(self):
self.declareParameter("ParamNoDescr", 1.5)
self.declareParameter("OtherParam",4,"Some fitting parameter")

def function1D(self, xvals, out):
pass
def function1D(self, xvals):
return 2*xvals

class IFunction1DTest(unittest.TestCase):

def test_instance_can_be_created_standalone(self):
func = PyLinear()
func = Times2()
self.assertTrue(isinstance(func, IFunction1D))

def test_instance_can_be_created_from_factory(self):
FunctionFactory.subscribe(PyLinear)
func_name = PyLinear.__name__
FunctionFactory.subscribe(Times2)
func_name = Times2.__name__
func = FunctionFactory.createFunction(func_name)
self.assertTrue(isinstance(func, IFunction1D))
FunctionFactory.unsubscribe(func_name)

def test_declareAttribute_only_accepts_known_types(self):
func = PyLinear()
func = Times2()
func.initialize() # Contains known types
self.assertEquals(4, func.nAttributes()) # Make sure initialize ran
self.assertRaises(ValueError, func.declareAttribute, "ListAtt", [1,2,3])

def test_correct_attribute_values_are_returned_when_asked(self):
func = PyLinear()
func = Times2()
func.initialize() # Contains known types

self.assertEquals(1, func.getAttributeValue("IntAtt"))
Expand All @@ -45,7 +46,7 @@ def test_correct_attribute_values_are_returned_when_asked(self):
self.assertEquals(True, func.getAttributeValue("BoolAtt"))

def test_correct_parameters_are_attached_during_init(self):
func = PyLinear()
func = Times2()
func.initialize()

self.assertEquals(3, func.nParams())
Expand All @@ -62,5 +63,15 @@ def test_correct_parameters_are_attached_during_init(self):
self.assertEquals("Some fitting parameter",func.paramDescription(2))
self.assertEquals(4.0,func.getParameterValue(2))

def test_function1D_can_be_called_directly(self):
func = Times2()
func.initialize()
xvals=np.array([1,2,3])
out = func.function1D(xvals)
self.assertEquals(3, out.shape[0])
self.assertEquals(2, out[0])
self.assertEquals(4, out[1])
self.assertEquals(6, out[2])

if __name__ == '__main__':
unittest.main()

0 comments on commit b0c2197

Please sign in to comment.