diff --git a/include/aidge/aidge.hpp b/include/aidge/aidge.hpp index dc0a12c76c8c72d656229ec90a81f1724f88faf7..32e96ac2886c4956888f0657dbcbced00d76ee78 100644 --- a/include/aidge/aidge.hpp +++ b/include/aidge/aidge.hpp @@ -41,6 +41,7 @@ #include "aidge/operator/AvgPooling.hpp" #include "aidge/operator/BatchNorm.hpp" #include "aidge/operator/BitShift.hpp" +#include "aidge/operator/Clip.hpp" #include "aidge/operator/Concat.hpp" #include "aidge/operator/Conv.hpp" #include "aidge/operator/ConvDepthWise.hpp" diff --git a/include/aidge/operator/Clip.hpp b/include/aidge/operator/Clip.hpp new file mode 100644 index 0000000000000000000000000000000000000000..aa37b50320e9deaa94e83ff7cc3578745b560228 --- /dev/null +++ b/include/aidge/operator/Clip.hpp @@ -0,0 +1,122 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#ifndef AIDGE_CORE_OPERATOR_CLIP_H_ +#define AIDGE_CORE_OPERATOR_CLIP_H_ + +#include <memory> +#include <vector> +#include <limits> + +#include "aidge/backend/OperatorImpl.hpp" +#include "aidge/graph/Node.hpp" +#include "aidge/operator/OperatorTensor.hpp" +#include "aidge/utils/Registrar.hpp" +#include "aidge/utils/StaticAttributes.hpp" +#include "aidge/utils/Types.h" + +namespace Aidge { + +enum class ClipAttr { Min, Max }; + + +class Clip_Op : public OperatorTensor, + public Registrable<Clip_Op, std::string, std::function<std::shared_ptr<OperatorImpl>(const Clip_Op&)>> { + +public: + static const std::string Type; +private: + using Attributes_ = StaticAttributes<ClipAttr, float, float>; + template <ClipAttr e> using attr = typename Attributes_::template attr<e>; + const std::shared_ptr<Attributes_> mAttributes; + +public: + Clip_Op() = delete; + Clip_Op(float min,float max) : + OperatorTensor(Type, {InputCategory::Data,InputCategory::OptionalData,InputCategory::OptionalData}, 1), + mAttributes(std::make_shared<Attributes_>(attr<ClipAttr::Min>(min), attr<ClipAttr::Max>(max))) + {} + /** + * @brief Copy-constructor. Copy the operator attributes and its output tensor(s), but not its input tensors (the new operator has no input associated). + * @param op Operator to copy. + */ + Clip_Op(const Clip_Op& op) + : OperatorTensor(op), + mAttributes(op.mAttributes) + + { + if (op.mImpl){ + SET_IMPL_MACRO(Clip_Op, *this, op.backend()); + }else{ + mImpl = nullptr; + } + } + + /** + * @brief Clone the operator using its copy-constructor. + * @see Operator::Clip_Op + */ + std::shared_ptr<Operator> clone() const override + { + return std::make_shared<Clip_Op>(*this); + } + bool dimsForwarded() const override final; + bool forwardDims(bool allowDataDependency = false) override final; + + /** + * @brief Setter to specify the backend to use + */ + void setBackend(const std::string& name, DeviceIdx_t device = 0) override final; + inline std::shared_ptr<Attributes> attributes() const override { return mAttributes; } + /** + * @brief Getter and Setter for min attribute value + * @return float& + */ + inline float& min() const noexcept { return mAttributes->getAttr<ClipAttr::Min>(); } + /** + * @brief Getter and Setter for max attribute value + * @return float& + */ + inline float& max() const noexcept { return mAttributes->getAttr<ClipAttr::Max>(); } + + std::set<std::string> getAvailableBackends() const override; + + static const std::vector<std::string> getInputsName(){ + return {"data_input","min_empty_tensor","max_empty_tensor"}; + } + static const std::vector<std::string> getOutputsName(){ + return {"data_output"}; + } +}; + /** + * @brief The Clip operator is a tensor operator that performs a clipping operation on tensor elements. + This class allows limiting tensor values to a specified range, defined by the min + and max parameters (Tensors of empty shapes). Values outside this range are replaced by the corresponding limit values + When ‘min’ is greater than ‘max’, the clip operator sets all the ‘input’ values to the value of ‘max’ + * @param[in] name Name of the node + * @param[in] min Min clip value as attribute + * @param[in] max Max clip value as attribute + * @return std::shared_ptr<Node> + */ + std::shared_ptr<Aidge::Node> Clip( + const std::string &name = "", + float min = std::numeric_limits<float>::lowest(), + float max = std::numeric_limits<float>::max() + ); + +} +namespace { +template <> +const char* const EnumStrings<Aidge::ClipAttr>::data[] + = {"min", "max"}; +} + +#endif /* AIDGE_CORE_OPERATOR_CLIP_H_ */ diff --git a/python_binding/operator/pybind_Clip.cpp b/python_binding/operator/pybind_Clip.cpp new file mode 100644 index 0000000000000000000000000000000000000000..27c47811a3c192f1f657f339fe4caf047a944224 --- /dev/null +++ b/python_binding/operator/pybind_Clip.cpp @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#include <pybind11/pybind11.h> + +#include "aidge/data/Tensor.hpp" +#include "aidge/operator/Clip.hpp" +#include "aidge/operator/OperatorTensor.hpp" +#include "aidge/backend/OperatorImpl.hpp" +#include "aidge/utils/Types.h" + +namespace py = pybind11; +namespace Aidge { + +void init_Clip(py::module& m) { + py::class_<Clip_Op, std::shared_ptr<Clip_Op>, OperatorTensor>(m, "ClipOp", py::multiple_inheritance()) + .def(py::init<float, float>(), py::arg("min") = std::numeric_limits<float>::lowest(), py::arg("max") = std::numeric_limits<float>::max()) + .def_static("get_inputs_name", &Clip_Op::getInputsName) + .def_static("get_outputs_name", &Clip_Op::getOutputsName) + .def("min",&Clip_Op::min,py::return_value_policy::reference_internal) + .def("max",&Clip_Op::max,py::return_value_policy::reference_internal); + + + declare_registrable<Clip_Op>(m, "ClipOp"); + + m.def("Clip", &Clip,py::arg("name") = "", + py::arg("min")= std::numeric_limits<float>::lowest(), + py::arg("max")= std::numeric_limits<float>::max(), + R"mydelimiter(ClipOp is a tensor operator that performs a clipping operation on tensor elements. + This class allows limiting tensor values to a specified range, defined by the min + and max parameters. Values outside this range are replaced by the corresponding + limit values. When 'min' is greater than 'max', the clip operator sets all the 'input' values to the value of 'max' + :param min: minimum clipping value. + :type min: float + :param max: maximum clipping value. + :type max: float + :param name: name of the node. + )mydelimiter"); +} +} // namespace Aidge diff --git a/python_binding/pybind_core.cpp b/python_binding/pybind_core.cpp index 6ddfa8cd532b94211d49825f1753c1c745a3e72e..fe471d1228f99754ecc083b07d8d3bfefb792d75 100644 --- a/python_binding/pybind_core.cpp +++ b/python_binding/pybind_core.cpp @@ -35,6 +35,7 @@ void init_Atan(py::module&); void init_AvgPooling(py::module&); void init_BatchNorm(py::module&); void init_BitShift(py::module&); +void init_Clip(py::module&); void init_Concat(py::module&); void init_ConstantOfShape(py::module&); void init_Conv(py::module&); @@ -120,6 +121,7 @@ void init_Aidge(py::module& m) { init_AvgPooling(m); init_BatchNorm(m); init_BitShift(m); + init_Clip(m); init_Concat(m); init_Conv(m); init_ConvDepthWise(m); diff --git a/src/operator/Clip.cpp b/src/operator/Clip.cpp new file mode 100644 index 0000000000000000000000000000000000000000..10b864b54594c86ed1486611fdd91fd916f2291b --- /dev/null +++ b/src/operator/Clip.cpp @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#include "aidge/operator/Clip.hpp" + +#include <memory> +#include <string> +#include "aidge/data/Tensor.hpp" +#include "aidge/utils/Registrar.hpp" +#include "aidge/utils/Types.h" +#include "aidge/operator/Clip.hpp" + +const std::string Aidge::Clip_Op::Type = "Clip"; + +bool Aidge::Clip_Op::dimsForwarded() const { + if ((getInput(1) && !getInput(1)->undefined()) + || (getInput(2) && !getInput(2)->undefined())) + { + // output dims are data dependent + return false; + } + + return OperatorTensor::dimsForwarded(); +} + + +bool Aidge::Clip_Op::forwardDims(bool allowDataDependency) +{ + if (getInput(1) ) + { + if( this->min() != std::numeric_limits<float>::lowest()) + { + Log::notice("{} : ignoring non-empty min attribute because input#1 " + "take precedence", + type()); + } + if (!allowDataDependency) { + Log::warn("{} : unable to forwardDims() because output dims are data " + "dependent on input#1", + type()); + return false; + } + std::shared_ptr<Tensor> fallback; + const auto& minV = mInputs[1]->refCastFrom(fallback, NativeType<float>::type, "cpu"); + this->min() = *(static_cast<float*>(minV.getImpl()->hostPtr())); + } + if (getInput(2)) + { + if( this->max() != std::numeric_limits<float>::max()) + { + Log::notice("{} : ignoring non-empty max attribute because input#2 " + "take precedence", + type()); + } + if (!allowDataDependency) { + Log::warn("{} : unable to forwardDims() because output dims are data " + "dependent on input#2", + type()); + return false; + } + std::shared_ptr<Tensor> fallback; + const auto& maxV = mInputs[2]->refCastFrom(fallback, NativeType<float>::type, "cpu"); + this->max() = *(static_cast<float*>(maxV.getImpl()->hostPtr())); + } + if (!inputsAssociated(false)) { + return false; + } + else if ((getInput(1) && !getInput(1)->empty()) || (getInput(2) && !getInput(2)->empty())) + { + AIDGE_THROW_OR_ABORT(std::runtime_error,"Expected Input#1 and Input#2 to be scalar (Tensors of empty shapes)"); + } + mOutputs[0] -> resize(getInput(0)->dims()); + return true; +} +void Aidge::Clip_Op::setBackend(const std::string& name, Aidge::DeviceIdx_t device) { + mImpl = Registrar<Clip_Op>::create(name)(*this); + mOutputs[0]->setBackend(name, device); +} +std::set<std::string> Aidge::Clip_Op::getAvailableBackends() const { + return Registrar<Clip_Op>::getKeys(); +} +std::shared_ptr<Aidge::Node> Aidge::Clip(const std::string &name,float min,float max) +{ + return std::make_shared<Node>(std::make_shared<Clip_Op>(min, max), name); +} \ No newline at end of file diff --git a/unit_tests/operator/Test_Clip_Op.cpp b/unit_tests/operator/Test_Clip_Op.cpp new file mode 100644 index 0000000000000000000000000000000000000000..51d6d248067c89be9703fd4d48f02347f285330a --- /dev/null +++ b/unit_tests/operator/Test_Clip_Op.cpp @@ -0,0 +1,110 @@ +/******************************************************************************** + * Copyright (c) 2023 CEA-List + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + ********************************************************************************/ + +#include <catch2/catch_test_macros.hpp> +#include <catch2/generators/catch_generators_random.hpp> +#include <cstddef> // std::size_t +#include <memory> +#include <random> // std::mt19937, std::uniform_int_distribution +#include <vector> + +#include "aidge/data/Tensor.hpp" +#include "aidge/operator/Clip.hpp" +#include "aidge/operator/OperatorTensor.hpp" + +namespace Aidge { +TEST_CASE("[core/operator] Clip_Op(forwardDims)", "[Clip][forwardDims]") { + constexpr std::uint16_t NBTRIALS = 10; + + // Create a random number generator + auto rd = Catch::Generators::Detail::getSeed; + std::mt19937 gen(rd()); + std::uniform_int_distribution<std::size_t> dimsDist(1, 10); + std::uniform_int_distribution<std::size_t> nbDimsDist(1, 5); + + // Create Clip Operator + std::shared_ptr<Node> myClip = Clip(); + auto op = std::static_pointer_cast<OperatorTensor>(myClip -> getOperator()); + + // Input tensor + std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>(Array2D<int,2,2> { + { + {1, 2}, + {3, 4} + } + }); + op -> associateInput(0,T0); + // Tensor representign Min value + std::shared_ptr<Tensor> Tmin = std::make_shared<Tensor>(2.0); + op -> associateInput(1,Tmin); + + // Tensor representign Max value + std::shared_ptr<Tensor> Tmax = std::make_shared<Tensor>(4.0); + op -> associateInput(2,Tmax); + /** + * @todo Special case: scalar not handled yet by + * ``OperatorTensor::forwardDims()`` + */ + SECTION("Scalar") { + //We set every Input as a Scalar + T0->resize({}); + Tmin->resize({}); + Tmax->resize({}); + + REQUIRE_NOTHROW(op->forwardDims(true)); + REQUIRE((op->getOutput(0)->dims() == std::vector<std::size_t>())); + } + SECTION("Normal Input") { + // a scalar is compatible with any other Tensor + // input_0 + std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>(Array2D<int,2,2> { + { + {1, 2}, + {3, 4} + } + }); + const std::size_t nb_dims = nbDimsDist(gen); + std::vector<std::size_t> dims(nb_dims); + for (std::size_t i = 0; i < nb_dims; ++i) + { + dims[i] = dimsDist(gen); + } + T0->resize(dims); + op->associateInput(0,T0); + REQUIRE_NOTHROW(op->forwardDims(true)); + REQUIRE((op->getOutput(0)->dims()) == dims); + } + + SECTION("Min and max attributes ") + { + std::shared_ptr<Node> clip_attr = Clip("",-1.0,2.0); + auto opc = std::static_pointer_cast<OperatorTensor>(clip_attr -> getOperator()); + std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>(); + opc -> associateInput(0,T0); + std::shared_ptr<Tensor> Tmin = std::make_shared<Tensor>(7); + opc-> associateInput(1,Tmin); + std::shared_ptr<Tensor> Tmax = std::make_shared<Tensor>(4); + opc -> associateInput(2,Tmax); + + REQUIRE_NOTHROW(opc->forwardDims(true)); + REQUIRE((opc->getOutput(0)->dims() == std::vector<std::size_t>())); + } + SECTION("Min and max attributes (No Input for min and max)") + { + std::shared_ptr<Node> clip = Clip("",-1.0,2.0); + auto opcl = std::static_pointer_cast<OperatorTensor>(clip -> getOperator()); + std::shared_ptr<Tensor> T0 = std::make_shared<Tensor>(); + opcl -> associateInput(0,T0); + REQUIRE_NOTHROW(opcl->forwardDims()); + REQUIRE((opcl->getOutput(0)->dims() == std::vector<std::size_t>())); + } +} +} // namespace Aidge