///|
/// Interchange object and attribute universes, preserving their order.
pub fn Context::dual(self : Context) -> Context raise FcaError {
let rows : Array[Array[Bool]] = []
for a = 0; a < self.attribute_count(); a = a + 1 {
let row : Array[Bool] = []
for o = 0; o < self.object_count(); o = o + 1 {
row.push(self.incidence[o][a])
}
rows.push(row)
}
context(self.attribute_names, self.object_names, rows)
}
///|
/// Induced subcontext in original index order; duplicates in either selection are ignored.
pub fn Context::project(
self : Context,
objects : Array[Int32],
attributes : Array[Int32],
) -> Context raise FcaError {
let objects = checked_indices(objects, self.object_count())
let attributes = checked_indices(attributes, self.attribute_count())
let rows : Array[Array[Bool]] = []
for o in objects {
rows.push(attributes.map(fn(a) { self.incidence[o][a] }))
}
context(
objects.map(fn(o) { self.object_names[o] }),
attributes.map(fn(a) { self.attribute_names[a] }),
rows,
)
}
///|
/// Add a named counterexample/observation without mutating the original context.
pub fn Context::with_object(
self : Context,
name : String,
attributes : Array[Int32],
) -> Context raise FcaError {
let attributes = checked_indices(attributes, self.attribute_count())
let names = self.objects()
names.push(name)
let rows = self.rows()
rows.push(
Array::makei(self.attribute_count(), fn(a) { attributes.contains(a) }),
)
context(names, self.attribute_names, rows)
}
///|
pub fn Context::without_object(
self : Context,
object : Int32,
) -> Context raise FcaError {
let selected = checked_indices([object], self.object_count())
self.project(
set_difference(universe(self.object_count()), selected),
universe(self.attribute_count()),
)
}
///|
/// Construct a context from per-object positive attribute indices; absent attributes are false.
pub fn context_from_sets(
objects : Array[String],
attributes : Array[String],
positives : Array[Array[Int32]],
) -> Context raise FcaError {
if positives.length() != objects.length() {
raise Invalid("positive-row count mismatch")
}
if objects.length() > 4096 || attributes.length() > 128 {
raise Budget("context dimensions exceeded")
}
let rows : Array[Array[Bool]] = []
for positive in positives {
let checked = checked_indices(positive, attributes.length())
rows.push(Array::makei(attributes.length(), fn(a) { checked.contains(a) }))
}
context(objects, attributes, rows)
}