From 80062c844c546a1569a372419118481cd119f758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mikula?= Date: Wed, 28 Apr 2021 14:00:50 +0200 Subject: [PATCH 01/15] Add Covering Edges --- concepts/algorithms/covering_edges.py | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 concepts/algorithms/covering_edges.py diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py new file mode 100644 index 0000000..6b498da --- /dev/null +++ b/concepts/algorithms/covering_edges.py @@ -0,0 +1,28 @@ +"""Covering Edges + +cf. Carpineto, Claudio, and Giovanni Romano. +Concept data analysis: Theory and applications. +John Wiley & Sons, 2004. +""" + + +def covering_edges(concept_list, context): + """Yield mapping edge as ``((extent, intent), (lower_extent, lower_intent))`` + pairs (concept and it's lower neighbor) from ``context`` and ``concept_list``""" + Objects = context._Objects + Properties = context._Properties + + concept_index = dict(concept_list) + + for extent, intent in concept_index.items(): + candidate_counter = dict.fromkeys(concept_index, 0) + + property_candidates = Properties.supremum - intent + + for atom in Properties.fromint(property_candidates).atoms(): + extent_candidate = Objects.fromint(extent & atom.prime()) + intent_candidate = concept_index[extent_candidate] + candidate_counter[extent_candidate] += 1 + + if (intent_candidate.count() - intent.count()) == candidate_counter[extent_candidate]: + yield (extent, intent), (extent_candidate, intent_candidate) From ba43c2584cce394ea85d520a8d475ed49ec79423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Sat, 8 May 2021 17:14:21 +0200 Subject: [PATCH 02/15] Clean code and add docstest --- concepts/algorithms/covering_edges.py | 62 +++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 6b498da..fe61367 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -8,7 +8,63 @@ def covering_edges(concept_list, context): """Yield mapping edge as ``((extent, intent), (lower_extent, lower_intent))`` - pairs (concept and it's lower neighbor) from ``context`` and ``concept_list``""" + pairs (concept and it's lower neighbor) from ``context`` and ``concept_list`` + + Example: + >>> from concepts import make_context + >>> from concepts.concepts import ConceptList, Concept + + >>> context = make_context(''' + ... |0|1|2|3|4|5| + ... A|X|X|X| | | | + ... B|X| |X|X|X|X| + ... C|X|X| | |X| | + ... D| |X|X| | | |''') + + >>> concepts = [('ABCD', ''), + ... ('ABC', '0'), + ... ('AC', '01'), + ... ('A', '012'), + ... ('', '012345'), + ... ('C', '014'), + ... ('AB', '02'), + ... ('B', '02345'), + ... ('BC', '04'), + ... ('ACD', '1'), + ... ('AD', '12'), + ... ('ABD', '2')] + + >>> concept_list = ConceptList.frompairs( + ... map(lambda c: (context._Objects.frommembers(c[0]), + ... context._Properties.frommembers(c[1])), + ... concepts)) + + >>> edges = covering_edges(concept_list, context) + + >>> [(''.join(concept[0].members()), # doctest: +NORMALIZE_WHITESPACE + ... ''.join(lower[0].members())) + ... for concept, lower in edges] + [('ABCD', 'ABC'), + ('ABCD', 'ACD'), + ('ABCD', 'ABD'), + ('ABC', 'AC'), + ('ABC', 'AB'), + ('ABC', 'BC'), + ('AC', 'A'), + ('AC', 'C'), + ('A', ''), + ('C', ''), + ('AB', 'A'), + ('AB', 'B'), + ('B', ''), + ('BC', 'C'), + ('BC', 'B'), + ('ACD', 'AC'), + ('ACD', 'AD'), + ('AD', 'A'), + ('ABD', 'AB'), + ('ABD', 'AD')] + """ Objects = context._Objects Properties = context._Properties @@ -17,9 +73,9 @@ def covering_edges(concept_list, context): for extent, intent in concept_index.items(): candidate_counter = dict.fromkeys(concept_index, 0) - property_candidates = Properties.supremum - intent + property_candidates = Properties.fromint(Properties.supremum & ~intent) - for atom in Properties.fromint(property_candidates).atoms(): + for atom in property_candidates.atoms(): extent_candidate = Objects.fromint(extent & atom.prime()) intent_candidate = concept_index[extent_candidate] candidate_counter[extent_candidate] += 1 From c43831652c4f129a860b28e4c3e4ce96142bcbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Sat, 8 May 2021 19:24:17 +0200 Subject: [PATCH 03/15] Fix broken docstiring test --- concepts/algorithms/covering_edges.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index fe61367..b7c2e8d 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -11,8 +11,8 @@ def covering_edges(concept_list, context): pairs (concept and it's lower neighbor) from ``context`` and ``concept_list`` Example: - >>> from concepts import make_context - >>> from concepts.concepts import ConceptList, Concept + >>> from concepts import make_context, ConceptList + >>> from concepts._common import Concept >>> context = make_context(''' ... |0|1|2|3|4|5| From 139c3fc24c3e8cb0d799b960a8747b0bb28ebe29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Fri, 2 Jul 2021 14:46:10 +0200 Subject: [PATCH 04/15] Prototype of lattice function using covering_edges --- concepts/algorithms/covering_edges.py | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index b7c2e8d..865aefc 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -5,11 +5,37 @@ John Wiley & Sons, 2004. """ +from .fcbo import fast_generate_from + + +def lattice(context): + edges = covering_edges(fast_generate_from(context), context) + + mapping = {} + + for concept, lower_neighbor in edges: + extent, intent = concept + lower_extent, lower_intent = lower_neighbor + + if extent in mapping: + _, _, _, lower = mapping[extent] + lower.append(lower_extent) + else: + mapping[extent] = (extent, intent, [], [lower_extent]) + + if lower_extent in mapping: + _, _, upper, _ = mapping[lower_extent] + upper.append(extent) + else: + mapping[lower_extent] = (lower_extent, lower_intent, [extent], []) + + return mapping.values() + def covering_edges(concept_list, context): """Yield mapping edge as ``((extent, intent), (lower_extent, lower_intent))`` pairs (concept and it's lower neighbor) from ``context`` and ``concept_list`` - + Example: >>> from concepts import make_context, ConceptList >>> from concepts._common import Concept @@ -40,7 +66,7 @@ def covering_edges(concept_list, context): ... concepts)) >>> edges = covering_edges(concept_list, context) - + >>> [(''.join(concept[0].members()), # doctest: +NORMALIZE_WHITESPACE ... ''.join(lower[0].members())) ... for concept, lower in edges] From 3d8eca6894c2336c29aa19aa6c7ca0245e247045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Thu, 8 Jul 2021 15:49:51 +0200 Subject: [PATCH 05/15] Cleaner and more optimized implementation (+4 squashed commits) Squashed commits: [ce4b687] Cleaner implementation [fd64536] Cleaner implementation [a951435] Performance optimization [fae6416] Possibly cleaner code --- concepts/algorithms/covering_edges.py | 28 +++++++++++---------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 865aefc..1b1b543 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -5,29 +5,23 @@ John Wiley & Sons, 2004. """ +from collections import Counter + from .fcbo import fast_generate_from def lattice(context): - edges = covering_edges(fast_generate_from(context), context) + concepts = tuple(fast_generate_from(context)) + edges = covering_edges(concepts, context) - mapping = {} + mapping = dict([(extent, (extent, intent, [], [])) for extent, intent in concepts]) for concept, lower_neighbor in edges: - extent, intent = concept - lower_extent, lower_intent = lower_neighbor - - if extent in mapping: - _, _, _, lower = mapping[extent] - lower.append(lower_extent) - else: - mapping[extent] = (extent, intent, [], [lower_extent]) + extent, _ = concept + lower_extent, _ = lower_neighbor - if lower_extent in mapping: - _, _, upper, _ = mapping[lower_extent] - upper.append(extent) - else: - mapping[lower_extent] = (lower_extent, lower_intent, [extent], []) + mapping[extent][3].append(lower_extent) + mapping[lower_extent][2].append(extent) return mapping.values() @@ -97,7 +91,7 @@ def covering_edges(concept_list, context): concept_index = dict(concept_list) for extent, intent in concept_index.items(): - candidate_counter = dict.fromkeys(concept_index, 0) + candidate_counter = Counter() property_candidates = Properties.fromint(Properties.supremum & ~intent) @@ -107,4 +101,4 @@ def covering_edges(concept_list, context): candidate_counter[extent_candidate] += 1 if (intent_candidate.count() - intent.count()) == candidate_counter[extent_candidate]: - yield (extent, intent), (extent_candidate, intent_candidate) + yield (extent, intent), (extent_candidate, intent_candidate) \ No newline at end of file From 43351b91ba769ef49c4cf11666e4eab1b3feaba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 12 Jul 2021 15:44:47 +0200 Subject: [PATCH 06/15] Modify to allow parallel version of lattice builder via covering edges --- concepts/algorithms/covering_edges.py | 60 +++++++++++++++++---------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 1b1b543..3646864 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -5,28 +5,14 @@ John Wiley & Sons, 2004. """ -from collections import Counter +import multiprocessing +import itertools +import collections from .fcbo import fast_generate_from -def lattice(context): - concepts = tuple(fast_generate_from(context)) - edges = covering_edges(concepts, context) - - mapping = dict([(extent, (extent, intent, [], [])) for extent, intent in concepts]) - - for concept, lower_neighbor in edges: - extent, _ = concept - lower_extent, _ = lower_neighbor - - mapping[extent][3].append(lower_extent) - mapping[lower_extent][2].append(extent) - - return mapping.values() - - -def covering_edges(concept_list, context): +def covering_edges(concept_list, context, concept_index=None): """Yield mapping edge as ``((extent, intent), (lower_extent, lower_intent))`` pairs (concept and it's lower neighbor) from ``context`` and ``concept_list`` @@ -88,10 +74,11 @@ def covering_edges(concept_list, context): Objects = context._Objects Properties = context._Properties - concept_index = dict(concept_list) + if not concept_index: + concept_index = dict(concept_list) - for extent, intent in concept_index.items(): - candidate_counter = Counter() + for extent, intent in concept_list: + candidate_counter = collections.Counter() property_candidates = Properties.fromint(Properties.supremum & ~intent) @@ -101,4 +88,33 @@ def covering_edges(concept_list, context): candidate_counter[extent_candidate] += 1 if (intent_candidate.count() - intent.count()) == candidate_counter[extent_candidate]: - yield (extent, intent), (extent_candidate, intent_candidate) \ No newline at end of file + yield (extent, intent), (extent_candidate, intent_candidate) + + +def _return_edges(batch, concept_index, context): + return list(covering_edges(batch, concept_index, context)) + + +def lattice(context, n_of_processes=1): + concepts = tuple(fast_generate_from(context)) + concept_index = dict(concepts) + + if n_of_processes == 1: + edges = covering_edges(concepts, concept_index, context) + else: + batches = [concepts[i::n_of_processes] for i in range(0, n_of_processes)] + + with multiprocessing.Pool(4) as p: + results = [p.apply_async(_return_edges, (batch, concept_index, context)) for batch in batches] + edges = itertools.chain([result.get()[0] for result in results]) + + mapping = dict([(extent, (extent, intent, [], [])) for extent, intent in concepts]) + + for concept, lower_neighbor in edges: + extent, _ = concept + lower_extent, _ = lower_neighbor + + mapping[extent][3].append(lower_extent) + mapping[lower_extent][2].append(extent) + + return mapping.values() \ No newline at end of file From 078790a25cff750b00fb94915ff80c0b23439f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 12 Jul 2021 16:14:48 +0200 Subject: [PATCH 07/15] Hotfix: wrong order of arguments and chain.from_iterator instead of chain --- concepts/algorithms/covering_edges.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 3646864..5da82f7 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -100,13 +100,13 @@ def lattice(context, n_of_processes=1): concept_index = dict(concepts) if n_of_processes == 1: - edges = covering_edges(concepts, concept_index, context) + edges = covering_edges(concepts, context, concept_index=concept_index) else: batches = [concepts[i::n_of_processes] for i in range(0, n_of_processes)] with multiprocessing.Pool(4) as p: - results = [p.apply_async(_return_edges, (batch, concept_index, context)) for batch in batches] - edges = itertools.chain([result.get()[0] for result in results]) + results = [p.apply_async(_return_edges, (batch, context, concept_index)) for batch in batches] + edges = itertools.chain.from_iterable([result.get() for result in results]) mapping = dict([(extent, (extent, intent, [], [])) for extent, intent in concepts]) From 6aba883ae7c3b59bf60ca167e176714128fc552d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 12 Jul 2021 16:17:35 +0200 Subject: [PATCH 08/15] Hotfix: parallel version ran always on 4 processes --- concepts/algorithms/covering_edges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 5da82f7..2644a67 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -104,7 +104,7 @@ def lattice(context, n_of_processes=1): else: batches = [concepts[i::n_of_processes] for i in range(0, n_of_processes)] - with multiprocessing.Pool(4) as p: + with multiprocessing.Pool(n_of_processes) as p: results = [p.apply_async(_return_edges, (batch, context, concept_index)) for batch in batches] edges = itertools.chain.from_iterable([result.get() for result in results]) From 7209db3efbddec6506c9e0c1ea61293cf4aed648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Thu, 16 Sep 2021 18:02:30 +0200 Subject: [PATCH 09/15] Move dataset fixture to the conftest for future alg unittests --- tests/conftest.py | 24 ++++++++++++++++++++++++ tests/test_algorithms.py | 22 ---------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 14773e7..4031f6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,8 @@ TEST_OUTPUT = pathlib.Path('test-output') +ENCODING = 'utf-8' + if not TEST_OUTPUT.exists(): TEST_OUTPUT.mkdir() @@ -66,6 +68,28 @@ def lattice(context): return context.lattice +@pytest.fixture(scope='session') +def bob_ross(test_examples, filename='bob-ross.cxt'): + path = test_examples / filename + + context = concepts.load_cxt(str(path), encoding=ENCODING) + + assert context.shape == (403, 67) + + return context + + +@pytest.fixture(scope='session') +def mushroom(test_examples, filename='mushroom.cxt'): + path = test_examples / filename + + context = concepts.load_cxt(str(path)) + + assert context.shape == (8_124, 119) + + return context + + @pytest.fixture(params=['str', 'bytes', 'pathlike', 'fileobj']) def path_or_fileobj(request, tmp_path, filename='context.json'): if request.param == 'str': diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index afe4bbe..1529d79 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -11,28 +11,6 @@ ENCODING = 'utf-8' -@pytest.fixture -def bob_ross(test_examples, filename=BOB_ROSS): - path = test_examples / filename - - context = concepts.load_cxt(str(path), encoding=ENCODING) - - assert context.shape == (403, 67) - - return context - - -@pytest.fixture -def mushroom(test_examples, filename='mushroom.cxt'): - path = test_examples / filename - - context = concepts.load_cxt(str(path)) - - assert context.shape == (8_124, 119) - - return context - - def test_lattice(lattice): pairs = [f'{x._extent.bits()} <-> {x._intent.bits()}' for x in lattice] From ecb99ade7b2c702c86ed7db2c08925ce1c545e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Thu, 16 Sep 2021 18:05:16 +0200 Subject: [PATCH 10/15] ENCODING constant used only once --- tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4031f6c..818333e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,8 +14,6 @@ TEST_OUTPUT = pathlib.Path('test-output') -ENCODING = 'utf-8' - if not TEST_OUTPUT.exists(): TEST_OUTPUT.mkdir() @@ -72,7 +70,7 @@ def lattice(context): def bob_ross(test_examples, filename='bob-ross.cxt'): path = test_examples / filename - context = concepts.load_cxt(str(path), encoding=ENCODING) + context = concepts.load_cxt(str(path), encoding='utf-8') assert context.shape == (403, 67) From bb037e976d9af64d68bd5ce3ff9f81278461dbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Thu, 16 Sep 2021 18:10:52 +0200 Subject: [PATCH 11/15] Missing comma in __all__ --- concepts/algorithms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/algorithms/__init__.py b/concepts/algorithms/__init__.py index 034c500..6648d87 100644 --- a/concepts/algorithms/__init__.py +++ b/concepts/algorithms/__init__.py @@ -8,7 +8,7 @@ __all__ = ['iterunion', 'fast_generate_from', 'fcbo_dual', - 'lattice', 'neighbors' + 'lattice', 'neighbors', 'iterconcepts', 'get_concepts'] From f72bcad4bb046d89d064c9e8db48ce04d9254de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 20 Sep 2021 14:06:53 +0200 Subject: [PATCH 12/15] Sort covering_edges concepts in short lexicographic order --- concepts/algorithms/covering_edges.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 2644a67..963f714 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -95,8 +95,10 @@ def _return_edges(batch, concept_index, context): return list(covering_edges(batch, concept_index, context)) -def lattice(context, n_of_processes=1): - concepts = tuple(fast_generate_from(context)) +def lattice_fcbo(context, n_of_processes=1): + """Returns tuple of tuples in form of ``(extent, intent, upper, lower)`` in short lexicographic order.""" + concepts = list(fast_generate_from(context)) + concepts.sort(key=lambda concept: concept[0].shortlex()) concept_index = dict(concepts) if n_of_processes == 1: @@ -117,4 +119,4 @@ def lattice(context, n_of_processes=1): mapping[extent][3].append(lower_extent) mapping[lower_extent][2].append(extent) - return mapping.values() \ No newline at end of file + return tuple(mapping.values()) \ No newline at end of file From 166c67737cc9d88b8f4ddf3b83278be16465f043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 20 Sep 2021 14:10:36 +0200 Subject: [PATCH 13/15] Import lattice_fcbo into algorithms --- concepts/algorithms/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/concepts/algorithms/__init__.py b/concepts/algorithms/__init__.py index 6648d87..fcf2ed9 100644 --- a/concepts/algorithms/__init__.py +++ b/concepts/algorithms/__init__.py @@ -5,10 +5,11 @@ from .common import iterunion from .fcbo import fast_generate_from, fcbo_dual from .lindig import lattice, neighbors +from .covering_edges import lattice_fcbo __all__ = ['iterunion', 'fast_generate_from', 'fcbo_dual', - 'lattice', 'neighbors', + 'lattice', 'neighbors', 'lattice_fcbo', 'iterconcepts', 'get_concepts'] From 20fa77fbd827567bdea870ae5840569f664d7896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 20 Sep 2021 14:34:04 +0200 Subject: [PATCH 14/15] Rename n_of_processes to process_count --- concepts/algorithms/covering_edges.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concepts/algorithms/covering_edges.py b/concepts/algorithms/covering_edges.py index 963f714..53da1f6 100644 --- a/concepts/algorithms/covering_edges.py +++ b/concepts/algorithms/covering_edges.py @@ -95,18 +95,18 @@ def _return_edges(batch, concept_index, context): return list(covering_edges(batch, concept_index, context)) -def lattice_fcbo(context, n_of_processes=1): +def lattice_fcbo(context, process_count=1): """Returns tuple of tuples in form of ``(extent, intent, upper, lower)`` in short lexicographic order.""" concepts = list(fast_generate_from(context)) concepts.sort(key=lambda concept: concept[0].shortlex()) concept_index = dict(concepts) - if n_of_processes == 1: + if process_count == 1: edges = covering_edges(concepts, context, concept_index=concept_index) else: - batches = [concepts[i::n_of_processes] for i in range(0, n_of_processes)] + batches = [concepts[i::process_count] for i in range(0, process_count)] - with multiprocessing.Pool(n_of_processes) as p: + with multiprocessing.Pool(process_count) as p: results = [p.apply_async(_return_edges, (batch, context, concept_index)) for batch in batches] edges = itertools.chain.from_iterable([result.get() for result in results]) From 89073238699a9343a52d873b859e46609b9494d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Mikula?= Date: Mon, 20 Sep 2021 14:34:34 +0200 Subject: [PATCH 15/15] API addition draft --- concepts/contexts.py | 65 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/concepts/contexts.py b/concepts/contexts.py index 0479e99..bebe582 100644 --- a/concepts/contexts.py +++ b/concepts/contexts.py @@ -465,13 +465,37 @@ def _minimize(extent, intent): class LatticeMixin: + algorithm_for_lattice: str = 'lindig' + process_count: int = 1 + _parallel_algorithms: list = ['fcbo'] + _single_thread_algorithms: list = ['lindig'] + + def __init__(self, + algorithm_for_lattice: typing.Optional[str] = None, + process_count: typing.Optional[int] = None) -> None: + + if algorithm_for_lattice is not None: + if algorithm_for_lattice not in self._parallel_algorithms + self._single_thread_algorithms: + raise NotImplementedError + self.algorithm_for_lattice = algorithm_for_lattice + + if process_count is not None: + if self.algorithm_for_lattice not in self._parallel_algorithms: + raise NotImplementedError + self.process_count = process_count def _lattice(self, infimum=()): """Yield ``(extent, intent, upper, lower)`` in short lexicographic order. cf. C. Lindig. 2000. Fast Concept Analysis. """ - return algorithms.lattice(self._Objects, infimum=infimum) + + if self.algorithm_for_lattice == 'lindig': + return algorithms.lattice(self._Objects, infimum=infimum) + elif self.algorithm_for_lattice == 'fcbo': + return algorithms.lattice_fcbo(self, process_count=self.process_count) + else: + raise NotImplementedError def _neighbors(self, objects): """Yield upper neighbors from extent (in colex order?). @@ -630,23 +654,36 @@ def todict(self, ignore_lattice: bool = False class Context(ExportableMixin, LatticeMixin, MinimizeMixin, PrimeMixin, ComparableMixin, FormattingMixin, Data): - """Formal context defining a relation between objects and properties. + """Formal context defining a relation between objects and properties.""" - Create context from ``objects``, ``properties``, and ``bools`` correspondence. + def __init__(self, + objects: typing.Iterable[str], + properties: typing.Iterable[str], + bools: typing.Iterable[typing.Tuple[bool, ...]], + algorithm_for_lattice: typing.Optional[str] = None, + process_count: typing.Optional[int] = None): + """Create context from ``objects``, ``properties``, and ``bools`` correspondence. - Args: - objects: Iterable of object label strings. - properties: Iterable of property label strings. - bools: Iterable of ``len(objects)`` tuples of ``len(properties)`` booleans. + Args: + objects: Iterable of object label strings. + properties: Iterable of property label strings. + bools: Iterable of ``len(objects)`` tuples of ``len(properties)`` booleans. + algorithm_for_lattice: String specifing name of the default algorithm which is + used to build the lattice. - Returns: - Context: New :class:`.Context` instance. + Returns: + Context: New :class:`.Context` instance. - Example: - >>> from concepts import Context - >>> Context(['man', 'woman'], ['male', 'female'], [(True, False), (False, True)]) # doctest: +ELLIPSIS - - """ + Example: + >>> from concepts import Context + >>> Context(['man', 'woman'], + ... ['male', 'female'], + ... [(True, False), (False, True)]) # doctest: +ELLIPSIS + + """ + Data.__init__(self, objects, properties, bools) + LatticeMixin.__init__(self, algorithm_for_lattice, process_count) + @property def objects(self) -> typing.Tuple[str, ...]: