//
// DO NOT EDIT THIS FILE!
// THIS FILE IS AUTOMATICALLY EXTRACTED FROM A LITERATE PROGRAM SOURCE FILE.
//
// This software is copyrighted 2023 by G. Andrew Mangogna.
// The following terms apply to all files associated with the software unless
// explicitly disclaimed in individual files.
// 
// The author hereby grants permission to use, copy, modify, distribute,
// and license this software and its documentation for any purpose, provided
// that existing copyright notices are retained in all copies and that this
// notice is included verbatim in any distributions. No written agreement,
// license, or royalty fee is required for any of the authorized uses.
// Modifications to this software may be copyrighted by their authors and
// need not follow the licensing terms described here, provided that the
// new terms are clearly indicated on the first page of each file where
// they apply.
// 
// IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR
// DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
// OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES
// THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
// 
// THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
// IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
// NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS,
// OR MODIFICATIONS.
// 
// GOVERNMENT USE: If you are acquiring this software on behalf of the
// U.S. government, the Government shall have only "Restricted Rights"
// in the software and related documentation as defined in the Federal
// Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2).  If you
// are acquiring the software on behalf of the Department of Defense,
// the software shall be classified as "Commercial Computer Software"
// and the Government shall have only "Restricted Rights" as defined in
// Clause 252.227-7013 (c) (1) of DFARs.  Notwithstanding the foregoing,
// the authors grant the U.S. Government and others acting in its behalf
// permission to use and distribute the software in accordance with the
// terms specified in this license.

const std = @import("std");
const builtin = @import("builtin");
const fmt = std.fmt;
const testing = std.testing;
const print = std.debug.print;
const assert = std.debug.assert;
const log = std.log.scoped(.rship_adj_matrix);
const debugging = builtin.mode == .Debug;
const StaticBitSet = std.StaticBitSet;
pub const AdjacentPair = struct {
    domain_element: usize,
    codomain_element: usize,
};
pub const RefConstraint = enum {
    at_most_one,            // symbolized by ?, i.e. optional, 0 or 1
    one,                    // symbolized by 1, i.e. one an only one
    at_least_one,           // symbolized by +, i.e. one or more
    any                     // symbolized by *, i.e. zero or more
};
pub fn AssociationAdjMatrix(
    comptime domain_size: u16,
    comptime codomain_ref: RefConstraint,
    comptime domain_ref: RefConstraint,
    comptime codomain_size: u16,
) type {
    if (domain_size == 0) @compileError("domain_size cannot be 0");
    if (codomain_size == 0) @compileError("codomain_size cannot be 0");
    return struct {
        const Self = @This();
        pub const domain_cardinality = domain_size;
        pub const domain_constraint = domain_ref;
        pub const codomain_cardinality = codomain_size;
        pub const codomain_constraint = codomain_ref;
        pub const DomainSet = StaticBitSet(domain_cardinality);
        pub const CodomainSet = StaticBitSet(codomain_cardinality);
        pub const emptyDomainSet = DomainSet.initEmpty();
        pub const emptyCodomainSet = CodomainSet.initEmpty();
        const RelationMatrix = [domain_cardinality]CodomainSet;
        const TransposeMatrix = [codomain_cardinality]DomainSet;
        relation_matrix: RelationMatrix,
        transpose_matrix: TransposeMatrix,
        pub fn init(
        ) Self {
            return Self {
                .relation_matrix = [_]CodomainSet{emptyCodomainSet}
                        ** domain_cardinality,
                .transpose_matrix = [_]DomainSet{emptyDomainSet}
                        ** codomain_cardinality,
            };
        }
        pub fn makeSingularDomainSet(
            domain_element: usize,
        ) DomainSet {
            var result = emptyDomainSet;
            result.set(domain_element);
            return result;
        }
        pub fn makeDomainSet(
            domain_elements: []const usize,
        ) DomainSet {
            var result = emptyDomainSet;
            for (domain_elements) |domain_element| {
                result.set(domain_element);
            }
        
            return result;
        }
        pub fn makeSingularCodomainSet(
            codomain_element: usize,
        ) CodomainSet {
            var result = emptyCodomainSet;
            result.set(codomain_element);
            return result;
        }
        pub fn makeCodomainSet(
            codomain_elements: []const usize,
        ) CodomainSet {
            var result = emptyCodomainSet;
            for (codomain_elements) |codomain_element| {
                result.set(codomain_element);
            }
        
            return result;
        }
        pub fn relate(
            self: *Self,
            domain_element: usize,
            codomain_element: usize,
        ) void {
            self.relation_matrix[domain_element].set(codomain_element) ;         // <1>
            self.transpose_matrix[codomain_element].set(domain_element);
        }
        pub fn relatePairs(
            self: *Self,
            pairs: []const AdjacentPair,
        ) void {
            for (pairs) |*pair| {
                self.relate(pair.domain_element, pair.codomain_element);
            }
        }
        pub fn unrelate(
            self: *Self,
            domain_element: usize,
            codomain_element: usize,
        ) void {
            self.relation_matrix[domain_element].unset(codomain_element) ;       // <1>
            self.transpose_matrix[codomain_element].unset(domain_element);
        }
        pub fn unrelatePairs(
            self: *Self,
            pairs: []const AdjacentPair,
        ) void {
            for (pairs) |*pair| {
                self.unrelate(pair.domain_element, pair.codomain_element);
            }
        }
        pub fn unrelateDomainElement(
            self: *Self,
            domain_element: usize,
        ) void {
            var codomain_iter = self.relation_matrix[domain_element].iterator(.{});
            while (codomain_iter.next()) |codomain_element| {
                self.transpose_matrix[codomain_element].unset(domain_element);
            }
            self.relation_matrix[domain_element] = emptyCodomainSet;
        }
        pub fn unrelateCodomainElement(
            self: *Self,
            codomain_element: usize,
        ) void {
            var domain_iter = self.transpose_matrix[codomain_element].iterator(.{});
            while (domain_iter.next()) |domain_element| {
                self.relation_matrix[domain_element].unset(codomain_element);
            }
            self.transpose_matrix[codomain_element] = emptyDomainSet;
        }
        pub fn rerelateDomainElement(
            self: *Self,
            domain_element: usize,
            curr_codomain_element: usize,
            new_codomain_element: usize,
        ) void {
            self.unrelate(domain_element, curr_codomain_element);
            self.relate(domain_element, new_codomain_element);
        }
        pub fn rerelateCodomainElement(
            self: *Self,
            codomain_element: usize,
            curr_domain_element: usize,
            new_domain_element: usize,
        ) void {
            self.unrelate(curr_domain_element, codomain_element);
            self.relate(new_domain_element, codomain_element);
        }
        pub fn image(
            self: Self,
            domain_set: DomainSet
        ) CodomainSet {
            const elem_count = domain_set.count();
            return switch (elem_count) {                                    // <1>
                0 => emptyCodomainSet,
        
                1 => onecase: {
                    const domain_element = domain_set.findFirstSet().?;     // <2>
                    break :onecase  self.relation_matrix[domain_element];
                },
        
                else => manycase: {
                    var codomain_set = emptyCodomainSet;
                    var domain_iter = domain_set.iterator(.{});
                    while(domain_iter.next()) |domain_element| {
                        codomain_set.setUnion(self.relation_matrix[domain_element]);
                    }
                    break :manycase codomain_set;
                }
            };
        }
        pub fn preimage(
            self: Self,
            codomain_set: CodomainSet
        ) DomainSet {
            const elem_count = codomain_set.count();
            return switch (elem_count) {
                0 => emptyDomainSet,
        
                1 => onecase: {
                    const codomain_element = codomain_set.findFirstSet().?;
                    break :onecase  self.transpose_matrix[codomain_element];
                },
        
                else => manycase: {
                    var domain_set = emptyDomainSet;
                    var codomain_iter = codomain_set.iterator(.{});
                    while(codomain_iter.next()) |codomain_element| {
                        domain_set.setUnion(self.transpose_matrix[codomain_element]);
                    }
                    break :manycase domain_set;
                }
            };
        }
        pub fn imageComplement(
            self: Self,
            domain_set: DomainSet,
            codomain_universe: CodomainSet,
        ) CodomainSet {
            var result = self.image(domain_set);
            result.toggleAll();
            result.setIntersection(codomain_universe);
            return result;
        }
        pub fn preimageComplement(
            self: Self,
            codomain_set: CodomainSet,
            domain_universe: DomainSet,
        ) DomainSet {
            var result = self.preimage(codomain_set);
            result.toggleAll();
            result.setIntersection(domain_universe);
            return result;
        }
        fn evalConsistency(
            ref_count: usize,
            constraint: RefConstraint
        ) bool {
            return switch(constraint) {
                .at_most_one => ref_count <= 1,
                .one => ref_count == 1,
                .at_least_one => ref_count >= 1,
                .any => true,
            };
        }
        fn isDomainConsistent(
            self: Self,
            domain_universe: DomainSet,
        ) bool {
            var result = true ;                                             // <1>
        
            if (domain_constraint != .any) {                                // <2>
                var element_iter = domain_universe.iterator(.{});
                while (element_iter.next()) |domain_element| {
                    const related_count = self.relation_matrix[domain_element].count();
                    const consistent = evalConsistency(related_count, domain_constraint);
                    if (!consistent) {
                        result = false;
                        if (debugging) {
                            log.warn("domain element {d}: has {d} relations: expected {s}",
                                .{domain_element, related_count, @tagName(domain_constraint)});
                        } else {
                            return result;
                        }
                    }
                }
            }
        
            return result;
        }
        fn isCodomainConsistent(
            self: Self,
            codomain_universe: CodomainSet,
        ) bool {
            var result = true;
        
            if (codomain_constraint != .any) {
                var element_iter = codomain_universe.iterator(.{});
                while (element_iter.next()) |codomain_element| {
                    const related_count = self.transpose_matrix[codomain_element].count();
                    const consistent = evalConsistency(related_count, codomain_constraint);
                    if (!consistent) {
                        result = false;
                        if (debugging) {
                            log.warn("codomain element {d}: has {d} relations: expected {s}",
                                .{codomain_element, related_count, @tagName(codomain_constraint)});
                        } else {
                            return result;
                        }
                    }
                }
            }
        
            return result;
        }
        pub fn isAssociationConsistent(
            self: Self,
            domain_universe: DomainSet,
            codomain_universe: CodomainSet,
        ) bool {
            return self.isDomainConsistent(domain_universe)
                    and
                    self.isCodomainConsistent(codomain_universe);
        }
        pub fn printAssociationMatrix (
            self: Self,
            domain_set: DomainSet,
            codomain_set: CodomainSet,
            optional_title: ?[] const u8,
        ) void {
            const space_per_cell: usize = 2;
            const lead_in_space: usize = 3;
            var leader = [_]u8 {' '} ** lead_in_space;
            const boundary_len = (Self.codomain_cardinality * space_per_cell) +
                    lead_in_space;
            const boundary = [_]u8{'='} ** boundary_len;
        
            print("\n{s}\n", .{boundary});
        
            var row_buf = [_]u8 {' '} ** (codomain_cardinality * space_per_cell);
        
            for (0..codomain_cardinality) |codomain_element| {
                var digit = fmt.digitToChar(@intCast(codomain_element % 10), .lower);
                row_buf[codomain_element * space_per_cell + 1] = digit;
            }
            print("{s}{s}\n", .{leader, row_buf});
        
            for (0..codomain_cardinality) |codomain_element| {
                row_buf[codomain_element * space_per_cell + 1] =
                    if (codomain_set.isSet(codomain_element)) '.' else 'x';
            }
            print("{s}{s}\n", .{leader, row_buf});
        
            for (0..domain_cardinality) |domain_element| {
                leader[0] = fmt.digitToChar(@intCast(domain_element % 10), .lower);
                leader[2] = if (domain_set.isSet(domain_element)) '.' else 'x';
                for (0..codomain_cardinality) |codomain_element| {
                    var mark: u8 = undefined;
                    if (self.relation_matrix[domain_element].isSet(codomain_element)) {
                        mark = '1';
                        assert(self.transpose_matrix[codomain_element].isSet(domain_element));
                    } else {
                        mark = '-';
                        assert(!self.relation_matrix[domain_element].isSet(codomain_element));
                    }
                    row_buf[codomain_element * space_per_cell + 1] = mark;
                }
                print("{s}{s}\n", .{leader, row_buf});
            }
        
            print("{s}\n", .{boundary});
            if (optional_title) |title| {
                print("{s}\n", .{title});
            }
            print("{s}\n", .{@typeName(Self)});
        }
    };
}
pub fn PartitionGenAdjMatrix(
    comptime partition_counts: []const u16,
    comptime codomain_size: u16,
) type {
    if (partition_counts.len == 0)
            @compileError("must define at least one domain partition");
    if (codomain_size == 0)
            @compileError("size of the codomain must be greater than zero");
    comptime var max_partition_size: usize = 0;
    for (partition_counts) |partition_count| {
        if (partition_count == 0)
            @compileError("size of a domain partition must be greater than zero");
        max_partition_size = @max(max_partition_size, partition_count);
    }
    return struct {
        const Self = @This();
        pub const codomain_cardinality: u16 = codomain_size;
        pub const partition_count: u16 = partition_sizes.len;
        pub const partition_sizes = partition_counts;
        pub const DomainSet = StaticBitSet(max_partition_size);
        pub const CodomainSet = StaticBitSet(codomain_cardinality);
        pub const emptyDomainSet = DomainSet.initEmpty();
        pub const emptyCodomainSet = CodomainSet.initEmpty();
        const PartitionAssociation =
            AssociationAdjMatrix(max_partition_size, .at_most_one, .one, codomain_cardinality);
        partitions: [partition_count]PartitionAssociation,
        pub fn init(
        ) Self {
            return Self {
                .partitions = [_]PartitionAssociation {PartitionAssociation.init()}
                    ** partition_count,
            };
        }
        pub fn makeSingularDomainSet(
            partition: usize,
            domain_element: usize,
        ) DomainSet {
            assert(domain_element < partition_sizes[partition]);
        
            var result = emptyDomainSet;
            result.set(domain_element);
            return result;
        }
        pub fn makeDomainSet(
            partition: usize,
            domain_elements: []const usize,
        ) DomainSet {
            var result = emptyDomainSet;
        
            for (domain_elements) |domain_element| {
                assert(domain_element < partition_sizes[partition]);
                result.set(domain_element);
            }
        
            return result;
        }
        pub fn makeSingularCodomainSet(
            codomain_element: usize,
        ) CodomainSet {
            var result = emptyCodomainSet;
            result.set(codomain_element);
            return result;
        }
        pub fn makeCodomainSet(
            codomain_elements: []const usize,
        ) CodomainSet {
            var result = emptyCodomainSet;
            for (codomain_elements) |codomain_element| {
                result.set(codomain_element);
            }
        
            return result;
        }
        pub fn relate(
            self: *Self,
            partition: usize,
            domain_element: usize,
            codomain_element: usize,
        ) void {
            assert(domain_element < partition_sizes[partition]);
        
            self.partitions[partition].relate(domain_element, codomain_element);
        }
        pub fn relatePairs(
            self: *Self,
            partition: usize,
            pairs: []const AdjacentPair,
        ) void {
            for (pairs) |*pair| {
                assert(pair.domain_element < partition_sizes[partition]);
            }
        
            self.partitions[partition].relatePairs(pairs);
        }
        pub fn unrelate(
            self: *Self,
            partition: usize,
            domain_element: usize,
            codomain_element: usize,
        ) void {
            assert(domain_element < partition_sizes[partition]);
        
            self.partitions[partition].unrelate(domain_element, codomain_element);
        }
        pub fn unrelatePairs(
            self: *Self,
            partition: usize,
            pairs: []const AdjacentPair,
        ) void {
            for (pairs) |*pair| {
                assert(pair.domain_element < partition_sizes[partition]);
            }
        
            self.partitions[partition].unrelatePairs(pairs);
        }
        pub fn unrelateDomainElement(
            self: *Self,
            partition: usize,
            domain_element: usize,
        ) void {
            assert(domain_element < partition_sizes[partition]);
        
            self.partitions[partition].unrelateDomainElement(domain_element);
        }
        pub fn classify(
            self: Self,
            codomain_element: usize,
        ) usize {
            var domain_partition: ?usize = null;
        
            for (&self.partitions, 0..) |*partition, p_index| {
                if (partition.transpose_matrix[codomain_element].count() != 0) {
                    domain_partition = p_index;
                    break;
                }
            }
        
            assert(domain_partition != null);
        
            return domain_partition.?;
        }
        pub fn unrelateCodomainElement(
            self: *Self,
            codomain_element: usize,
        ) void {
            const partition = self.classify(codomain_element);
            self.partitions[partition].unrelateCodomainElement(codomain_element);
        }
        pub fn reclassify(
            self: *Self,
            prev_partition: usize,
            prev_domain_element: usize,
            new_partition: usize,
            new_domain_element: usize,
        ) void {
            assert(prev_domain_element < partition_sizes[prev_partition]);
            assert(new_domain_element < partition_sizes[new_partition]);
        
            const codomain_set = self.partitions[prev_partition].
                    relation_matrix[prev_domain_element];
            assert(codomain_set.count() == 1);
            const codomain_element = codomain_set.findFirstSet().?;
        
            self.unrelate(prev_partition, prev_domain_element, codomain_element);
            self.relate(new_partition, new_domain_element, codomain_element);
        }
        pub fn image(
            self: Self,
            partition: usize,
            domain_set: DomainSet,
        ) CodomainSet {
            const elem_count = domain_set.count();
            return switch (elem_count) {
                0 => emptyCodomainSet,
        
                1 => onecase: {
                    const domain_element = domain_set.findFirstSet().?;
                    break :onecase  self.partitions[partition].relation_matrix[domain_element];
                },
        
                else => manycase: {
                    var codomain_set = emptyCodomainSet;
                    var domain_iter = domain_set.iterator(.{});
                    while(domain_iter.next()) |domain_element| {
                        codomain_set.setUnion(self.partitions[partition].
                            relation_matrix[domain_element]);
                    }
                    break :manycase codomain_set;
                }
            };
        }
        pub fn preimage(
            self: Self,
            partition: usize,
            codomain_set: CodomainSet,
        ) DomainSet {
            const elem_count = codomain_set.count();
            return switch (elem_count) {
                0 => emptyDomainSet,
        
                1 => onecase: {
                    const codomain_element = codomain_set.findFirstSet().?;
                    break :onecase  self.partitions[partition].transpose_matrix[codomain_element];
                },
        
                else => manycase: {
                    var domain_set = emptyDomainSet;
                    var codomain_iter = codomain_set.iterator(.{});
                    while(codomain_iter.next()) |codomain_element| {
                        domain_set.setUnion(self.partitions[partition].
                            transpose_matrix[codomain_element]);
                    }
                    break :manycase domain_set;
                }
            };
        }
        pub fn isPartitionGenConsistent(
            self: Self,
            partition_universes: []DomainSet,
            codomain_universe: CodomainSet,
        ) bool {
            var result = true;
            var total_image = emptyCodomainSet;
        
            for (&self.partitions, 0..) |*partition, p_index| {
                if (!partition.isAssociationConsistent(
                        partition_universes[p_index], codomain_universe)) {             // <1>
                    result = false;
                    if (debugging) {
                        log.warn("partitioned generalization: partition {d}: is not injective",
                                .{p_index});
                    } else {
                        return result;
                    }
                }
                var partition_image = self.image(p_index, partition_universes[p_index]);
                if (total_image.intersectWith(partition_image).count() != 0) {  // <2>
                    result = false;
                    if (debugging) {
                        log.warn("partitioned generalization: partition {d}: is not disjoint",
                                .{p_index});
                    } else {
                        return result;
                    }
                }
                total_image.setUnion(partition_image);
            }
        
            if (!total_image.eql(codomain_universe)) {                                  // <3>
                result = false;
                if (debugging) {
                    log.warn("partitioned generalization is not completely partitioned", .{});
                } else {
                    return result;
                }
            }
        
            return result;
        }
        pub fn printPartitionGenMatrix (
            self: Self,
            partition_universes: []DomainSet,
            codomain_universe: CodomainSet,
            optional_title: ?[] const u8,
        ) void {
            for (&self.partitions, 0..) |*partition, p_index| {
                var p_buffer: [32]u8 = undefined;
                const p_buf = fmt.bufPrint(&p_buffer, "partition {d}", .{p_index})
                    catch continue;
                partition.printAssociationMatrix(partition_universes[p_index],
                    codomain_universe, p_buf);
            }
            if (optional_title) |title| {
                print("{s}\n", .{title});
            }
        }
    };
}
pub fn UnionGenAdjMatrix(
    comptime partitions: u16,
    comptime set_size: u16,
) type {
    if (set_size == 0)
            @compileError("set_size argument must be greater than zero");
    if (partitions == 0)
            @compileError("partitions argument must be greater than zero");
    return struct {
        const Self = @This();
        pub const gen_cardinality: u16 = set_size;
        pub const partition_count: u16 = partitions;
        pub const UnionGenSet = StaticBitSet(gen_cardinality);
        pub const emptyDomainSet = UnionGenSet.initEmpty();
        pub const emptyCodomainSet = emptyDomainSet;
        partitions: [partition_count]UnionGenSet,
        pub fn init(
        ) Self {
            return Self {
                .partitions = [_]UnionGenSet {emptyDomainSet} ** partition_count,
            };
        }
        pub fn makeSingularUnionGenSet(
            element: usize,
        ) UnionGenSet {
            var result = emptyDomainSet;
            result.set(element);
            return result;
        }
        pub fn makeUnionGenSet(
            elements: []const usize,
        ) UnionGenSet {
            var result = emptyDomainSet;
            for (elements) |element| {
                result.set(element);
            }
        
            return result;
        }
        pub fn relate(
            self: *Self,
            partition: usize,
            gen_element: usize,
        ) void {
            self.partitions[partition].set(gen_element);
        }
        pub fn relatePairs(
            self: *Self,
            partition: usize,
            elements: []const usize,
        ) void {
            for (elements) |gen_element| {
                self.relate(partition, gen_element);
            }
        }
        fn unrelate(
            self: *Self,
            partition: usize,
            gen_element: usize,
        ) void {
            self.partitions[partition].unset(gen_element);
        }
        pub fn unrelatePairs(
            self: *Self,
            partition: usize,
            elements: []const usize,
        ) void {
            for (elements) |gen_element| {
                self.unrelate(partition, gen_element);
            }
        }
        pub fn classify(
            self: Self,
            codomain_element: usize,
        ) usize {
            var domain_partition: ?usize = null;
        
            for (&self.partitions, 0..) |*partition, p_index| {
                if (partition.isSet(codomain_element)) {
                    domain_partition = p_index;
                    break;
                }
            }
        
            assert(domain_partition != null);
        
            return domain_partition.?;
        }
        pub fn reclassify(
            self: *Self,
            partition: usize,
            gen_element: usize,
        ) void {
            const prev_partition = self.classify(gen_element);
            self.partitions[prev_partition].unset(gen_element);
            self.partitions[partition].set(gen_element);
        }
        pub fn image(
            self: Self,
            partition: usize,
            domain_set: UnionGenSet,
        ) UnionGenSet {
            return self.partitions[partition].intersectWith(domain_set);
        }
        pub fn preimage(
            self: Self,
            partition: usize,
            codomain_set: UnionGenSet,
        ) UnionGenSet {
            return self.image(partition, codomain_set);
        }
        pub fn isUnionGenConsistent(
            self: Self,
            gen_universe: UnionGenSet,
        ) bool {
            var result = true;
            var total_image = emptyCodomainSet;
        
            for (0..partition_count) |p_index| {
                var partition_image = self.image(p_index, gen_universe);
                if (total_image.intersectWith(partition_image).count() != 0) {
                    result = false;
                    if (debugging) {
                        log.warn("union generalization partition {d}: is not disjoint",
                                .{p_index});
                    } else {
                        return result;
                    }
                }
                total_image.setUnion(partition_image);
            }
        
            if (!total_image.eql(gen_universe)) {
                result = false;
                if (debugging) {
                    log.warn("union generalization is not complete", .{});
                } else {
                    return result;
                }
            }
        
            return result;
        }
        pub fn printUnionGenMatrix (
            self: Self,
            gen_universe: UnionGenSet,
            optional_title: ?[] const u8,
        ) void {
            const space_per_cell: usize = 2;
            const lead_in_space: usize = 3;
            var leader = [_]u8{' '} ** lead_in_space;
            const boundary_len = (gen_cardinality * space_per_cell) + lead_in_space;
            const boundary = [_]u8{'='} ** boundary_len;
        
            print("\n{s}\n", .{boundary});
        
            var row_buf = [_]u8 {' '} ** (gen_cardinality * space_per_cell);
        
            for (0..gen_cardinality) |element_index| {
                var digit = fmt.digitToChar(@intCast(element_index % 10), .lower);
                row_buf[element_index * space_per_cell + 1] = digit;
            }
            print("{s}{s}\n", .{leader, row_buf});
        
            for (0..gen_cardinality) |element_index| {
                row_buf[element_index * space_per_cell + 1] =
                    if (gen_universe.isSet(element_index)) '.' else 'x';
            }
            print("{s}{s}\n", .{leader, row_buf});
        
            leader[2] = '.';
            for (&self.partitions, 0..) |*partition, p_index| {
                leader[0] = fmt.digitToChar(@intCast(p_index % 10), .lower);
        
                var set_iter = gen_universe.iterator(.{});
                while (set_iter.next()) |element_index| {
                    row_buf[element_index * space_per_cell + 1] =
                        if (partition.isSet(element_index)) '1' else '-';
                }
                print("{s}{s}\n", .{leader, row_buf});
            }
        
            print("{s}\n", .{boundary});
            if (optional_title) |title| {
                print("{s}\n", .{title});
            }
            print("{s}\n", .{@typeName(Self)});
        }
    };
}

test "adjacency matrix initialization" {
    const R_33 = AssociationAdjMatrix(3, .one, .one, 3);

    try testing.expectEqual(@as(usize, 3), R_33.domain_cardinality);
    try testing.expectEqual(@as(usize, 3), R_33.codomain_cardinality);

    var r_33 = R_33.init();

    for (&r_33.relation_matrix) |*set| {
        try testing.expectEqual(@as(usize, 0), set.count());
    }
    for (&r_33.transpose_matrix) |*set| {
        try testing.expectEqual(@as(usize, 0), set.count());
    }

    testing.refAllDecls(R_33);
}
test "domain and codomain set construction" {
    const R_33 = AssociationAdjMatrix(3, .one, .one, 3);

    var domain_set = R_33.makeDomainSet(&[_]usize {0, 1});
    try testing.expect(domain_set.isSet(0));
    try testing.expect(domain_set.isSet(1));

    var codomain_set = R_33.makeCodomainSet(&[_]usize {1, 2});
    try testing.expect(codomain_set.isSet(1));
    try testing.expect(codomain_set.isSet(2));
}
test "relating elements" {
    const R_33 = AssociationAdjMatrix(3, .one, .one, 3);
    var r_33 = R_33.init();

    r_33.relate(0, 1);
    try testing.expect(r_33.relation_matrix[0].isSet(1));
    try testing.expect(r_33.transpose_matrix[1].isSet(0));

    r_33.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 1, .codomain_element = 2},
    });

    try testing.expect(r_33.relation_matrix[1].isSet(2));
    try testing.expect(r_33.transpose_matrix[2].isSet(1));
}
test "unrelating instances" {
    const R_23 = AssociationAdjMatrix(2, .at_most_one, .one, 3);
    var r_23 = R_23.init();

    r_23.relate(0, 1);
    try testing.expect(r_23.relation_matrix[0].isSet(1));

    r_23.unrelate(0,1);
    try testing.expect(!r_23.relation_matrix[0].isSet(1));
    try testing.expect(!r_23.transpose_matrix[1].isSet(0));

    r_23.relate(1, 2);
    try testing.expect(r_23.relation_matrix[1].isSet(2));
    try testing.expect(r_23.transpose_matrix[2].isSet(1));

    r_23.unrelatePairs(&[_]AdjacentPair {
        .{.domain_element = 1, .codomain_element = 2}
    });
    try testing.expect(!r_23.relation_matrix[1].isSet(2));
    try testing.expect(!r_23.transpose_matrix[2].isSet(1));

    r_23.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 0, .codomain_element = 2},
    });
    try testing.expect(r_23.relation_matrix[0].isSet(0));
    try testing.expect(r_23.relation_matrix[0].isSet(1));
    try testing.expect(r_23.relation_matrix[0].isSet(2));
    try testing.expect(r_23.transpose_matrix[0].isSet(0));
    try testing.expect(r_23.transpose_matrix[1].isSet(0));
    try testing.expect(r_23.transpose_matrix[2].isSet(0));

    r_23.unrelateDomainElement(0);
    try testing.expect(!r_23.relation_matrix[0].isSet(0));
    try testing.expect(!r_23.relation_matrix[0].isSet(1));
    try testing.expect(!r_23.relation_matrix[0].isSet(2));
    try testing.expect(!r_23.transpose_matrix[0].isSet(0));
    try testing.expect(!r_23.transpose_matrix[1].isSet(0));
    try testing.expect(!r_23.transpose_matrix[2].isSet(0));

    r_23.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 2},
        .{.domain_element = 1, .codomain_element = 2},
    });
    try testing.expect(r_23.relation_matrix[0].isSet(2));
    try testing.expect(r_23.relation_matrix[1].isSet(2));
    try testing.expect(r_23.transpose_matrix[2].isSet(0));
    try testing.expect(r_23.transpose_matrix[2].isSet(1));

    r_23.unrelateCodomainElement(2);
    try testing.expect(!r_23.relation_matrix[0].isSet(2));
    try testing.expect(!r_23.relation_matrix[1].isSet(2));
    try testing.expect(!r_23.transpose_matrix[2].isSet(0));
    try testing.expect(!r_23.transpose_matrix[2].isSet(1));
}
test "images and preimages" {
    // Consider a reflexive association (also called a homogeneous binary relation)
    // that is A ?--? A. This association be used as a serial relation, imposing an
    // order on the domain elements, by properly relating the set elements.
    const R_44 = AssociationAdjMatrix(4, .at_most_one, .at_most_one, 4);
    var r_44 = R_44.init() ;

    // All elements participate in the binary relation.
    var domain_universe = R_44.DomainSet.initFull();
    var codomain_universe = R_44.CodomainSet.initFull();

    // Relate the elements so that they form a sequence.
    // Here, we form the sequence: 0 -- 1 -- 2 -- 3
    r_44.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 2},
        .{.domain_element = 2, .codomain_element = 3},
    });
    try testing.expect(r_44.isAssociationConsistent(domain_universe, codomain_universe));
    r_44.printAssociationMatrix(domain_universe, codomain_universe,
            "? -- ? Initial Order");

    // Note that the element at the head never appears in the
    // set of related codomain elements and the element at the tail
    // never appears in the set of related domain elements.
    // For this example, 0 is the head of the sequence and 3
    // is the tail.

    // To never appear in the image, means the head must appear in the
    // complement of the image.
    var head_element = r_44.imageComplement(domain_universe, codomain_universe);
    try testing.expectEqual(@as(usize, 1), head_element.count());
    try testing.expect(head_element.isSet(0));

    // Inverting the logic computes the element at the tail.
    var tail_element = r_44.preimageComplement(codomain_universe, domain_universe);
    try testing.expectEqual(@as(usize, 1), tail_element.count());
    try testing.expect(tail_element.isSet(3));

    // The element after the head is just the image of the head element,
    // Image() is the successor function.
    var after_head = r_44.image(head_element);
    try testing.expectEqual(@as(usize, 1), after_head.count());
    try testing.expect(after_head.isSet(1));

    // There is no element before the head.
    // Preimage() is the predecessor function.
    var before_head = r_44.preimage(head_element);
    try testing.expectEqual(@as(usize, 0), before_head.count());

    // There is no element after the tail.
    var after_tail = r_44.image(tail_element);
    try testing.expectEqual(@as(usize, 0), after_tail.count());

    // The element before the tail is 2.
    var before_tail = r_44.preimage(tail_element);
    try testing.expectEqual(@as(usize, 1), before_tail.count());
    try testing.expect(before_tail.isSet(2));

    // By rerelating the elements in the sets, we can obtain a new ordering.
    // Previous order:  0 -- 1 -- 2 -- 3
    // New order:       0 -- 2 -- 1 -- 3
    r_44.rerelateDomainElement(0, 1, 2);     // 0 -- 1 is now 0 -- 2
    r_44.rerelateDomainElement(1, 2, 3);     // 1 -- 2 is now 1 -- 3
    r_44.rerelateDomainElement(2, 3, 1);     // 2 -- 3 is now 2 -- 1
    try testing.expect(r_44.isAssociationConsistent(domain_universe, codomain_universe));
    // Interestingly, the new ordering is created by flipping bit values.
    r_44.printAssociationMatrix(domain_universe, codomain_universe,
            "? -- ? New Order");

    // Now the element after the head element is 2.
    after_head = r_44.image(head_element);
    try testing.expectEqual(@as(usize, 1), after_head.count());
    try testing.expect(after_head.isSet(2));

    // The element before the tail is 1.
    before_tail = r_44.preimage(tail_element);
    try testing.expectEqual(@as(usize, 1), before_tail.count());
    try testing.expect(before_tail.isSet(1));
}
test "one-to-one association" {
    // A 1--1 B
    const R_1_to_1 = AssociationAdjMatrix(4, .one, .one, 4);
    var r_1_to_1 = R_1_to_1.init();

    var domain_univ = R_1_to_1.DomainSet.initFull();
    var codomain_univ = R_1_to_1.CodomainSet.initFull();

    // For one-to-one, each A must be related to exactly one B and vice versa.
    // Offset the element numbers so that the adjacency matrix is not diagonal.
    r_1_to_1.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 2},
        .{.domain_element = 2, .codomain_element = 3},
        .{.domain_element = 3, .codomain_element = 0},
    }) ;
    r_1_to_1.printAssociationMatrix(domain_univ, codomain_univ,
            "one-to-one association");

    try testing.expect(r_1_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B element related to A.1
    var a_1_set = R_1_to_1.makeSingularDomainSet(1);
    var b_set = r_1_to_1.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(2));

    // Find the A element that is related to B.3
    var b_3_set = R_1_to_1.makeSingularCodomainSet(3);
    var a_set = r_1_to_1.preimage(b_3_set);
    try testing.expectEqual(@as(usize, 1), a_set.count());
    try testing.expect(a_set.isSet(2));

    // Unrelate A.1 and B.2
    // This makes the adjacency matrix referentially inconsistent.
    r_1_to_1.unrelate(1, 2);
    try testing.expect(!r_1_to_1.isAssociationConsistent(domain_univ, codomain_univ));
}
test "at most one-to-one" {
    // A ?--1 B
    const R_x_to_1 = AssociationAdjMatrix(4, .at_most_one, .one, 6);
    var r_x_to_1 = R_x_to_1.init();

    var domain_univ = R_x_to_1.DomainSet.initFull();
    var codomain_univ = R_x_to_1.CodomainSet.initFull();

    // All A's must be related to one B.
    // B.0 and B.5 are unrelated to any A.
    r_x_to_1.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 2},
        .{.domain_element = 2, .codomain_element = 3},
        .{.domain_element = 3, .codomain_element = 4},
    });
    r_x_to_1.printAssociationMatrix(domain_univ, codomain_univ,
            "at most one-to-one association");

    try testing.expect(r_x_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B related to A.1
    var a_1_set = R_x_to_1.makeSingularDomainSet(1);
    var b_set = r_x_to_1.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(2));

    // Find the A that is related to B.3
    var b_3_set = R_x_to_1.makeSingularCodomainSet(3);
    var a_set = r_x_to_1.preimage(b_3_set);
    try testing.expectEqual(@as(usize, 1), a_set.count());
    try testing.expect(a_set.isSet(2));

    // Find the unrelated B's (red B's)
    var red_b_set = r_x_to_1.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 2), red_b_set.count());
    try testing.expect(red_b_set.isSet(0));
    try testing.expect(red_b_set.isSet(5));

    // Unrelate A.1 from B.2
    // This makes the association referentially inconsistent
    // because A.1 is not related.
    // The fact that B.2 is unrelated is fine.
    r_x_to_1.unrelate(1, 2);
    try testing.expect(!r_x_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Restore consistency by relating A.1 to B.5
    r_x_to_1.relate(1, 5);
    try testing.expect(r_x_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Relating A.1 back to B.2 again causes the association
    // to become inconsistent because A.1 is related to more than one
    // codomain element.
    r_x_to_1.relate(1, 2);
    try testing.expect(!r_x_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Simply deleting A.1 restores consistency, leaving B.2 and B.5
    // unrelated. Deleting A.1 is equivalent to unrelating A.1
    // from any codomain element and removing it from the domain universe set.
    r_x_to_1.unrelateDomainElement(1);
    domain_univ.unset(1);
    try testing.expect(r_x_to_1.isAssociationConsistent(domain_univ, codomain_univ));
}
test "at least one-to-one" {
    // A +--1 B
    const R_M_to_1 = AssociationAdjMatrix(8, .at_least_one, .one, 4);
    var r_M_to_1 = R_M_to_1.init();

    var domain_univ = R_M_to_1.DomainSet.initFull();
    var codomain_univ = R_M_to_1.CodomainSet.initFull();

    // All the A's must be related.
    // Note we relate several A's to the same B
    r_M_to_1.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 0},
        .{.domain_element = 3, .codomain_element = 1},
        .{.domain_element = 4, .codomain_element = 2},
        .{.domain_element = 5, .codomain_element = 2},
        .{.domain_element = 6, .codomain_element = 3},
        .{.domain_element = 7, .codomain_element = 3},
    });
    r_M_to_1.printAssociationMatrix(domain_univ, codomain_univ,
            "at least one-to-one association");

    try testing.expect(r_M_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B (there's always one for this type of association)
    // related to A.1
    var a_1_set = R_M_to_1.makeSingularDomainSet(1);
    var b_set = r_M_to_1.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(0));

    // Find the set of A's that are related to B.2
    // There is always at least one and in this case there are two.
    var b_2_set = R_M_to_1.makeSingularCodomainSet(2);
    var a_set = r_M_to_1.preimage(b_2_set);
    try testing.expectEqual(@as(usize, 2), a_set.count());
    try testing.expect(domain_univ.isSet(4));
    try testing.expect(domain_univ.isSet(5));

    // Unrelate A.1
    // This makes the association referentially inconsistent
    // i.e. this makes A.1 red.
    r_M_to_1.unrelate(1, 0);
    try testing.expect(!r_M_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Consistency can be restored by deleting A.1.
    // This is because B.0 is also related to A.0 and A.2
    r_M_to_1.unrelateDomainElement(1);
    domain_univ.unset(1);
    try testing.expect(r_M_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Deleting A.3 causes inconsistency because B.1 is only related
    // to A.3 or, in other words, deleting A.3 turns B.1 to red and
    // no red elements are allowed in a +--1 association.
    r_M_to_1.unrelateDomainElement(3);
    domain_univ.unset(3);
    try testing.expect(!r_M_to_1.isAssociationConsistent(domain_univ, codomain_univ));
}
test "any-to-one" {
    // A *--1 B
    const R_X_to_1 = AssociationAdjMatrix(4, .any, .one, 6);

    var r_X_to_1 = R_X_to_1.init();

    var domain_univ = R_X_to_1.DomainSet.initFull();
    var codomain_univ = R_X_to_1.CodomainSet.initFull();

    // Relate all the domain elements to the codomain elements.
    // Note we relate several A's to the same B
    // Codomain elements B.1, B.4, and B.5 are unrelated.
    r_X_to_1.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 2},
        .{.domain_element = 3, .codomain_element = 3},
    });
    r_X_to_1.printAssociationMatrix(domain_univ, codomain_univ,
            "any-to-one association");

    try testing.expect(r_X_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B (there's always only one in this case) related to A.1
    var a_1_set = R_X_to_1.makeSingularDomainSet(1);
    var b_set = r_X_to_1.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(0));

    // Find the set of A's unrelated to B.0
    var b_0_set = R_X_to_1.makeSingularCodomainSet(0);
    var a_set = r_X_to_1.preimage(b_0_set);
    try testing.expectEqual(@as(usize, 2), a_set.count());
    try testing.expect(a_set.isSet(0));
    try testing.expect(a_set.isSet(1));

    // Find the unrelated B's (red B's)
    var red_b_set = r_X_to_1.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 3), red_b_set.count());
    try testing.expect(red_b_set.isSet(1));
    try testing.expect(red_b_set.isSet(4));
    try testing.expect(red_b_set.isSet(5));

    // Find the unrelated A's. There can be none because the association
    // relation is a function.
    var red_a_set = r_X_to_1.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 0), red_a_set.count());

    // Unrelate A.1 from B.0
    // This makes the association referentially inconsistent.
    r_X_to_1.unrelate(1, 0);
    try testing.expect(!r_X_to_1.isAssociationConsistent(domain_univ, codomain_univ));

    // Deleting A.1 makes the association consistent again.
    // In fact deleting any A instance leaves the association consistent.
    r_X_to_1.unrelateDomainElement(1);
    domain_univ.unset(1);
    try testing.expect(r_X_to_1.isAssociationConsistent(domain_univ, codomain_univ));
}
test "at most one-to-at most one" {
    // A ?--? B
    const R_x_to_x = AssociationAdjMatrix(5, .at_most_one, .at_most_one, 5);

    var r_x_to_x = R_x_to_x.init();

    // Register all set elements.
    var domain_univ = R_x_to_x.DomainSet.initFull();
    var codomain_univ = R_x_to_x.CodomainSet.initFull();

    // Relate 3 elements in both the domain and codomain.
    // Elements A.3 and A.4 are unrelated.
    // Elements B.0 and B.4 are unrelated.
    r_x_to_x.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 2},
        .{.domain_element = 2, .codomain_element = 3},
    });
    r_x_to_x.printAssociationMatrix(domain_univ, codomain_univ,
            "at most one-to-at most one association");

    try testing.expect(r_x_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B related to A.1
    var a_1_set = R_x_to_x.makeSingularDomainSet(1);
    var b_set = r_x_to_x.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(2));

    // Find the A related to B.3
    var b_3_set = R_x_to_x.makeSingularCodomainSet(3);
    var a_set = r_x_to_x.preimage(b_3_set);
    try testing.expectEqual(@as(usize, 1), a_set.count());
    try testing.expect(a_set.isSet(2));

    // Find the unrelated B's (red B's).
    var red_b_set = r_x_to_x.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 2), red_b_set.count());
    try testing.expect(red_b_set.isSet(0));
    try testing.expect(red_b_set.isSet(4));

    // Find the unrelated A's (red A's).
    var red_a_set = r_x_to_x.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 2), red_a_set.count());
    try testing.expect(red_a_set.isSet(3));
    try testing.expect(red_a_set.isSet(4));
}
test "at least one-to-at most one" {
    // A +--? B
    const R_M_to_x = AssociationAdjMatrix(6, .at_least_one, .at_most_one, 4);

    var r_M_to_x = R_M_to_x.init();

    var domain_univ = R_M_to_x.DomainSet.initFull();
    var codomain_univ = R_M_to_x.CodomainSet.initFull();

    // Relate 5 of the domain elements.
    // A.5 is unrelated.
    // All 4 of the codomain elements must be related.
    r_M_to_x.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 2},
        .{.domain_element = 4, .codomain_element = 3},
    });
    r_M_to_x.printAssociationMatrix(domain_univ, codomain_univ,
            "at least one-to-at most one association");

    try testing.expect(r_M_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B related to A.1
    var a_1_set = R_M_to_x.makeSingularDomainSet(1);
    var b_set = r_M_to_x.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(0));

    // Find the A's related to B.1
    // There is always at least one and in this case there are 2.
    var b_1_set = R_M_to_x.makeSingularCodomainSet(1);
    var a_set = r_M_to_x.preimage(b_1_set);
    try testing.expectEqual(@as(usize, 2), a_set.count());
    try testing.expect(a_set.isSet(0));
    try testing.expect(a_set.isSet(2));

    // Find the unrelated B's (red B's).
    // (there are none since the association is surjective).
    var red_b_set = r_M_to_x.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 0), red_b_set.count());

    // Find the unrelated A's (red A's).
    var red_a_set = r_M_to_x.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 1), red_a_set.count());
    try testing.expect(red_a_set.isSet(5));

    // Unrelate A.2 from A.1
    // This leaves the association consistent because
    // A.0 is still associated with B.1.
    r_M_to_x.unrelate(2, 1);
    try testing.expect(r_M_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // Unrelate A.4 from B.3
    // This makes the association referentially inconsistent
    // because only A.4 was related to B.3.
    r_M_to_x.unrelate(4, 3);
    try testing.expect(!r_M_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // Consistency is restored if we relate A.5 to B.3
    // B.3 (the one just unrelated that caused the inconsistency).
    // This leaves A.4 unrelated (now a red A), but that is allowed for
    // this multiplicity and conditionality.
    r_M_to_x.relate(5, 3);
    try testing.expect(r_M_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // We can get back to the original associations by re-relating
    // B.3 from A.5 back to A.4.
    r_M_to_x.rerelateCodomainElement(3, 5, 4);
    try testing.expect(r_M_to_x.isAssociationConsistent(domain_univ, codomain_univ));
}
test "any-to-at most one" {
    // A *--? B
    const R_X_to_x = AssociationAdjMatrix(10, .any, .at_most_one, 10);
    var r_X_to_x = R_X_to_x.init();

    // Use half the possible domain and codomain elements.
    // This illustrates the notion that the universe of the domain and codomain
    // may be different than the number of bits allocated to store the sets.
    var domain_univ = R_X_to_x.emptyDomainSet;
    domain_univ.setRangeValue(
            .{.start = 0, .end = R_X_to_x.domain_cardinality / 2}, true);
    var codomain_univ = R_X_to_x.emptyCodomainSet;
    codomain_univ.setRangeValue(
            .{.start = 0, .end = R_X_to_x.domain_cardinality / 2}, true);

    // Relate 4 of the available A's. A.4 is unrelated.
    // B.0 and B.4 are unrelated.
    r_X_to_x.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 1},
        .{.domain_element = 1, .codomain_element = 1},
        .{.domain_element = 2, .codomain_element = 2},
        .{.domain_element = 3, .codomain_element = 3},
    });
    r_X_to_x.printAssociationMatrix(domain_univ, codomain_univ,
            "any-to-at most one association");

    try testing.expect(r_X_to_x.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B related to A.1
    var a_1_set = R_X_to_x.makeSingularDomainSet(1);
    var b_set = r_X_to_x.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(1));

    // Find the A's related to B.1
    var b_1_set = R_X_to_x.makeSingularCodomainSet(1);
    var a_set = r_X_to_x.preimage(b_1_set);
    try testing.expectEqual(@as(usize, 2), a_set.count());
    try testing.expect(a_set.isSet(0));
    try testing.expect(a_set.isSet(1));

    // Find the B's not related to any A (red B's).
    // Note it is necessary to intersect the result with the codomain set since
    // not all bits in the domain_univ reflect active classes.
    var red_b_set = r_X_to_x.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 2), red_b_set.count());
    try testing.expect(red_b_set.isSet(0));
    try testing.expect(red_b_set.isSet(4));

    // Find the A's not related to any B (red A's).
    var red_a_set = r_X_to_x.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 1), red_a_set.count());
    try testing.expect(red_a_set.isSet(4));

    // Relate A.1 to B.3
    // This makes A.1 refer to multiple B's and that violates consistency.
    r_X_to_x.relate(1, 3);
    try testing.expect(!r_X_to_x.isAssociationConsistent(domain_univ, codomain_univ));
}
test "at least one-to-at least one" {
    // A +--+ B
    const R_M_to_M = AssociationAdjMatrix(5, .at_least_one, .at_least_one, 3);
    var r_M_to_M = R_M_to_M.init();

    var domain_univ = R_M_to_M.DomainSet.initFull();
    var codomain_univ = R_M_to_M.CodomainSet.initFull();

    // Relate all the elements. To be consistent all elements must be somehow related.
    r_M_to_M.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 2},
        .{.domain_element = 4, .codomain_element = 2},
    });

    r_M_to_M.printAssociationMatrix(domain_univ, codomain_univ,
            "at least one-to-at least one association");

    try testing.expect(r_M_to_M.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B's (there's always at least one in this case)
    // related to A.2
    var a_2_set = R_M_to_M.makeSingularDomainSet(2);
    var b_set = r_M_to_M.image(a_2_set);
    try testing.expectEqual(@as(usize, 2), b_set.count());
    try testing.expect(b_set.isSet(0));
    try testing.expect(b_set.isSet(1));

    // Find the A's related to B.0
    var b_0_set = R_M_to_M.makeSingularCodomainSet(0);
    var a_set = r_M_to_M.preimage(b_0_set);
    try testing.expectEqual(@as(usize, 3), a_set.count());
    try testing.expect(a_set.isSet(0));
    try testing.expect(a_set.isSet(1));
    try testing.expect(a_set.isSet(2));

    // Unrelate A.3 from B.1
    // The relation remains consistent because A.3 remains associated
    // to a B, namely B.2.
    r_M_to_M.unrelate(3, 2);
    try testing.expect(r_M_to_M.isAssociationConsistent(domain_univ, codomain_univ));

    // Unrelate A.4 from B.2.
    // This makes the association referentially inconsistent because
    // A.4 was only associated to a single B (i.e. A turned red).
    r_M_to_M.unrelate(4, 2);
    try testing.expect(!r_M_to_M.isAssociationConsistent(domain_univ, codomain_univ));
}
test "at least one-to-any" {
    // A +--* B
    const R_M_to_X = AssociationAdjMatrix(8, .at_least_one, .any, 4);
    var r_M_to_X = R_M_to_X.init();

    var domain_univ = R_M_to_X.DomainSet.initFull();
    var codomain_univ = R_M_to_X.CodomainSet.initFull();

    // Relate a subset of A's, while making some of the
    // A's related to the same B.
    // Note we also relate several A's to the same B.
    // A.5, A.6, and A.7 are unrelated.
    r_M_to_X.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 2},
        .{.domain_element = 4, .codomain_element = 3},
    });

    r_M_to_X.printAssociationMatrix(domain_univ, codomain_univ,
            "at least one_to_any association");

    try testing.expect(r_M_to_X.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B's (there's always at least one in this case)
    // related to A.2
    var a_2_set = R_M_to_X.makeSingularDomainSet(2);
    var b_set = r_M_to_X.image(a_2_set);
    try testing.expectEqual(@as(usize, 2), b_set.count());
    try testing.expect(b_set.isSet(0));
    try testing.expect(b_set.isSet(1));

    // Find the A's related to B.3
    var b_3_set = R_M_to_X.makeSingularCodomainSet(3);
    var a_set = r_M_to_X.preimage(b_3_set);
    try testing.expectEqual(@as(usize, 1), a_set.count());
    try testing.expect(a_set.isSet(4));

    // Find the unrelated A's (red A's).
    var red_a_set = r_M_to_X.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 3), red_a_set.count());
    try testing.expect(red_a_set.isSet(5));
    try testing.expect(red_a_set.isSet(6));
    try testing.expect(red_a_set.isSet(7));

    // Unrelate A.4 from B.3
    // This causes the relation to be inconsistent because no A
    // is associated to B.3 (i.e. B.3 turns red).
    r_M_to_X.unrelate(4, 3);
    try testing.expect(!r_M_to_X.isAssociationConsistent(domain_univ, codomain_univ));
}
test "any-to-any" {
    // A *--* B
    const R_X_to_X = AssociationAdjMatrix(8, .any, .any, 8);
    var r_X_to_X = R_X_to_X.init();

    var domain_univ = R_X_to_X.DomainSet.initFull();
    var codomain_univ = R_X_to_X.CodomainSet.initFull();

    // Relate the some of the elements.
    // A.5, A.6, and A.7 are unrelated.
    // B.3, B.4, B.5, B.6, and B.7 unrelated.
    r_X_to_X.relatePairs(&[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 0},
        .{.domain_element = 1, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 0},
        .{.domain_element = 2, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 1},
        .{.domain_element = 3, .codomain_element = 2},
        .{.domain_element = 4, .codomain_element = 2},
    });
    r_X_to_X.printAssociationMatrix(domain_univ, codomain_univ,
            "any-to-any association");

    // Note this test is superfluous. There is no way to violate
    // referential consistency for an binary relation that is any-to-any.
    try testing.expect(r_X_to_X.isAssociationConsistent(domain_univ, codomain_univ));

    // Find the B's related to A.1
    var a_1_set = R_X_to_X.makeSingularDomainSet(1);
    var b_set = r_X_to_X.image(a_1_set);
    try testing.expectEqual(@as(usize, 1), b_set.count());
    try testing.expect(b_set.isSet(0));

    // Find the A's related to B.1
    var b_1_set = R_X_to_X.makeSingularCodomainSet(1);
    var a_set = r_X_to_X.preimage(b_1_set);
    try testing.expectEqual(@as(usize, 2), a_set.count());
    try testing.expect(a_set.isSet(2));
    try testing.expect(a_set.isSet(3));

    // Find the A's unrelated to B.1.
    a_set = r_X_to_X.preimageComplement(b_1_set, domain_univ);
    try testing.expectEqual(@as(usize, 6), a_set.count());
    try testing.expect(a_set.isSet(0));
    try testing.expect(a_set.isSet(1));
    try testing.expect(a_set.isSet(4));
    try testing.expect(a_set.isSet(5));
    try testing.expect(a_set.isSet(6));
    try testing.expect(a_set.isSet(7));

    // Find all the unrelated A's (red A's).
    var red_a_set = r_X_to_X.preimageComplement(codomain_univ, domain_univ);
    try testing.expectEqual(@as(usize, 3), red_a_set.count());
    try testing.expect(red_a_set.isSet(5));
    try testing.expect(red_a_set.isSet(6));
    try testing.expect(red_a_set.isSet(7));

    // Find all the unrelated B's (red B's).
    var red_b_set = r_X_to_X.imageComplement(domain_univ, codomain_univ);
    try testing.expectEqual(@as(usize, 5), red_b_set.count());
    try testing.expect(red_b_set.isSet(3));
    try testing.expect(red_b_set.isSet(4));
    try testing.expect(red_b_set.isSet(5));
    try testing.expect(red_b_set.isSet(6));
    try testing.expect(red_b_set.isSet(7));
}
test "partitioned generalization initialization" {
    const R_38 = PartitionGenAdjMatrix(&[_]u16 {3, 4, 5}, 8);

    try testing.expectEqual(@as(u16, 3), R_38.partition_count);
    try testing.expectEqual(@as(u16, 8), R_38.codomain_cardinality);

    var r_38 = R_38.init();

    for (&r_38.partitions) |*domain_partition| {
        for (&domain_partition.relation_matrix) |*codomain_set| {
            try testing.expectEqual(@as(usize, 0), codomain_set.count());
        }
        for (&domain_partition.transpose_matrix) |*domain_set| {
            try testing.expectEqual(@as(usize, 0), domain_set.count());
        }
    }

    testing.refAllDecls(R_38);
}
test "partitioned generalization relating" {
    const R_46 = PartitionGenAdjMatrix(&[_]u16 {3, 4, 5, 4}, 8);
    var r_46 = R_46.init();

    const codomain_universe = R_46.CodomainSet.initFull();
    var partition_universes = [_]R_46.DomainSet {R_46.emptyDomainSet} **
            R_46.partition_count;

    partition_universes[0].set(0);
    partition_universes[0].set(1);
    r_46.relatePairs(0, &[_]AdjacentPair {
        .{.domain_element = 0, .codomain_element = 7},
        .{.domain_element = 1, .codomain_element = 6},
    });

    try testing.expect(r_46.partitions[0].relation_matrix[0].isSet(7));
    try testing.expect(r_46.partitions[0].transpose_matrix[7].isSet(0));
    try testing.expectEqual(@as(usize, 0), r_46.classify(7));

    try testing.expect(r_46.partitions[0].relation_matrix[1].isSet(6));
    try testing.expect(r_46.partitions[0].transpose_matrix[6].isSet(1));
    try testing.expectEqual(@as(usize, 0), r_46.classify(6));

    partition_universes[1].set(2);
    partition_universes[1].set(3);
    r_46.relatePairs(1, &[_]AdjacentPair {
        .{.domain_element = 2, .codomain_element = 5},
        .{.domain_element = 3, .codomain_element = 4},
    });

    try testing.expect(r_46.partitions[1].relation_matrix[2].isSet(5));
    try testing.expect(r_46.partitions[1].transpose_matrix[5].isSet(2));
    try testing.expectEqual(@as(usize, 1), r_46.classify(5));

    try testing.expect(r_46.partitions[1].relation_matrix[3].isSet(4));
    try testing.expect(r_46.partitions[1].transpose_matrix[4].isSet(3));
    try testing.expectEqual(@as(usize, 1), r_46.classify(4));

    partition_universes[2].set(1);
    partition_universes[2].set(2);
    partition_universes[2].set(3);
    partition_universes[2].set(4);
    r_46.relatePairs(2, &[_]AdjacentPair {
        .{.domain_element = 1, .codomain_element = 3},
        .{.domain_element = 2, .codomain_element = 2},
        .{.domain_element = 3, .codomain_element = 1},
        .{.domain_element = 4, .codomain_element = 0},
    });

    try testing.expect(r_46.partitions[2].relation_matrix[1].isSet(3));
    try testing.expect(r_46.partitions[2].transpose_matrix[3].isSet(1));
    try testing.expectEqual(@as(usize, 2), r_46.classify(3));

    try testing.expect(r_46.partitions[2].relation_matrix[2].isSet(2));
    try testing.expect(r_46.partitions[2].transpose_matrix[2].isSet(2));
    try testing.expectEqual(@as(usize, 2), r_46.classify(2));

    try testing.expect(r_46.partitions[2].relation_matrix[3].isSet(1));
    try testing.expect(r_46.partitions[2].transpose_matrix[1].isSet(3));
    try testing.expectEqual(@as(usize, 2), r_46.classify(1));

    try testing.expect(r_46.partitions[2].relation_matrix[4].isSet(0));
    try testing.expect(r_46.partitions[2].transpose_matrix[0].isSet(4));
    try testing.expectEqual(@as(usize, 2), r_46.classify(0));

    r_46.printPartitionGenMatrix(&partition_universes, codomain_universe,
            "before reclassification");

    // previously: 0/0 was related to 7
    // reclassify so: 3/1 is related to 7
    partition_universes[0].unset(0);
    partition_universes[3].set(1);
    r_46.reclassify(0, 0, 3, 1);
    r_46.printPartitionGenMatrix(&partition_universes, codomain_universe,
            "after reclassification");

    try testing.expect(!r_46.partitions[0].relation_matrix[0].isSet(7));
    try testing.expect(!r_46.partitions[0].transpose_matrix[7].isSet(0));
    try testing.expect(r_46.partitions[3].relation_matrix[1].isSet(7));
    try testing.expect(r_46.partitions[3].transpose_matrix[7].isSet(1));
}
test "partitioned generalization image and preimage tests" {
    const RotaryWingAircraftSubtypes = enum {
        helicopter,
        vtol_aircraft,      // Vertical Take Off and Landing
    };
    const rwa_helicopter_partition: u16 = @intFromEnum(RotaryWingAircraftSubtypes.helicopter);
    const rwa_vtol_partition: u16 = @intFromEnum(RotaryWingAircraftSubtypes.vtol_aircraft);

    const FixedWingAircraftSubtypes = enum {
        airplane,
        vtol_aircraft,
    };
    const fwa_vtol_partition: u16 = @intFromEnum(FixedWingAircraftSubtypes.vtol_aircraft);
    const fwa_airplane_partition: u16 = @intFromEnum(FixedWingAircraftSubtypes.airplane);

    const R1 = PartitionGenAdjMatrix(&[_]u16 {5, 5}, 5);
    const R2 = PartitionGenAdjMatrix(&[_]u16 {5, 5}, 5);

    var r1 = R1.init();
    var r2 = R2.init();

    var rwa_partitions = [_]R1.DomainSet {R1.emptyDomainSet} ** 2;
    var rwa_codomain_set = R1.emptyCodomainSet;
    var fwa_partitions = [_]R2.DomainSet {R1.emptyDomainSet} ** 2;
    var fwa_codomain_set = R2.emptyCodomainSet;

    // Relate a helicopter instance.
    // Note the domain partition and codomain universes must be set up
    // so that referential consistency can be evaluated.
    const N00H: usize = 0;
    rwa_partitions[rwa_helicopter_partition].set(N00H);
    rwa_codomain_set.set(0);
    r1.relate(rwa_helicopter_partition, N00H, 0);
    r1.printPartitionGenMatrix(&rwa_partitions, rwa_codomain_set, "Helicopter as RWA");
    try testing.expect(r1.isPartitionGenConsistent(&rwa_partitions, rwa_codomain_set));

    // Relate an airplane instance.
    const N00A: usize = 0;
    fwa_partitions[fwa_airplane_partition].set(N00A);
    fwa_codomain_set.set(0);
    r2.relate(fwa_airplane_partition, N00A, 0);
    r2.printPartitionGenMatrix(&fwa_partitions, fwa_codomain_set, "Airplane as FWA");
    try testing.expect(r2.isPartitionGenConsistent(&fwa_partitions, fwa_codomain_set));

    // Navigate R1 from Rotary Winged Aircraft to Helicopter
    // to find all the helicopters (one in this case).
    var rwa_set = R1.makeSingularCodomainSet(0);
    var helicopters = r1.preimage(rwa_helicopter_partition, rwa_set);
    try testing.expectEqual(@as(usize, 1), helicopters.count());

    // Navigate R2 from Fixed Winged Aircraft to Airplane
    // to find all the airplanes (one in this case).
    var fwa_set = R2.makeSingularCodomainSet(0);
    var airplanes = r2.preimage(fwa_airplane_partition, fwa_set);
    try testing.expectEqual(@as(usize, 1), airplanes.count());

    // Relate a vtol instance as a rotary wing aircraft.
    const N00V: usize = 0;
    rwa_partitions[rwa_vtol_partition].set(N00V);
    rwa_codomain_set.set(1);
    r1.relate(rwa_vtol_partition, N00V, 1);
    r1.printPartitionGenMatrix(&rwa_partitions, rwa_codomain_set, "Helicopter / VTOL as RWA");
    try testing.expect(r1.isPartitionGenConsistent(&rwa_partitions, rwa_codomain_set));

    // Relate the same vtol instance as a fixed wing aircraft.
    fwa_partitions[fwa_vtol_partition].set(N00V);
    fwa_codomain_set.set(1);
    r2.relate(fwa_vtol_partition, N00V, 1);
    r2.printPartitionGenMatrix(&fwa_partitions, fwa_codomain_set, "Airplane / VTOL as FWA");
    try testing.expect(r2.isPartitionGenConsistent(&fwa_partitions, fwa_codomain_set));

    // Navigate R1 to find all the vtol aircraft (one in this case).
    rwa_set = R1.makeSingularCodomainSet(1);
    var vtol_rwa = r1.preimage(rwa_vtol_partition, rwa_set);
    try testing.expectEqual(@as(usize, 1), vtol_rwa.count());
    // Navigate R2 to find all the vtol aircraft (one in this case).
    fwa_set = R2.makeSingularCodomainSet(1);
    var vtol_fwa = r2.preimage(fwa_vtol_partition, fwa_set);
    try testing.expectEqual(@as(usize, 1), vtol_fwa.count());

    // The vtol domain element is one and the same for both navigations.
    var vtol_rwa_element = vtol_rwa.findFirstSet().?;
    var vtol_fwa_element = vtol_fwa.findFirstSet().?;
    try testing.expectEqual(vtol_rwa_element, vtol_fwa_element);

    // Navigate R1 from the vtol aircraft to the rotary wing aircraft.
    var r1_vtol_set = R1.makeSingularDomainSet(rwa_vtol_partition, N00V);
    rwa_set = r1.image(rwa_vtol_partition, r1_vtol_set);
    try testing.expectEqual(@as(usize, 1), rwa_set.count());
    try testing.expectEqual(rwa_set.findFirstSet().?, 1);

    // Navigate R2 from the vtol aircraft to the fixed wing aircraft.
    var r2_vtol_set = R2.makeSingularDomainSet(fwa_vtol_partition, N00V);
    fwa_set = r2.image(fwa_vtol_partition, r2_vtol_set);
    try testing.expectEqual(@as(usize, 1), fwa_set.count());
    try testing.expectEqual(fwa_set.findFirstSet().?, 1);

    // Navigate R1 to find all the vtol aircraft and then navigate R2 to
    // find the fixed wing aircraft counterpart. This is just the composition
    // of the preimage over R1 followed by the image over R2.
    rwa_set = R1.makeSingularCodomainSet(1);
    fwa_set = r2.image(fwa_vtol_partition, r1.preimage(rwa_vtol_partition, rwa_set));
    try testing.expectEqual(@as(usize, 1), fwa_set.count());
    try testing.expectEqual(fwa_set.findFirstSet().?, 1);

    // Unrelating the vtol instances, makes the generalization inconsistent.
    r1.unrelateDomainElement(rwa_vtol_partition, N00V);
    r1.printPartitionGenMatrix(&rwa_partitions, rwa_codomain_set, "R1 after unrelate of N00V");
    try testing.expect(!r1.isPartitionGenConsistent(&rwa_partitions, rwa_codomain_set));

    r2.unrelateDomainElement(fwa_vtol_partition, N00V);
    r2.printPartitionGenMatrix(&fwa_partitions, fwa_codomain_set, "R2 after unrelate of N00V");
    try testing.expect(!r2.isPartitionGenConsistent(&fwa_partitions, fwa_codomain_set));
}
test "union generalization initialization" {
    const R_38 = UnionGenAdjMatrix(3, 8);

    try testing.expectEqual(@as(u16, 8), R_38.gen_cardinality);
    try testing.expectEqual(@as(u16, 3), R_38.partition_count);

    var r_38 = R_38.init();

    for (&r_38.partitions) |*partition| {
        try testing.expectEqual(@as(usize, 0), partition.count());
    }

    testing.refAllDecls(R_38);
}
test "union generalization relating" {
    const R_36 = UnionGenAdjMatrix(3, 6);
    var r_36 = R_36.init();

    var gen_universe = R_36.UnionGenSet.initFull();

    r_36.relatePairs(0, &[_]usize{0, 2});
    try testing.expect(r_36.partitions[0].isSet(0));
    try testing.expect(r_36.partitions[0].isSet(2));

    r_36.relatePairs(1, &[_]usize{1, 3, 4});
    try testing.expect(r_36.partitions[1].isSet(1));
    try testing.expect(r_36.partitions[1].isSet(3));
    try testing.expect(r_36.partitions[1].isSet(4));

    r_36.relate(2, 5);
    try testing.expect(r_36.partitions[2].isSet(5));
    r_36.printUnionGenMatrix (gen_universe, "before reclassification");

    r_36.reclassify(0, 5);
    try testing.expect(r_36.partitions[0].isSet(5));
    r_36.printUnionGenMatrix (gen_universe, "after reclassification");
}
test "union generalization image and preimage tests" {
    // This example uses the layout of the repeated generalization diagram
    // seen previously.
    // First we layout some symbolic names for R1.
    const vehicle_count: u16 = 5;
    const car_partition: usize = 0;
    const truck_partition: usize = 1;
    const bus_partition: usize = 2;
    const R1_partition_count: u16 = 3;

    const R1 = UnionGenAdjMatrix(R1_partition_count, vehicle_count);
    var r1 = R1.init();

    var vehicle_universe = R1.emptyCodomainSet;

    // Next we populate the generalization. The names are chosen
    // to reflect the type of vehicle for R1.
    const chevy: usize = 0;
    const ford: usize = 1;
    const freightliner: usize = 2;
    const peterbilt: usize = 3;
    const collins: usize = 4;

    // Populate the domain partitions of R1.
    vehicle_universe.set(chevy);
    vehicle_universe.set(ford);
    r1.relatePairs(car_partition, &[_]usize {chevy, ford});

    vehicle_universe.set(freightliner);
    vehicle_universe.set(peterbilt);
    r1.relatePairs(truck_partition, &[_]usize {freightliner, peterbilt});

    vehicle_universe.set(collins);
    r1.relate(bus_partition, collins);
    try testing.expect(r1.isUnionGenConsistent(vehicle_universe));

    var r1_truck_classification = r1.classify(freightliner);
    try testing.expectEqual(truck_partition, r1_truck_classification);
    var r1_bus_classification = r1.classify(collins);
    try testing.expectEqual(bus_partition, r1_bus_classification);

    // Configure R2.
    const box_truck_partition: usize = 0;
    const semi_truck_partition: usize = 1;
    const R2_partition_count: u16 = 2;

    const R2 = UnionGenAdjMatrix(R2_partition_count, vehicle_count);
    var r2 = R2.init();

    // For the repeated generalization, we have to state how the Truck
    // elements are partitioned. Note we must use the same element numbers
    // as the Truck elements in R1.
    r2.relate(box_truck_partition, freightliner);
    r2.relate(semi_truck_partition, peterbilt);

    // Truck is a domain partition for R1 and the codomain for R2.
    // So we use the Truck partition of R1 as the codomain universe for R2.
    const truck_universe = &r1.partitions[truck_partition];
    try testing.expect(r2.isUnionGenConsistent(truck_universe.*));

    // Navigate from the peterbilt semi to vehicle across R2 then R1.
    // This is just the composition of the navigating R2 followed by R1.
    const peterbilt_set = R2.makeSingularUnionGenSet(peterbilt);
    var vehicle_set = r1.image(truck_partition,
            r2.image(semi_truck_partition, peterbilt_set));
    try testing.expectEqual(@as(usize, 1), vehicle_set.count());
    try testing.expect(vehicle_set.isSet(peterbilt));
}
