///|
fn subset_count(dimension : Int32, limit : Int32) -> Int32 raise FcaError {
if dimension < 0 || dimension > 16 {
raise Budget("exhaustive algorithms require at most 16 attributes")
}
if limit < 1 || limit > 65536 {
raise Invalid("candidate limit must be 1..65536")
}
let count = 1 << dimension
if count > limit {
raise Budget("subset candidate budget exceeded")
}
count
}
///|
fn mask_indices(mask : Int32, dimension : Int32) -> Array[Int32] {
let result : Array[Int32] = []
for i = 0; i < dimension; i = i + 1 {
if (mask & (1 << i)) != 0 {
result.push(i)
}
}
result
}
///|
/// Duquenne–Guigues base by the definition of pseudo-intents.
/// Numeric subset order is inclusion-compatible: every strict subset is visited first.
/// This is exact, deliberately bounded, and not a large-scale mining algorithm.
pub fn Context::canonical_basis(
self : Context,
candidate_limit~ : Int32 = 65536,
work_limit~ : Int32 = 1000000,
) -> RuleSystem raise FcaError {
if work_limit < 1 || work_limit > 10000000 {
raise Invalid("work limit must be 1..10000000")
}
let count = subset_count(self.attribute_count(), candidate_limit)
let rules : Array[Implication] = []
let closed_pseudo : Array[Array[Int32]] = []
let mut work = 0
for mask = 0; mask < count; mask = mask + 1 {
work = work + 1
if work > work_limit {
raise Budget("pseudo-intent work budget exceeded")
}
let candidate = mask_indices(mask, self.attribute_count())
let closed = self.closure(candidate)
if candidate != closed {
let mut pseudo = true
for i = 0; i < rules.length(); i = i + 1 {
work = work + 1
if work > work_limit {
raise Budget("pseudo-intent work budget exceeded")
}
if set_subset(rules[i].premise, candidate) &&
!set_subset(closed_pseudo[i], candidate) {
pseudo = false
break
}
}
if pseudo {
if rules.length() >= 4096 {
raise Budget("canonical base exceeds 4096 implications")
}
rules.push(implication(candidate, closed, self.attribute_count()))
closed_pseudo.push(closed)
}
}
}
rule_system(self.attribute_count(), rules)
}