/*
* opencog/atoms/reduct/NumericFunctionLink.cc
*
* Copyright (C) 2015, 2018, 2021 Linas Vepstas
* All Rights Reserved
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License v3 as
* published by the Free Software Foundation and including the exceptions
* at http://opencog.org/wiki/Licenses
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to:
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <limits>
#include <opencog/util/mt19937ar.h>
#include <opencog/atoms/atom_types/atom_types.h>
#include <opencog/atoms/atom_types/NameServer.h>
#include <opencog/atoms/core/NumberNode.h>
#include "NumericFunctionLink.h"
using namespace opencog;
NumericFunctionLink::NumericFunctionLink(const HandleSeq&& oset, Type t)
: FunctionLink(std::move(oset), t)
{
init();
}
void NumericFunctionLink::init(void)
{
Type t = get_type();
if (NUMERIC_FUNCTION_LINK == t)
throw InvalidParamException(TRACE_INFO,
"NumericFunctionLinks are private and cannot be instantiated.");
if (not nameserver().isA(t, NUMERIC_FUNCTION_LINK))
throw InvalidParamException(TRACE_INFO, "Expecting an NumericFunctionLink");
if (FLOOR_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "FloorLink expects one argument");
else if (HEAVISIDE_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "HeavisideLink expects one argument");
else if (LOG2_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "Log2Link expects one argument");
else if (POW_LINK == t and 2 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "PowLink expects two arguments");
else if (RANDOM_NUMBER_LINK == t and 2 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "RandomNumberLink expects two arguments");
else if (SINE_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "SineLink expects one argument");
else if (COSINE_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "CosineLink expects one argument");
else if (TAN_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "TanLink expects one argument");
else if (EXP_LINK == t and 1 != _outgoing.size())
throw InvalidParamException(TRACE_INFO, "ExpLink expects one argument");
}
// ===========================================================
/// Generic utility -- convert the argument to a vector of doubles,
/// if possible. Return nullptr if not possible.
const std::vector<double>*
NumericFunctionLink::get_vector(AtomSpace* as, bool silent,
ValuePtr vptr, Type& t)
{
t = vptr->get_type();
bool is_fv = nameserver().isA(t, FLOAT_VALUE);
bool is_nu = (NUMBER_NODE == t);
if (not is_fv and not is_nu) return nullptr;
if (is_nu)
return & NumberNodeCast(vptr)->value();
if (is_fv)
return & FloatValueCast(vptr)->value();
return nullptr; // not reached
}
// ============================================================
/// Generic utility -- execute the Handle, and, if that returned
/// a vector of doubles, then apply the function to them.
/// If there wasn't a numeric vectors, return a null pointer.
/// In this last case, the result of reduction is returned
/// in `reduction`
ValuePtr
NumericFunctionLink::apply_func(AtomSpace* as, bool silent,
const Handle& arg,
double (*fun)(double),
ValuePtr& vx)
{
vx = arg->execute(as, silent);
// get_vector gets numeric values, if possible.
Type vxtype;
const std::vector<double>* xvec = get_vector(as, silent, vx, vxtype);
// No numeric values available. Sorry!
if (nullptr == xvec or 0 == xvec->size())
return nullptr;
std::vector<double> funvec;
size_t sz = xvec->size();
for (size_t i=0; i<sz; i++)
funvec.push_back(fun(xvec->operator[](i)));
if (NUMBER_NODE == vxtype)
return createNumberNode(std::move(funvec));
return createFloatValue(std::move(funvec));
}
// ============================================================
/// Generic utility -- execute the HandleSeq, and, if that returned
/// vectors of doubles, then apply the function to them.
/// If there weren't any vectors, return a null pointer.
/// In this last case, the result of reduction is returned
/// in `reduction`
ValuePtr
NumericFunctionLink::apply_func(AtomSpace* as, bool silent,
const HandleSeq& args,
double (*fun)(double, double),
ValueSeq& reduction)
{
ValuePtr vx(args[0]->execute(as, silent));
ValuePtr vy(args[1]->execute(as, silent));
// get_vector gets numeric values, if possible.
Type vxtype;
const std::vector<double>* xvec = get_vector(as, silent, vx, vxtype);
Type vytype;
const std::vector<double>* yvec = get_vector(as, silent, vy, vytype);
// No numeric values available. Sorry!
if (nullptr == xvec or nullptr == yvec or
0 == xvec->size() or 0 == yvec->size())
{
reduction.push_back(vx);
reduction.push_back(vy);
return nullptr;
}
std::vector<double> funvec;
if (1 == xvec->size())
{
double x = xvec->back();
for (double y : *yvec)
funvec.push_back(fun(x, y));
}
else if (1 == yvec->size())
{
double y = yvec->back();
for (double x : *xvec)
funvec.push_back(fun(x, y));
}
else
{
size_t sz = std::min(xvec->size(), yvec->size());
for (size_t i=0; i<sz; i++)
funvec.push_back(fun(xvec->operator[](i), yvec->operator[](i)));
}
if (NUMBER_NODE == vxtype and NUMBER_NODE == vytype)
return createNumberNode(std::move(funvec));
return createFloatValue(std::move(funvec));
}
// ============================================================
static double impulse(double x) { return 1-std::signbit(x); }
static MT19937RandGen randy(616432);
static double get_ran(double lb, double ub)
{
// Linear algebra slope-intercept formula.
return (ub - lb) * randy.randdouble() + lb;
}
/// The various NumericFunctionLinks implement various numeric
/// functions on vector arguments (i.e. on FloatValues and
/// NumberNodes, both of which hold float pt vectors by default.)
///
/// ----
/// The HeavisideLink implements the arithmetic operation of "greater
/// than" on a component-by-component level. That is,
/// Heaviside (a, b, c) (d, e, f) is just (a>d, b>e, c>f).
/// where the comparison is 1.0 if true, else 0.0.
/// Note it returns a FloatValue and NOT a BoolValue!
///
/// ----
/// The Log2Link implements the elementary function of
/// logarithm base two. That is,
/// Log2 (a, b, c) evaluates to (log2(a), log2(b), log2(c)).
///
/// ----
/// The RandomNumberLink returns a NumberNode that lies within the
/// min-max range, using a uniform distribution.
///
/// For example,
///
/// RandomNumberLink
/// NumberNode 0.1
/// NumberNode 0.5
///
/// will return a random number between 0.1 ad 0.5
///
/// It always returns either a NumberNode, or a set of NumberNodes.
/// This is in contrast to a RandomValue, which always returns a
/// vector of doubles (a FloatValue).
///
/// ----
/// The PowLink implements the arithmetic operation of raising
/// an argument to a power. If both arguments are vectors, then
/// they need to be the same size, and the power is computed
/// component by component. That is
/// Pow (a, b, c) (d, e, f) is just (a**d, b**e, c**f).
/// If one of the arguments is a scalar, then that scalar is applied:
/// Pow (a, b, c) n is just (a**n, b**n, c**n).
/// Pow a (p, q, r) is just (a**p, a**q, a**r).
ValuePtr NumericFunctionLink::execute_unary(AtomSpace* as, bool silent)
{
ValuePtr reduction;
ValuePtr result;
Type t = get_type();
if (FLOOR_LINK == t)
result = apply_func(as, silent, _outgoing[0], floor, reduction);
else if (HEAVISIDE_LINK == t)
result = apply_func(as, silent, _outgoing[0], impulse, reduction);
else if (LOG2_LINK == t)
result = apply_func(as, silent, _outgoing[0], log2, reduction);
else if (SINE_LINK == t)
result = apply_func(as, silent, _outgoing[0], sin, reduction);
else if (COSINE_LINK == t)
result = apply_func(as, silent, _outgoing[0], cos, reduction);
else if (TAN_LINK == t)
result = apply_func(as, silent, _outgoing[0], tan, reduction);
else if (EXP_LINK == t)
result = apply_func(as, silent, _outgoing[0], exp, reduction);
else
throw InvalidParamException(TRACE_INFO,
"Internal Error: unhandled derived type!");
if (result) return result;
// No numeric values available. Sorry!
// Return the best-possible reduction that we did get.
if (reduction->is_atom())
return createNumericFunctionLink(
HandleSeq({HandleCast(reduction)}), t);
// Unable to reduce at all. Just return the original atom.
return get_handle();
}
ValuePtr NumericFunctionLink::execute_binary(AtomSpace *as, bool silent)
{
ValueSeq reduction;
ValuePtr result;
Type t = get_type();
if (RANDOM_NUMBER_LINK == t)
result = apply_func(as, silent, _outgoing, get_ran, reduction);
else if (POW_LINK == t)
result = apply_func(as, silent, _outgoing, pow, reduction);
else
throw InvalidParamException(TRACE_INFO,
"Internal Error: unhandled derived type!");
if (result) return result;
// No numeric values available. Sorry!
// Return the best-possible reduction that we did get.
if (reduction[0]->is_atom() and reduction[1]->is_atom())
return createNumericFunctionLink(HandleSeq(
{HandleCast(reduction[0]),
HandleCast(reduction[1])}), t);
// Unable to reduce at all. Just return the original atom.
return get_handle();
}
ValuePtr NumericFunctionLink::execute(AtomSpace* as, bool silent)
{
if (1 == _outgoing.size())
return execute_unary(as, silent);
return execute_binary(as, silent);
}
DEFINE_LINK_FACTORY(NumericFunctionLink, NUMERIC_FUNCTION_LINK);
// ===========================================================