diff --git a/compiler/circle-resizer/CMakeLists.txt b/compiler/circle-resizer/CMakeLists.txt index 5f27b90e3f2..df1a6e31ce2 100644 --- a/compiler/circle-resizer/CMakeLists.txt +++ b/compiler/circle-resizer/CMakeLists.txt @@ -1 +1,3 @@ +add_subdirectory(src) add_subdirectory(app) +add_subdirectory(tests) diff --git a/compiler/circle-resizer/include/Dim.h b/compiler/circle-resizer/include/Dim.h new file mode 100644 index 00000000000..22d651c16e6 --- /dev/null +++ b/compiler/circle-resizer/include/Dim.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_RESIZER_DIM_H__ +#define __CIRCLE_RESIZER_DIM_H__ + +#include + +namespace circle_resizer +{ + +/** + * The representation of a single dimension. Note that a dimension can be dynamic. + */ +class Dim +{ +public: + /** + * @brief Initialize single dimension. Note that '-1' means a dynamic dimension. + * + * Exceptions: + * - std::runtime_error if provided dim value is less than -1. + */ + explicit Dim(int32_t dim); + + /** + * @brief Create dynamic dimension. Note that it's equivalent of Dim{-1}. + */ + static Dim dynamic(); + +public: + /** + * @brief Returns true if the dimension is dynamic. Otherwise, return false. + */ + bool is_dynamic() const; + + /** + * @brief Returns value of dimension in int32_t representation. + */ + int32_t value() const; + + /** + * @brief Returns true of the current dimension and the provided rhs are equal. + */ + bool operator==(const Dim &rhs) const; + +private: + // Note that in the future, we might need to support dimension with lower and upper bounds + int32_t _dim_value; +}; + +} // namespace circle_resizer + +#endif // __CIRCLE_RESIZER_DIM_H__ diff --git a/compiler/circle-resizer/include/Shape.h b/compiler/circle-resizer/include/Shape.h new file mode 100644 index 00000000000..5b5d6debdba --- /dev/null +++ b/compiler/circle-resizer/include/Shape.h @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_RESIZER_SHAPE_H__ +#define __CIRCLE_RESIZER_SHAPE_H__ + +#include "Dim.h" + +#include +#include + +namespace circle_resizer +{ + +/** + * The representation of a single shape. + */ +class Shape +{ +public: + /** + * @brief Initialize shape with initializer list of dims. + */ + Shape(const std::initializer_list &dims); + + /** + * @brief Initialize shape with vector of dims. + */ + Shape(const std::vector &shape_vec); + + /** + * @brief Initialize static shape with initializer list of of uint32_t values. + * + * Exceptions: + * - std::out_of_range if some elements in shape_vec exceed int32_t range. + */ + Shape(const std::initializer_list &shape_vec); + + /** + * @brief Create scalar shape. Note, that the same can be achieved with Shape{}. + */ + static Shape scalar(); + +public: + /** + * @brief Returns number of dimensions in the shape. + */ + size_t rank() const; + + /** + * @brief Returns dimension of the position determined by axis. + * + * Exceptions: + * - std::invalid_argument if the method is called on a scalar shape. + * - std::out_of_range if the provided axis is greater than rank. + */ + Dim operator[](const size_t &axis) const; + + /** + * @brief Returns true if the shape is a scalar. Otherwise, return false. + */ + bool is_scalar() const; + + /** + * @brief Returns true if all dimensions in the shape are static or the shape is a scalar. + * Otherwise, return false. + */ + bool is_dynamic() const; + + /** + * @brief Returns true of the current shape and the provided rhs are equal. + */ + bool operator==(const Shape &rhs) const; + + /** + * @brief Print the shape in format [1, 2, 3]. + */ + friend std::ostream &operator<<(std::ostream &os, const Shape &shape); + +private: + std::vector _dims; +}; + +} // namespace circle_resizer + +#endif // __CIRCLE_RESIZER_SHAPE_H__ diff --git a/compiler/circle-resizer/include/ShapeParser.h b/compiler/circle-resizer/include/ShapeParser.h new file mode 100644 index 00000000000..e1af93b9526 --- /dev/null +++ b/compiler/circle-resizer/include/ShapeParser.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CIRCLE_RESIZER_SHAPE_PARSER_H__ +#define __CIRCLE_RESIZER_SHAPE_PARSER_H__ + +#include "Shape.h" + +#include +#include + +namespace circle_resizer +{ + +/** + * @brief Parse shapes from string representation to Shapes object. + * + * The single shape is represented by comma-separated integers inside squared brackets. + * If there is more than one shape, they are separated by commas. + * An example for single shape: [1,2,3], an example for many shapes: [1,2,3],[4,5]. + * + * Exceptions: + * std::invalid_argument if the parsing failed. + */ +std::vector parse_shapes(const std::string &shapes); + +} // namespace circle_resizer + +#endif // __CIRCLE_RESIZER_SHAPE_PARSER_H__ diff --git a/compiler/circle-resizer/src/CMakeLists.txt b/compiler/circle-resizer/src/CMakeLists.txt new file mode 100644 index 00000000000..03f5a2dba14 --- /dev/null +++ b/compiler/circle-resizer/src/CMakeLists.txt @@ -0,0 +1,9 @@ +list(APPEND CIRCLE_RESIZER_CORE_SOURCES Dim.cpp) +list(APPEND CIRCLE_RESIZER_CORE_SOURCES Shape.cpp) +list(APPEND CIRCLE_RESIZER_CORE_SOURCES ShapeParser.cpp) + +add_library(circle_resizer_core STATIC "${CIRCLE_RESIZER_CORE_SOURCES}") + +target_include_directories(circle_resizer_core PUBLIC ../include) + +install(TARGETS circle_resizer_core DESTINATION lib) diff --git a/compiler/circle-resizer/src/Dim.cpp b/compiler/circle-resizer/src/Dim.cpp new file mode 100644 index 00000000000..676dd9d0036 --- /dev/null +++ b/compiler/circle-resizer/src/Dim.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Dim.h" + +#include + +using namespace circle_resizer; + +Dim::Dim(int32_t dim) : _dim_value{dim} +{ + if (dim < -1) + { + throw std::runtime_error("Invalid value of dimension: " + dim); + } +} + +Dim Dim::dynamic() { return Dim{-1}; } + +bool Dim::is_dynamic() const { return _dim_value == -1; } + +int32_t Dim::value() const { return _dim_value; } + +bool Dim::operator==(const Dim &rhs) const { return value() == rhs.value(); } diff --git a/compiler/circle-resizer/src/Shape.cpp b/compiler/circle-resizer/src/Shape.cpp new file mode 100644 index 00000000000..e5c129af771 --- /dev/null +++ b/compiler/circle-resizer/src/Shape.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Shape.h" + +#include +#include + +using namespace circle_resizer; + +Shape::Shape(const std::initializer_list &dims) : _dims{dims} {} + +Shape::Shape(const std::vector &shape_vec) : _dims{shape_vec} {} + +Shape::Shape(const std::initializer_list &shape_vec) +{ + for (const auto &dim : shape_vec) + { + if (dim >= std::numeric_limits::max()) + { + std::out_of_range("Provided dimension: " + std::to_string(dim) + " is out of range"); + } + _dims.emplace_back(Dim{static_cast(dim)}); + } +} + +Shape Shape::scalar() { return Shape{std::initializer_list{}}; } + +size_t Shape::rank() const { return _dims.size(); } + +Dim Shape::operator[](const size_t &axis) const +{ + if (is_scalar()) + { + throw std::invalid_argument("You cannot gather dimension from a scalar"); + } + if (axis > rank() - 1) + { + throw std::out_of_range("Axis=" + std::to_string(axis) + + " is out of range of shape's rank: " + std::to_string(rank())); + } + return _dims[axis]; +} + +bool Shape::is_scalar() const { return _dims.empty(); } + +bool Shape::is_dynamic() const +{ + if (is_scalar()) + { + return false; + } + return std::any_of(std::begin(_dims), std::end(_dims), + [](const Dim &dim) { return dim.is_dynamic(); }); +} + +bool Shape::operator==(const Shape &rhs) const +{ + if (rank() != rhs.rank()) + { + return false; + } + for (size_t axis = 0; axis < rank(); ++axis) + { + if (_dims[axis].value() != rhs[axis].value()) + { + return false; + } + } + return true; +} + +std::ostream &circle_resizer::operator<<(std::ostream &os, const Shape &shape) +{ + if (shape.is_scalar()) + { + os << "[]"; + return os; + } + os << "["; + for (int i = 0; i < shape.rank() - 1; ++i) + { + os << shape[i].value() << ", "; + } + os << shape[shape.rank() - 1].value() << "]"; + return os; +} diff --git a/compiler/circle-resizer/src/ShapeParser.cpp b/compiler/circle-resizer/src/ShapeParser.cpp new file mode 100644 index 00000000000..baa189f4160 --- /dev/null +++ b/compiler/circle-resizer/src/ShapeParser.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ShapeParser.h" +#include + +#include +#include +#include + +using namespace circle_resizer; + +namespace +{ + +bool is_blank(const std::string &s) +{ + return !s.empty() && std::find_if(s.begin(), s.end(), + [](unsigned char c) { return !std::isblank(c); }) == s.end(); +} + +Shape parse_single_shape(const std::string &shape) +{ + if (shape.empty() || is_blank(shape)) + { + return Shape::scalar(); + } + + std::vector result_dims; + std::stringstream shape_stream(shape); + std::string token; + try + { + while (std::getline(shape_stream, token, ',')) + { + result_dims.push_back(Dim{std::stoi(token)}); + } + } + catch (...) + { + throw std::invalid_argument("Error during shape processing: " + shape); + } + if (result_dims.empty()) + { + throw std::invalid_argument("No shapes found in input string: " + shape); + } + return Shape{result_dims}; +} + +} // namespace + +std::vector circle_resizer::parse_shapes(const std::string &shapes) +{ + std::vector result_shapes; + auto shapes_tmp = shapes; + std::string token; + size_t begin_pos = 0, end_pos = 0; + while ((begin_pos = shapes_tmp.find_first_of("[")) != std::string::npos && + (end_pos = shapes_tmp.find_first_of("]")) != std::string::npos) + { + if (begin_pos > end_pos) + { + throw std::invalid_argument("Invalid shape format: " + shapes); + } + const size_t token_size = end_pos - begin_pos - 1; + token = shapes_tmp.substr(begin_pos + 1, token_size); + result_shapes.push_back(parse_single_shape(token)); + shapes_tmp.erase(0, end_pos + 1); + } + + if (result_shapes.empty()) + { + throw std::invalid_argument("No shapes found in the input string: " + shapes); + } + + // the rest of the input not handled by loop above cannot be processed properly + if (shapes_tmp.size() > 0 && !is_blank(shapes_tmp)) + { + throw std::invalid_argument("The part of input shapes: " + shapes_tmp + " cannot be processed"); + } + + return result_shapes; +} diff --git a/compiler/circle-resizer/tests/CMakeLists.txt b/compiler/circle-resizer/tests/CMakeLists.txt new file mode 100644 index 00000000000..6f450467537 --- /dev/null +++ b/compiler/circle-resizer/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +if(NOT ENABLE_TEST) + return() +endif(NOT ENABLE_TEST) + +list(APPEND CIRCLE_RESIZER_TEST_SOURCES Shape.test.cpp) +list(APPEND CIRCLE_RESIZER_TEST_SOURCES ShapeParser.test.cpp) + +nnas_find_package(GTest REQUIRED) +GTest_AddTest(circle_resizer_unit_test ${CIRCLE_RESIZER_TEST_SOURCES}) +target_link_libraries(circle_resizer_unit_test circle_resizer_core) diff --git a/compiler/circle-resizer/tests/Shape.test.cpp b/compiler/circle-resizer/tests/Shape.test.cpp new file mode 100644 index 00000000000..795bec01690 --- /dev/null +++ b/compiler/circle-resizer/tests/Shape.test.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Shape.h" + +#include + +using namespace circle_resizer; + +TEST(ShapeTest, create_scalar) +{ + auto const scalar = Shape::scalar(); + EXPECT_TRUE(scalar.is_scalar()); + EXPECT_EQ(scalar.rank(), 0); +} + +TEST(ShapeTest, gather_scalar_NEG) +{ + auto const scalar = Shape::scalar(); + EXPECT_THROW(scalar[0], std::invalid_argument); +} + +TEST(ShapeTest, is_dynamic) +{ + EXPECT_FALSE(Shape::scalar().is_dynamic()); + + const auto shape1 = Shape{1, 2}; + EXPECT_FALSE(shape1.is_dynamic()); + + const auto shape2 = Shape{Dim{1}, Dim::dynamic()}; + EXPECT_TRUE(shape2.is_dynamic()); + + const auto shape3 = Shape{Dim::dynamic(), Dim::dynamic()}; + EXPECT_TRUE(shape3.is_dynamic()); + + const auto shape4 = Shape{2}; + EXPECT_FALSE(shape4.is_dynamic()); +} + +TEST(ShapeTest, index_operator_NEG) +{ + const auto shape = Shape{Dim{1}, Dim::dynamic()}; + EXPECT_THROW(shape[2], std::out_of_range); +} + +TEST(ShapeTest, equal_operator) +{ + // static vs static with other rank + auto shape1 = Shape{1, 2, 3}; + auto shape2 = Shape{1, 2}; + EXPECT_FALSE(shape1 == shape2); + + // different static vs static + shape1 = Shape{1, 2, 3}; + shape2 = Shape{1, 3, 3}; + EXPECT_FALSE(shape1 == shape2); + + // the same dynamic vs dynamic + shape1 = Shape{Dim::dynamic(), Dim::dynamic()}; + shape2 = Shape{Dim::dynamic(), Dim::dynamic()}; + EXPECT_TRUE(shape1 == shape2); + + shape1 = Shape{Dim::dynamic(), Dim{2}}; + shape2 = Shape{Dim::dynamic(), Dim{2}}; + EXPECT_TRUE(shape1 == shape2); + + // different dynamic vs dynamic + shape1 = Shape{Dim::dynamic(), Dim::dynamic()}; + shape2 = Shape{Dim::dynamic(), Dim{2}}; + EXPECT_FALSE(shape1 == shape2); + + // static vs dynamic + shape1 = Shape{Dim::dynamic(), Dim::dynamic()}; + shape2 = Shape{1, 2}; + EXPECT_FALSE(shape1 == shape2); + + // scalar vs scalar + shape1 = Shape::scalar(); + shape2 = Shape::scalar(); + EXPECT_TRUE(shape1 == shape2); + + // scalar vs static + shape1 = Shape{1}; + shape2 = Shape::scalar(); + EXPECT_FALSE(shape1 == shape2); +} + +TEST(ShapeTest, print) +{ + { // scalar + auto shape = Shape::scalar(); + std::stringstream ss; + ss << shape; + EXPECT_EQ(ss.str(), "[]"); + } + + { // 1D + auto shape = Shape{1}; + std::stringstream ss; + ss << shape; + EXPECT_EQ(ss.str(), "[1]"); + } + + { // static + auto shape = Shape{1, 2, 3}; + std::stringstream ss; + ss << shape; + EXPECT_EQ(ss.str(), "[1, 2, 3]"); + } + + { // dynamic + auto shape = Shape{Dim{1}, Dim::dynamic(), Dim{3}}; + std::stringstream ss; + ss << shape; + EXPECT_EQ(ss.str(), "[1, -1, 3]"); + } + + { // all dimensions dynamic + auto shape = Shape{Dim::dynamic(), Dim::dynamic()}; + std::stringstream ss; + ss << shape; + EXPECT_EQ(ss.str(), "[-1, -1]"); + } +} diff --git a/compiler/circle-resizer/tests/ShapeParser.test.cpp b/compiler/circle-resizer/tests/ShapeParser.test.cpp new file mode 100644 index 00000000000..521d4deb869 --- /dev/null +++ b/compiler/circle-resizer/tests/ShapeParser.test.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 Samsung Electronics Co., Ltd. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ShapeParser.h" + +#include + +#include +#include +#include +#include + +using namespace circle_resizer; +using Shapes = std::vector; + +class ParseShapeTestFixture : public ::testing::TestWithParam> +{ +}; + +TEST_P(ParseShapeTestFixture, successful_parsing) +{ + const auto [input_shape_str, expected_shapes] = GetParam(); + Shapes result_shapes; + EXPECT_NO_THROW(result_shapes = parse_shapes(input_shape_str)); + ASSERT_EQ(result_shapes, expected_shapes); +} + +INSTANTIATE_TEST_SUITE_P(ParseShapeTest, ParseShapeTestFixture, + ::testing::Values( + // single shape + std::make_tuple("[3,4]", Shapes{Shape{3, 4}}), + std::make_tuple("[3]", Shapes{Shape{3}}), + std::make_tuple("[-1]", Shapes{Shape{Dim::dynamic()}}), + std::make_tuple("[ 5, 6]", Shapes{Shape{5, 6}}), + std::make_tuple("[3 , 4]", Shapes{Shape{3, 4}}), + std::make_tuple("[-1 , 4]", Shapes{Shape{Dim::dynamic(), Dim{4}}}), + // many shapes + std::make_tuple("[3,4],[5,6]", Shapes{Shape{3, 4}, Shape{5, 6}}), + std::make_tuple("[1],[2]", Shapes{Shape{1}, Shape{2}}), + std::make_tuple(" [3, 4] , [5,6]", Shapes{Shape{3, 4}, Shape{5, 6}}), + std::make_tuple(" [ 1 ] ,[ 2]", Shapes{Shape{1}, Shape{2}}), + std::make_tuple(" [ 1 ] ,[ 2] ", Shapes{Shape{1}, Shape{2}}), + std::make_tuple(" [1,2],[3,4,5],[6,7,8,9]", + Shapes{Shape{1, 2}, Shape{3, 4, 5}, Shape{6, 7, 8, 9}}), + // scalars + std::make_tuple("[]", Shapes{Shape::scalar()}), + std::make_tuple("[],[]", Shapes{Shape::scalar(), Shape::scalar()}), + std::make_tuple("[],[2]", Shapes{Shape::scalar(), Shape{2}}), + std::make_tuple("[ ]", Shapes{Shape::scalar()}))); + +class InvalidArgParseShapeTestFixture : public ::testing::TestWithParam +{ +}; + +TEST_P(InvalidArgParseShapeTestFixture, invalid_input_NEG) +{ + EXPECT_THROW(parse_shapes(GetParam()), std::invalid_argument); +} + +INSTANTIATE_TEST_SUITE_P(InvalidArgParseShape, InvalidArgParseShapeTestFixture, + ::testing::Values(std::string{""}, std::string{"]"}, std::string{"1a,2"}, + std::string{"[-2]"}, std::string{"[-2,1,3]"}, + std::string{"-1"}, std::string{"7,7"}, std::string{"8"}, + std::string{"[8],9"}, std::string{"1,2"}, + std::string{"[1],[2],"}, std::string{"[[]],"}, + std::string{"][1]"}, std::string{"]["}));