Skip to content

Commit

Permalink
#634, #635: Return human-readable error from C++ part of the library.
Browse files Browse the repository at this point in the history
  • Loading branch information
annoviko committed Oct 20, 2020
1 parent 61f3a25 commit 4159410
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ template <typename TypeContainer,
>
pyclustering_package * create_package(TypeContainer data) {
using container_t = typename std::remove_pointer<TypeContainer>::type;
using contaner_data_t = container_t::value_type;
using contaner_data_t = typename container_t::value_type;

pyclustering_package * package = create_package<contaner_data_t>(data->size());
if (package) {
Expand Down
27 changes: 25 additions & 2 deletions ccore/include/pyclustering/utils/traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ namespace utils {
namespace traits {


/*!
@brief Utility metafunction that maps a sequence of any types to the type `void`.
*/
template<class...>
using void_t = void;


/*!
@brief Checks whether `TypeRawString` is a raw-string type.
Expand Down Expand Up @@ -78,7 +87,7 @@ struct is_string : std::integral_constant<bool,
@tparam Type: a type to check.
*/
template <typename, typename = std::void_t<>>
template <typename, typename = void_t<>>
struct is_container_with_fundamental_content : std::false_type { };


Expand All @@ -93,7 +102,7 @@ struct is_container_with_fundamental_content : std::false_type { };
*/
template <typename Type>
struct is_container_with_fundamental_content <
Type, std::void_t<
Type, void_t<
typename Type::value_type,
typename Type::size_type,
typename Type::const_iterator,
Expand All @@ -103,10 +112,24 @@ struct is_container_with_fundamental_content <
> : std::is_fundamental<typename Type::value_type> { };


/*!
@brief Removes pointer, `const`, `volatile` from type `Type` if there are have a place in the type.
@tparam Type: a type to update.
*/
template <typename Type>
using remove_cvp = std::remove_cv<typename std::remove_pointer<Type>::type>;


/*!
@brief Helper type that removes pointer, `const`, `volatile` from type `Type` if there are have a place in the type.
@tparam Type: a type to update.
*/
template <typename Type>
using remove_cvp_t = typename remove_cvp<Type>::type;

Expand Down
10 changes: 10 additions & 0 deletions ccore/src/interface/clique_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
#include <pyclustering/cluster/clique.hpp>



constexpr const char * CLIQUE_NOT_ENOUGH_MEMORY = "There is not enough memory to perform cluster analysis using CLIQUE algorithm. "
"CLIQUE algorithm might not be suitable in case of high dimension data because CLIQUE is a grid-based algorithm and "
"the amount of CLIQUE blocks (cells) is defined as '[amount_intervals]^[amount_dimensions]'.";



pyclustering_package * clique_algorithm(const pyclustering_package * const p_sample, const std::size_t p_intervals, const std::size_t p_threshold) try {
pyclustering::dataset input_dataset;
p_sample->extract(input_dataset);
Expand Down Expand Up @@ -61,6 +68,9 @@ pyclustering_package * clique_algorithm(const pyclustering_package * const p_sam

return package;
}
catch (std::bad_alloc &) {
return create_package(CLIQUE_NOT_ENOUGH_MEMORY);
}
catch (std::exception & p_exception) {
return create_package(p_exception.what());
}
11 changes: 7 additions & 4 deletions pyclustering/cluster/clique.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@


import itertools
import warnings

from pyclustering.cluster import cluster_visualizer
from pyclustering.cluster.encoder import type_encoding
Expand All @@ -42,7 +43,6 @@
import matplotlib.patches as patches
import matplotlib.animation as animation
except Exception as error_instance:
import warnings
warnings.warn("Impossible to import matplotlib (please, install 'matplotlib'), pyclustering's visualization "
"functionality is not available (details: '%s')." % str(error_instance))

Expand Down Expand Up @@ -601,13 +601,16 @@ def __process_by_ccore(self):
user's target platform is supported.
"""
(self.__clusters, self.__noise, block_logical_locations, block_max_corners, block_min_corners, block_points) = \
wrapper.clique(self.__data, self.__amount_intervals, self.__density_threshold)
result = wrapper.clique(self.__data, self.__amount_intervals, self.__density_threshold)
if isinstance(result, str):
raise RuntimeError("Error has been detected. " + result)

(self.__clusters, self.__noise, block_logical_locations, max_corners, min_corners, block_points) = result

amount_cells = len(block_logical_locations)
for i in range(amount_cells):
self.__cells.append(clique_block(block_logical_locations[i],
spatial_block(block_max_corners[i], block_min_corners[i]),
spatial_block(max_corners[i], min_corners[i]),
block_points[i],
True))

Expand Down
8 changes: 8 additions & 0 deletions pyclustering/cluster/tests/integration/it_clique.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
import matplotlib
matplotlib.use('Agg')

from pyclustering.cluster.clique import clique
from pyclustering.cluster.tests.clique_templates import clique_test_template

from pyclustering.samples.definitions import SIMPLE_SAMPLES, FCPS_SAMPLES

from pyclustering.core.tests import remove_library

from pyclustering.tests.assertion import assertion


class clique_integration_test(unittest.TestCase):
def test_clustering_sample_simple_1_by_core(self):
Expand Down Expand Up @@ -131,6 +134,11 @@ def test_visualize_no_failure_two_dimensional_by_core(self):
def test_visualize_no_failure_three_dimensional_by_core(self):
clique_test_template.visualize(SIMPLE_SAMPLES.SAMPLE_SIMPLE11, 3, 0, True)

def test_high_dimension_data_failure(self):
data = [[0, 1, 2, 1, 3, 4, 5, 1, 2, 3, 3, 1, 3], [0, 1, 0, 1, 3, 8, 5, 5, 3, 3, 3, 0, 0]]
clique_instance = clique(data, 15, 0)
assertion.exception(RuntimeError, clique_instance.process)

@remove_library
def test_processing_when_library_core_corrupted(self):
clique_test_template.clustering(SIMPLE_SAMPLES.SAMPLE_SIMPLE1, 8, 0, [5, 5], 0, True)
3 changes: 3 additions & 0 deletions pyclustering/core/clique_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def clique(sample, intervals, threshold):
results = package_extractor(package).extract()
ccore.free_pyclustering_package(package)

if isinstance(results, bytes):
return results.decode('utf-8')

return (results[clique_package_indexer.CLIQUE_PACKAGE_INDEX_CLUSTERS],
results[clique_package_indexer.CLIQUE_PACKAGE_INDEX_NOISE],
results[clique_package_indexer.CLIQUE_PACKAGE_INDEX_LOGICAL_LOCATION],
Expand Down
47 changes: 33 additions & 14 deletions pyclustering/core/pyclustering_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,18 @@ class package_builder:
@brief Package builder provides service to create 'pyclustering_package' from data that is stored in 'list' container.
"""
def __init__(self, dataset, c_data_type):
def __init__(self, dataset, c_data_type=None):
"""!
@brief Initialize package builder object by dataset.
@details String data is packed as it is without encoding. If string encoding is required then it should be
provided already encoded, for example in case of `utf-8`:
@code
encoded_string = "String to pack".encode('utf-8')
pyclustering_package = package_builder(encoded_string)
@endcode
@param[in] dataset (list): Data that should be packed in 'pyclustering_package'.
@param[in] c_data_type (ctype.type): If specified than specified data type is used for data storing in package.
@param[in] c_data_type (ctype.type): Data C-type that is used to store data in the package.
"""
self.__dataset = dataset
Expand Down Expand Up @@ -150,7 +156,10 @@ def __get_type(self, pyclustering_data_type):

def __create_package(self, dataset):
dataset_package = pyclustering_package()


if isinstance(dataset, str):
return self.__create_package_string(dataset_package, dataset)

if isinstance(dataset, numpy.matrix):
return self.__create_package_numpy_matrix(dataset_package, dataset)

Expand Down Expand Up @@ -235,6 +244,13 @@ def __create_package_numpy_matrix(self, dataset_package, dataset):
return pointer(dataset_package)


def __create_package_string(self, dataset_package, string_utf8):
dataset_package.size = len(string_utf8)
dataset_package.type = pyclustering_type_data.PYCLUSTERING_TYPE_CHAR
dataset_package.data = cast(string_utf8, POINTER(c_void_p))
return pointer(dataset_package)



class package_extractor:
"""!
Expand Down Expand Up @@ -270,13 +286,15 @@ def __extract_data(self, ccore_package_pointer):


def __unpack_data(self, pointer_package, pointer_data, type_package):
if (type_package == pyclustering_type_data.PYCLUSTERING_TYPE_CHAR) or (
type_package == pyclustering_type_data.PYCLUSTERING_TYPE_WCHAR_T):
result = str()
append_element = lambda string, symbol: string + symbol
else:
result = []
append_element = lambda container, item: container.append(item)
if type_package == pyclustering_type_data.PYCLUSTERING_TYPE_CHAR:
pointer_string = cast(pointer_data, c_char_p)
return pointer_string.value

elif type_package == pyclustering_type_data.PYCLUSTERING_TYPE_WCHAR_T:
raise NotImplementedError("Data type 'wchar_t' is not supported.")

result = []
append_element = lambda container, item: container.append(item)

for index in range(0, pointer_package[0].size):
if type_package == pyclustering_type_data.PYCLUSTERING_TYPE_LIST:
Expand All @@ -290,10 +308,11 @@ def __unpack_data(self, pointer_package, pointer_data, type_package):


def __unpack_pointer_data(self, pointer_package):
type_package = pointer_package[0].type
current_package = pointer_package[0]
type_package = current_package.type

if pointer_package[0].size == 0:
if current_package.size == 0:
return []
pointer_data = cast(pointer_package[0].data, POINTER(pyclustering_type_data.get_ctype(type_package)))

pointer_data = cast(current_package.data, POINTER(pyclustering_type_data.get_ctype(type_package)))
return self.__unpack_data(pointer_package, pointer_data, type_package)
37 changes: 23 additions & 14 deletions pyclustering/core/tests/ut_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,40 @@

from pyclustering.core.pyclustering_package import package_builder, package_extractor

from ctypes import c_ulong, c_size_t, c_double, c_uint, c_float
from ctypes import c_ulong, c_size_t, c_double, c_uint, c_float, c_char_p


class Test(unittest.TestCase):
def templatePackUnpack(self, dataset, c_type_data = None):
def templatePackUnpack(self, dataset, c_type_data=None):
package_pointer = package_builder(dataset, c_type_data).create()
unpacked_package = package_extractor(package_pointer).extract()

packing_data = dataset
if (isinstance(packing_data, numpy.matrix)):
if isinstance(packing_data, numpy.ndarray):
packing_data = dataset.tolist()

assert self.compare_containers(packing_data, unpacked_package);
if isinstance(packing_data, str):
self.assertEqual(dataset, unpacked_package)
else:
self.assertTrue(self.compare_containers(packing_data, unpacked_package))


def compare_containers(self, container1, container2):
def is_container(container):
return (isinstance(container, list) or isinstance(container, tuple))
return isinstance(container, list) or isinstance(container, tuple)

if (len(container1) == 0 and len(container2) == 0):
if len(container1) == 0 and len(container2) == 0:
return True

if (len(container1) != len(container2)):
if len(container1) != len(container2):
return False

for index in range(len(container1)):
if (is_container(container1[index]) and is_container(container2[index])):
if is_container(container1[index]) and is_container(container2[index]):
return self.compare_containers(container1[index], container2[index])

elif (is_container(container1[index]) == is_container(container2[index])):
if (container1[index] != container2[index]):
elif is_container(container1[index]) == is_container(container2[index]):
if container1[index] != container2[index]:
return False

else:
Expand Down Expand Up @@ -130,13 +133,19 @@ def testTupleFloat(self):
self.templatePackUnpack([ (1.0, 2.0, 3.8), (4.6, 5.0), (6.8, 7.4, 8.5, 9.6) ], c_float)

def testTupleEmpty(self):
self.templatePackUnpack([ (), (), () ])
self.templatePackUnpack([(), (), ()])

def testNumpyMatrixOneColumn(self):
self.templatePackUnpack(numpy.matrix([[1.0], [2.0], [3.0]]), c_double)
self.templatePackUnpack(numpy.array([[1.0], [2.0], [3.0]]), c_double)

def testNumpyMatrixTwoColumns(self):
self.templatePackUnpack(numpy.matrix([[1.0, 1.0], [2.0, 2.0]]), c_double)
self.templatePackUnpack(numpy.array([[1.0, 1.0], [2.0, 2.0]]), c_double)

def testNumpyMatrixThreeColumns(self):
self.templatePackUnpack(numpy.matrix([[1.1, 2.2, 3.3], [2.2, 3.3, 4.4], [3.3, 4.4, 5.5]]), c_double)
self.templatePackUnpack(numpy.array([[1.1, 2.2, 3.3], [2.2, 3.3, 4.4], [3.3, 4.4, 5.5]]), c_double)

def testString(self):
self.templatePackUnpack("Test message number one".encode('utf-8'))

def testEmptyString(self):
self.templatePackUnpack("".encode('utf-8'))
12 changes: 12 additions & 0 deletions pyclustering/tests/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,15 @@ def fail(message=None):
raise AssertionError("Failure")
else:
raise AssertionError("Failure: '" + message + "'")

@staticmethod
def exception(expected_exception, callable_object, *args, **kwargs):
try:
callable_object(*args, **kwargs)
except expected_exception:
return
except Exception as actual_exception:
raise AssertionError("Expected: '%s', Actual: '%s'" %
(expected_exception.__name__, actual_exception.__class__.__name__))

raise AssertionError("Expected: '%s', Actual: 'None'" % expected_exception.__name__)

0 comments on commit 4159410

Please sign in to comment.