conceptflow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. conceptflow/__init__.py +40 -0
  2. conceptflow/_version.py +1 -0
  3. conceptflow/algorithms/__init__.py +47 -0
  4. conceptflow/algorithms/_registry.py +58 -0
  5. conceptflow/algorithms/derivation.py +80 -0
  6. conceptflow/algorithms/enumeration.py +208 -0
  7. conceptflow/algorithms/hasse.py +83 -0
  8. conceptflow/algorithms/implications.py +150 -0
  9. conceptflow/algorithms/order.py +27 -0
  10. conceptflow/algorithms/reduction.py +119 -0
  11. conceptflow/cluster/__init__.py +22 -0
  12. conceptflow/cluster/concept_lattice.py +136 -0
  13. conceptflow/core/__init__.py +11 -0
  14. conceptflow/core/concept.py +56 -0
  15. conceptflow/core/context.py +241 -0
  16. conceptflow/core/lattice.py +129 -0
  17. conceptflow/core/many_valued_context.py +263 -0
  18. conceptflow/decomposition/__init__.py +23 -0
  19. conceptflow/decomposition/ordinal_factorization.py +432 -0
  20. conceptflow/exploration/__init__.py +11 -0
  21. conceptflow/exploration/builder.py +220 -0
  22. conceptflow/exploration/view.py +61 -0
  23. conceptflow/feature_extraction/__init__.py +5 -0
  24. conceptflow/feature_extraction/concept_encoder.py +172 -0
  25. conceptflow/io/__init__.py +6 -0
  26. conceptflow/io/cxt.py +126 -0
  27. conceptflow/metrics/__init__.py +11 -0
  28. conceptflow/metrics/rules.py +96 -0
  29. conceptflow/model_selection/__init__.py +11 -0
  30. conceptflow/preprocessing/__init__.py +23 -0
  31. conceptflow/preprocessing/conceptual_scaler.py +241 -0
  32. conceptflow/preprocessing/scales.py +466 -0
  33. conceptflow/rules/__init__.py +5 -0
  34. conceptflow/rules/implication_basis.py +128 -0
  35. conceptflow/validation/__init__.py +5 -0
  36. conceptflow/validation/input.py +68 -0
  37. conceptflow/visualization/__init__.py +39 -0
  38. conceptflow/visualization/d3_backend.py +405 -0
  39. conceptflow/visualization/dimflux/LICENSE +395 -0
  40. conceptflow/visualization/dimflux/README.md +77 -0
  41. conceptflow/visualization/dimflux/libs/brunt-fork.jar +0 -0
  42. conceptflow/visualization/dimflux/src/__init__.py +0 -0
  43. conceptflow/visualization/dimflux/src/dim_flux/additive_realizer.py +213 -0
  44. conceptflow/visualization/dimflux/src/dim_flux/dim_draw.py +132 -0
  45. conceptflow/visualization/dimflux/src/dim_flux/lgs.py +149 -0
  46. conceptflow/visualization/dimflux/src/dim_flux/projection.py +137 -0
  47. conceptflow/visualization/dimflux/src/dim_flux/realizer.py +97 -0
  48. conceptflow/visualization/dimflux/src/fca/context.py +130 -0
  49. conceptflow/visualization/dimflux/src/fca/lattice.py +125 -0
  50. conceptflow/visualization/dimflux/src/fdp/forces.py +519 -0
  51. conceptflow/visualization/dimflux/src/utils/variables.py +184 -0
  52. conceptflow/visualization/dimflux_layout.py +153 -0
  53. conceptflow/visualization/graph_data.py +210 -0
  54. conceptflow/visualization/html_figure.py +96 -0
  55. conceptflow/visualization/lattice_plot.py +92 -0
  56. conceptflow/visualization/nested.py +1553 -0
  57. conceptflow-0.1.0.dist-info/METADATA +748 -0
  58. conceptflow-0.1.0.dist-info/RECORD +61 -0
  59. conceptflow-0.1.0.dist-info/WHEEL +5 -0
  60. conceptflow-0.1.0.dist-info/licenses/LICENSE +28 -0
  61. conceptflow-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,40 @@
1
+ """
2
+ ConceptFlow.
3
+
4
+ A scikit-learn compatible Formal Concept Analysis framework.
5
+ """
6
+
7
+ from conceptflow._version import __version__
8
+ from conceptflow.core import (
9
+ Concept,
10
+ ConceptLattice,
11
+ FormalContext,
12
+ ManyValuedContext,
13
+ )
14
+ from conceptflow.visualization import (
15
+ GraphData,
16
+ GraphEdge,
17
+ GraphNode,
18
+ lattice_to_graph_data,
19
+ plot_lattice,
20
+ )
21
+
22
+ from conceptflow.exploration import (
23
+ ExplorationBuilder,
24
+ ExplorationView,
25
+ )
26
+
27
+ __all__ = [
28
+ "__version__",
29
+ "Concept",
30
+ "FormalContext",
31
+ "ManyValuedContext",
32
+ "ConceptLattice",
33
+ "GraphData",
34
+ "GraphEdge",
35
+ "GraphNode",
36
+ "lattice_to_graph_data",
37
+ "plot_lattice",
38
+ "ExplorationBuilder",
39
+ "ExplorationView",
40
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,47 @@
1
+ from conceptflow.algorithms.derivation import (
2
+ attribute_closure,
3
+ attribute_derivation,
4
+ object_derivation,
5
+ )
6
+ from conceptflow.algorithms.enumeration import (
7
+ enumerate_concepts,
8
+ enumerate_concepts_bruteforce,
9
+ enumerate_concepts_nextclosure,
10
+ enumerate_concepts_closebyone,
11
+ )
12
+
13
+ from conceptflow.algorithms._registry import (
14
+ SUPPORTED_ENUMERATION_ALGORITHMS,
15
+ normalize_enumeration_algorithm,
16
+ )
17
+
18
+ from conceptflow.algorithms.hasse import compute_hasse_edges, is_cover
19
+ from conceptflow.algorithms.implications import Implication, compute_canonical_basis
20
+ from conceptflow.algorithms.order import strict_subconcept_of, subconcept_of
21
+
22
+ from conceptflow.algorithms.reduction import (
23
+ ClarificationResult,
24
+ clarified_context,
25
+ clarify_context,
26
+ )
27
+
28
+ __all__ = [
29
+ "object_derivation",
30
+ "attribute_derivation",
31
+ "attribute_closure",
32
+ "enumerate_concepts",
33
+ "enumerate_concepts_bruteforce",
34
+ "enumerate_concepts_nextclosure",
35
+ "enumerate_concepts_closebyone",
36
+ "subconcept_of",
37
+ "strict_subconcept_of",
38
+ "is_cover",
39
+ "compute_hasse_edges",
40
+ "Implication",
41
+ "compute_canonical_basis",
42
+ "SUPPORTED_ENUMERATION_ALGORITHMS",
43
+ "normalize_enumeration_algorithm",
44
+ "ClarificationResult",
45
+ "clarified_context",
46
+ "clarify_context",
47
+ ]
@@ -0,0 +1,58 @@
1
+ """
2
+ Algorithm registry utilities.
3
+
4
+ This module centralizes algorithm name handling so that public APIs can
5
+ support clean canonical names and optional aliases without duplicating
6
+ string logic across the codebase.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ ENUMERATION_ALGORITHM_ALIASES = {
13
+ "bruteforce": "bruteforce",
14
+ "brute_force": "bruteforce",
15
+ "brute-force": "bruteforce",
16
+ "nextclosure": "nextclosure",
17
+ "next_closure": "nextclosure",
18
+ "next-closure": "nextclosure",
19
+ "closebyone": "closebyone",
20
+ "close_by_one": "closebyone",
21
+ "close-by-one": "closebyone",
22
+ "cbo": "closebyone",
23
+ }
24
+
25
+ SUPPORTED_ENUMERATION_ALGORITHMS = frozenset(
26
+ ENUMERATION_ALGORITHM_ALIASES.values()
27
+ )
28
+
29
+
30
+ def normalize_enumeration_algorithm(name: str) -> str:
31
+ """
32
+ Normalize a concept enumeration algorithm name.
33
+
34
+ Parameters
35
+ ----------
36
+ name:
37
+ User-provided algorithm name.
38
+
39
+ Returns
40
+ -------
41
+ str
42
+ Canonical algorithm name.
43
+
44
+ Raises
45
+ ------
46
+ ValueError
47
+ If the algorithm is unknown.
48
+ """
49
+ normalized = name.lower().strip().replace(" ", "_")
50
+
51
+ if normalized not in ENUMERATION_ALGORITHM_ALIASES:
52
+ supported = ", ".join(sorted(SUPPORTED_ENUMERATION_ALGORITHMS))
53
+ raise ValueError(
54
+ f'Unknown concept enumeration algorithm "{name}". '
55
+ f"Supported algorithms are: {supported}."
56
+ )
57
+
58
+ return ENUMERATION_ALGORITHM_ALIASES[normalized]
@@ -0,0 +1,80 @@
1
+ """
2
+ Derivation and closure operators for Formal Concept Analysis.
3
+
4
+ This module contains the basic FCA derivation operations.
5
+
6
+ For a formal context (G, M, I):
7
+
8
+ - A' is the set of attributes common to all objects in A.
9
+ - B' is the set of objects having all attributes in B.
10
+ - B'' is the closure of B.
11
+
12
+ The functions in this module operate on FormalContext objects but are kept
13
+ outside the FormalContext class so that algorithms remain reusable and easier
14
+ to optimize later.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Iterable
20
+
21
+ import numpy as np
22
+
23
+ from conceptflow.core.context import FormalContext
24
+
25
+
26
+ def object_derivation(
27
+ context: FormalContext,
28
+ object_indices: Iterable[int],
29
+ ) -> frozenset[int]:
30
+ """
31
+ Compute A' for a set of object indices A.
32
+ """
33
+ object_indices = frozenset(object_indices)
34
+
35
+ if not object_indices:
36
+ return frozenset(range(context.n_attributes))
37
+
38
+ for index in object_indices:
39
+ if index < 0 or index >= context.n_objects:
40
+ raise IndexError(
41
+ f"Object index {index} is out of range for a context with "
42
+ f"{context.n_objects} objects."
43
+ )
44
+
45
+ mask = context.incidence[list(object_indices), :].all(axis=0)
46
+ return frozenset(np.flatnonzero(mask).tolist())
47
+
48
+
49
+ def attribute_derivation(
50
+ context: FormalContext,
51
+ attribute_indices: Iterable[int],
52
+ ) -> frozenset[int]:
53
+ """
54
+ Compute B' for a set of attribute indices B.
55
+ """
56
+ attribute_indices = frozenset(attribute_indices)
57
+
58
+ if not attribute_indices:
59
+ return frozenset(range(context.n_objects))
60
+
61
+ for index in attribute_indices:
62
+ if index < 0 or index >= context.n_attributes:
63
+ raise IndexError(
64
+ f"Attribute index {index} is out of range for a context with "
65
+ f"{context.n_attributes} attributes."
66
+ )
67
+
68
+ mask = context.incidence[:, list(attribute_indices)].all(axis=1)
69
+ return frozenset(np.flatnonzero(mask).tolist())
70
+
71
+
72
+ def attribute_closure(
73
+ context: FormalContext,
74
+ attribute_indices: Iterable[int],
75
+ ) -> frozenset[int]:
76
+ """
77
+ Compute B'' for a set of attribute indices B.
78
+ """
79
+ extent = attribute_derivation(context, attribute_indices)
80
+ return object_derivation(context, extent)
@@ -0,0 +1,208 @@
1
+ """
2
+ Concept enumeration algorithms for ConceptFlow.
3
+
4
+ This module contains algorithms for enumerating formal concepts from a
5
+ formal context.
6
+
7
+ Implemented methods:
8
+
9
+ - brute-force enumeration
10
+ - NextClosure enumeration
11
+ - CloseByOne-style recursive enumeration
12
+
13
+ The brute-force algorithm is kept as a correctness baseline. NextClosure is
14
+ the first FCA-native enumeration algorithm used by ConceptFlow.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Iterable
20
+ from itertools import chain, combinations
21
+
22
+ from conceptflow.algorithms._registry import normalize_enumeration_algorithm
23
+ from conceptflow.algorithms.derivation import (
24
+ attribute_closure,
25
+ attribute_derivation,
26
+ )
27
+ from conceptflow.core.concept import Concept
28
+ from conceptflow.core.context import FormalContext
29
+
30
+
31
+ def powerset(items: Iterable[int]) -> Iterable[tuple[int, ...]]:
32
+ """
33
+ Yield all subsets of the given iterable.
34
+ """
35
+ items = list(items)
36
+ return chain.from_iterable(
37
+ combinations(items, r) for r in range(len(items) + 1)
38
+ )
39
+
40
+
41
+ def enumerate_concepts_bruteforce(context: FormalContext) -> list[Concept]:
42
+ """
43
+ Enumerate all formal concepts using brute force.
44
+
45
+ This tries every subset of attributes, closes it, and constructs the
46
+ corresponding concept.
47
+ """
48
+ concepts: set[Concept] = set()
49
+
50
+ attribute_indices = range(context.n_attributes)
51
+
52
+ for subset in powerset(attribute_indices):
53
+ intent = attribute_closure(context, subset)
54
+ extent = attribute_derivation(context, intent)
55
+ concepts.add(Concept(extent=extent, intent=intent))
56
+
57
+ return sorted(
58
+ concepts,
59
+ key=lambda c: (
60
+ len(c.extent),
61
+ sorted(c.extent),
62
+ sorted(c.intent),
63
+ ),
64
+ )
65
+
66
+
67
+ def _next_closure_candidate(
68
+ context: FormalContext,
69
+ current_intent: frozenset[int],
70
+ attribute_order: list[int],
71
+ ) -> frozenset[int] | None:
72
+ """
73
+ Compute the next closed intent after current_intent in lectic order.
74
+ """
75
+ index_of = {attribute: i for i, attribute in enumerate(attribute_order)}
76
+
77
+ for i in range(len(attribute_order) - 1, -1, -1):
78
+ attribute = attribute_order[i]
79
+
80
+ if attribute in current_intent:
81
+ continue
82
+
83
+ prefix = {a for a in current_intent if index_of[a] < i}
84
+ candidate_seed = prefix | {attribute}
85
+ candidate_closure = attribute_closure(context, candidate_seed)
86
+
87
+ is_valid = True
88
+
89
+ for j in range(i):
90
+ earlier_attribute = attribute_order[j]
91
+
92
+ if (
93
+ earlier_attribute in candidate_closure
94
+ ) != (
95
+ earlier_attribute in current_intent
96
+ ):
97
+ is_valid = False
98
+ break
99
+
100
+ if is_valid:
101
+ return candidate_closure
102
+
103
+ return None
104
+
105
+
106
+ def enumerate_concepts_nextclosure(context: FormalContext) -> list[Concept]:
107
+ """
108
+ Enumerate all formal concepts using the NextClosure algorithm.
109
+ """
110
+ concepts: list[Concept] = []
111
+
112
+ attribute_order = list(range(context.n_attributes))
113
+ current_intent: frozenset[int] | None = attribute_closure(context, [])
114
+
115
+ while current_intent is not None:
116
+ extent = attribute_derivation(context, current_intent)
117
+ concepts.append(Concept(extent=extent, intent=current_intent))
118
+
119
+ current_intent = _next_closure_candidate(
120
+ context=context,
121
+ current_intent=current_intent,
122
+ attribute_order=attribute_order,
123
+ )
124
+
125
+ return concepts
126
+
127
+ def enumerate_concepts_closebyone(context: FormalContext) -> list[Concept]:
128
+ """
129
+ Enumerate formal concepts using a simple CloseByOne-style recursion.
130
+
131
+ This implementation is intended as a clear baseline implementation of
132
+ the CloseByOne idea: recursively generate closures and use a canonicity
133
+ condition to avoid duplicates.
134
+ """
135
+ concepts: set[Concept] = set()
136
+ n_attributes = context.n_attributes
137
+
138
+ def is_canonical(
139
+ current_intent: frozenset[int],
140
+ candidate_intent: frozenset[int],
141
+ attribute_index: int,
142
+ ) -> bool:
143
+ """
144
+ Check the CloseByOne canonicity condition.
145
+
146
+ A candidate is canonical if no earlier attribute was introduced by
147
+ the closure unless it was already present in the current intent.
148
+ """
149
+ for earlier in range(attribute_index):
150
+ if earlier in candidate_intent and earlier not in current_intent:
151
+ return False
152
+
153
+ return True
154
+
155
+ def recurse(intent: frozenset[int], start_attribute: int) -> None:
156
+ extent = attribute_derivation(context, intent)
157
+ concepts.add(Concept(extent=extent, intent=intent))
158
+
159
+ for attribute_index in range(start_attribute, n_attributes):
160
+ if attribute_index in intent:
161
+ continue
162
+
163
+ candidate_seed = intent | {attribute_index}
164
+ candidate_intent = attribute_closure(context, candidate_seed)
165
+
166
+ if is_canonical(
167
+ current_intent=intent,
168
+ candidate_intent=candidate_intent,
169
+ attribute_index=attribute_index,
170
+ ):
171
+ recurse(
172
+ intent=candidate_intent,
173
+ start_attribute=attribute_index + 1,
174
+ )
175
+
176
+ initial_intent = attribute_closure(context, [])
177
+ recurse(initial_intent, 0)
178
+
179
+ return sorted(
180
+ concepts,
181
+ key=lambda c: (
182
+ len(c.extent),
183
+ sorted(c.extent),
184
+ sorted(c.intent),
185
+ ),
186
+ )
187
+
188
+ def enumerate_concepts(
189
+ context: FormalContext,
190
+ method: str = "nextclosure",
191
+ ) -> list[Concept]:
192
+ """
193
+ Enumerate concepts using the selected method.
194
+ """
195
+ method = normalize_enumeration_algorithm(method)
196
+
197
+ if method == "bruteforce":
198
+ return enumerate_concepts_bruteforce(context)
199
+
200
+ if method == "nextclosure":
201
+ return enumerate_concepts_nextclosure(context)
202
+
203
+ if method == "closebyone":
204
+ return enumerate_concepts_closebyone(context)
205
+
206
+ raise RuntimeError(
207
+ f'Algorithm "{method}" was normalized but has no implementation.'
208
+ )
@@ -0,0 +1,83 @@
1
+ """
2
+ Hasse diagram / cover relation utilities for concept lattices.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import warnings
8
+ from collections.abc import Iterable
9
+
10
+ from conceptflow.algorithms.order import strict_subconcept_of
11
+ from conceptflow.core.concept import Concept
12
+
13
+ _HASSE_LARGE_LATTICE_THRESHOLD = 500
14
+
15
+
16
+ def is_cover(
17
+ lower: Concept,
18
+ upper: Concept,
19
+ concepts: Iterable[Concept],
20
+ ) -> bool:
21
+ """
22
+ Return whether ``upper`` covers ``lower``.
23
+ """
24
+ concepts = tuple(concepts)
25
+
26
+ if not strict_subconcept_of(lower, upper):
27
+ return False
28
+
29
+ for middle in concepts:
30
+ if middle == lower or middle == upper:
31
+ continue
32
+
33
+ if (
34
+ strict_subconcept_of(lower, middle)
35
+ and strict_subconcept_of(middle, upper)
36
+ ):
37
+ return False
38
+
39
+ return True
40
+
41
+
42
+ def compute_hasse_edges(
43
+ concepts: Iterable[Concept],
44
+ ) -> tuple[tuple[Concept, Concept], ...]:
45
+ """
46
+ Compute cover edges of a concept lattice.
47
+
48
+ Returns
49
+ -------
50
+ tuple of tuple
51
+ Edges as ``(lower, upper)`` pairs.
52
+ """
53
+ concepts = tuple(concepts)
54
+ n = len(concepts)
55
+
56
+ if n > _HASSE_LARGE_LATTICE_THRESHOLD:
57
+ warnings.warn(
58
+ f"Computing Hasse edges for {n} concepts. "
59
+ "This may be slow for large lattices.",
60
+ stacklevel=2,
61
+ )
62
+
63
+ # Precompute strict order as index sets: above[i] holds all j where
64
+ # concepts[i] < concepts[j]. This avoids O(n) tuple conversion on every
65
+ # is_cover() call and enables set-difference to find direct covers.
66
+ above: list[frozenset[int]] = [
67
+ frozenset(
68
+ j for j in range(n)
69
+ if i != j and strict_subconcept_of(concepts[i], concepts[j])
70
+ )
71
+ for i in range(n)
72
+ ]
73
+
74
+ edges: list[tuple[Concept, Concept]] = []
75
+
76
+ for i in range(n):
77
+ # Elements above i that are reachable via an intermediate step are
78
+ # not direct covers. Their union is: ⋃_{j ∈ above[i]} above[j].
79
+ reachable_via_intermediate = frozenset(k for j in above[i] for k in above[j])
80
+ for j in above[i] - reachable_via_intermediate:
81
+ edges.append((concepts[i], concepts[j]))
82
+
83
+ return tuple(edges)
@@ -0,0 +1,150 @@
1
+ """
2
+ Duquenne-Guigues (stem) basis computation for ConceptFlow.
3
+
4
+ The canonical basis is the smallest set of implications (attribute-set
5
+ "premise forces conclusion" rules) that is logically equivalent to every
6
+ implication that holds in a formal context. It is built from the context's
7
+ *pseudo-intents*.
8
+
9
+ Definition (Ganter & Wille, "Formal Concept Analysis", Prop. 2.15)
10
+ -------------------------------------------------------------------
11
+ For a formal context K = (G, M, I) with derivation/closure operator
12
+ ``''`` (``attribute_closure``), a set P subseteq M is a **pseudo-intent** if:
13
+
14
+ 1. P'' != P (P is not itself a concept intent), and
15
+ 2. for every pseudo-intent Q that is a *proper subset* of P: Q'' subseteq P.
16
+
17
+ Condition 2 is recursive, but well-founded: since Q must be strictly
18
+ smaller than P, processing candidate sets in increasing size order (ties
19
+ broken by any fixed rule, here lectic/lexicographic) guarantees every
20
+ pseudo-intent smaller than the current candidate has already been decided
21
+ before it is needed.
22
+
23
+ The canonical basis is then exactly:
24
+
25
+ { P -> (P'' \\ P) : P is a pseudo-intent }
26
+
27
+ This is the same style of "clear, correct, exponential-worst-case"
28
+ algorithm as ``enumerate_concepts_bruteforce``: it tries every subset of
29
+ attributes (there is no way around this in general -- deciding whether a
30
+ given set is a pseudo-intent is already as hard as the underlying concept
31
+ lattice can be large), but it is straightforward to verify against the
32
+ definition above.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from dataclasses import dataclass
38
+ from itertools import chain, combinations
39
+
40
+ from conceptflow.algorithms.derivation import attribute_closure
41
+ from conceptflow.core.context import FormalContext
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Implication:
46
+ """
47
+ A single exact implication premise -> conclusion.
48
+
49
+ Parameters
50
+ ----------
51
+ premise:
52
+ Attribute indices forming the "if" side.
53
+
54
+ conclusion:
55
+ Attribute indices forced by the premise, *beyond* the premise
56
+ itself (i.e. ``conclusion`` and ``premise`` are disjoint).
57
+ """
58
+
59
+ premise: frozenset[int]
60
+ conclusion: frozenset[int]
61
+
62
+ def __post_init__(self) -> None:
63
+ object.__setattr__(self, "premise", frozenset(self.premise))
64
+ object.__setattr__(self, "conclusion", frozenset(self.conclusion))
65
+
66
+ def premise_names(self, context: FormalContext) -> tuple[str, ...]:
67
+ """Return the premise as attribute names, in a fixed order."""
68
+ return tuple(context.attributes[i] for i in sorted(self.premise))
69
+
70
+ def conclusion_names(self, context: FormalContext) -> tuple[str, ...]:
71
+ """Return the conclusion as attribute names, in a fixed order."""
72
+ return tuple(context.attributes[i] for i in sorted(self.conclusion))
73
+
74
+ def __repr__(self) -> str:
75
+ return (
76
+ f"Implication(premise={sorted(self.premise)}, "
77
+ f"conclusion={sorted(self.conclusion)})"
78
+ )
79
+
80
+
81
+ def _powerset(n_attributes: int):
82
+ """Yield every subset of range(n_attributes), smallest first."""
83
+ attributes = range(n_attributes)
84
+ return chain.from_iterable(
85
+ combinations(attributes, size) for size in range(n_attributes + 1)
86
+ )
87
+
88
+
89
+ def compute_canonical_basis(context: FormalContext) -> list[Implication]:
90
+ """
91
+ Compute the Duquenne-Guigues (stem) basis of a formal context.
92
+
93
+ Step by step
94
+ ------------
95
+ 1. Generate every candidate attribute subset, sorted by
96
+ ``(size, sorted(subset))`` -- smallest first. This order is what
97
+ makes step 3 well-defined: any pseudo-intent that could matter for
98
+ a minimality check on the current candidate is strictly smaller,
99
+ so it was already visited earlier in this same loop.
100
+ 2. Close the candidate with the context's own derivation operator
101
+ (``attribute_closure``, i.e. B''). If the candidate is already
102
+ equal to its own closure, it is a genuine concept intent, not a
103
+ pseudo-intent -- skip it.
104
+ 3. Otherwise, check the minimality condition against every
105
+ already-found pseudo-intent Q that is a proper subset of this
106
+ candidate: all of their closures must already fit inside the
107
+ candidate. If so, the candidate is itself a new pseudo-intent;
108
+ record it together with its closure.
109
+ 4. Once every candidate has been classified, turn each pseudo-intent
110
+ P (with closure P'') into one implication P -> (P'' \\ P) -- the
111
+ attributes P forces *beyond itself*.
112
+
113
+ Parameters
114
+ ----------
115
+ context:
116
+ The formal context to compute the basis for.
117
+
118
+ Returns
119
+ -------
120
+ list[Implication]
121
+ The canonical basis, ordered by ``(premise size, premise)``.
122
+ """
123
+ pseudo_intents: dict[frozenset[int], frozenset[int]] = {}
124
+
125
+ for subset in _powerset(context.n_attributes):
126
+ candidate = frozenset(subset)
127
+ closure = attribute_closure(context, candidate)
128
+
129
+ if closure == candidate:
130
+ # Already a concept intent -- nothing new to force.
131
+ continue
132
+
133
+ is_minimal = all(
134
+ known_closure <= candidate
135
+ for known_premise, known_closure in pseudo_intents.items()
136
+ if known_premise < candidate
137
+ )
138
+
139
+ if is_minimal:
140
+ pseudo_intents[candidate] = closure
141
+
142
+ ordered_premises = sorted(
143
+ pseudo_intents,
144
+ key=lambda premise: (len(premise), sorted(premise)),
145
+ )
146
+
147
+ return [
148
+ Implication(premise=premise, conclusion=pseudo_intents[premise] - premise)
149
+ for premise in ordered_premises
150
+ ]
@@ -0,0 +1,27 @@
1
+ """
2
+ Order relations for formal concepts.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from conceptflow.core.concept import Concept
8
+
9
+
10
+ def subconcept_of(lower: Concept, upper: Concept) -> bool:
11
+ """
12
+ Return whether ``lower <= upper`` in the concept lattice.
13
+
14
+ In FCA:
15
+
16
+ lower <= upper iff extent(lower) subseteq extent(upper)
17
+
18
+ Equivalently, this is reverse inclusion on intents.
19
+ """
20
+ return lower.extent.issubset(upper.extent)
21
+
22
+
23
+ def strict_subconcept_of(lower: Concept, upper: Concept) -> bool:
24
+ """
25
+ Return whether ``lower < upper`` in the concept lattice.
26
+ """
27
+ return lower != upper and subconcept_of(lower, upper)