Skip to content

ragraph.analysis.compatibility

Compatibility analysis

Calculate compatibility between different variants for several functional elements.

CompatibilityAnalysis

CompatibilityAnalysis(
    graph: Graph,
    variants: Dict[str, List[Node]],
    interface_method: Optional[
        Callable[[Graph, Node, Node], bool]
    ] = None,
    compatibility_method: Callable[
        [Graph, Node, Node], bool
    ] = get_compatibility_method(
        compatibility_kind="compatibility",
        incompatibility_kind=None,
    ),
    interface_compatibility: bool = True,
    score_method: Callable[
        [Tuple[Node, ...]], float
    ] = get_score_method(
        variant_agg="sum", config_agg="sum", weights=None
    ),
    constraints: Optional[List[Node]] = None,
    applicability_method: Callable[
        [Graph, Node, Node], bool
    ] = get_applicability_method(
        applicability_kind="applicability",
        inapplicability_kind=None,
    ),
)

Compatibility analysis class.

The input graph is expected to contain nodes that represent variants of certain (functional) elements. A variant is a variation of an element that fulfills the same functionality as other variants of that elements.

Parameters:

Name Type Description Default
graph Graph

Graph containing compatibility data between different variants.

required
variants Dict[str, List[Node]]

Dictionary of element names to lists of variant nodes.

required
interface_method Optional[Callable[[Graph, Node, Node], bool]]

An optional method accepting a Graph and two variant nodes that returns whether the two given variants have an interface. Acts as a guard for whether compatibility checking is necessary when interface_compatibility is set.

None
compatibility_method Callable[[Graph, Node, Node], bool]

A method accepting a Graph and two variant nodes to check whether the two given variants are compatible (by returning True).

get_compatibility_method(compatibility_kind='compatibility', incompatibility_kind=None)
interface_compatibility bool

When set, the compatibility of two variants is only checked when they share an interface.

True
score_method Callable[[Tuple[Node, ...]], float]

A method accepting a tuple of variant nodes and returning a single performance score. See get_score_method.

get_score_method(variant_agg='sum', config_agg='sum', weights=None)
constraints Optional[List[Node]]

Optional list of constraint nodes ("project scope") for which variants must be applicable in order to be taken into account.

None
applicability_method Callable[[Graph, Node, Node], bool]

A method accepting a graph, a variant node and a constraint node that returns whether a variant is applicable when that constraint is set.

get_applicability_method(applicability_kind='applicability', inapplicability_kind=None)
Source code in ragraph/analysis/compatibility/__init__.py
def __init__(
    self,
    graph: Graph,
    variants: Dict[str, List[Node]],
    interface_method: Optional[Callable[[Graph, Node, Node], bool]] = None,
    compatibility_method: Callable[[Graph, Node, Node], bool] = get_compatibility_method(
        compatibility_kind="compatibility", incompatibility_kind=None
    ),
    interface_compatibility: bool = True,
    score_method: Callable[[Tuple[Node, ...]], float] = get_score_method(
        variant_agg="sum", config_agg="sum", weights=None
    ),
    constraints: Optional[List[Node]] = None,
    applicability_method: Callable[[Graph, Node, Node], bool] = get_applicability_method(
        applicability_kind="applicability", inapplicability_kind=None
    ),
):
    self.graph = graph
    """Graph containing compatibility data between different variants."""

    self.interface_method = interface_method
    """An optional method accepting a Graph and two variant nodes that returns whether the two
    given variants have an interface. Acts as a guard for whether compatibility checking is
    necessary when `interface_compatibility` is set."""

    self.compatibility_method = compatibility_method
    """A method accepting a Graph and two variant nodes to check whether the two given variants
    are compatible (by returning `True`)."""

    self.interface_compatibility = interface_compatibility
    """When set, the compatibility of two variants is only checked when they share an interface.
    """

    self.applicability_method = applicability_method
    """A method accepting a graph, a variant node and a constraint node that returns whether a
    variant is applicable when that constraint is set."""

    self.score_method = score_method
    """A method accepting a tuple of variant nodes and returning a single performance score.
    See [`get_score_method`][ragraph.analysis.compatibility.get_score_method]."""

    self._elements: List[str] = []
    self._element_nums: List[int] = []
    self._disabled_elements: List[str] = []

    self._constraints: List[Node] = [] if constraints is None else constraints

    self._variants: Dict[str, List[Node]] = dict()
    self._variants_list: List[Node] = []
    self.variants = variants

applicability_method instance-attribute

applicability_method = applicability_method

A method accepting a graph, a variant node and a constraint node that returns whether a variant is applicable when that constraint is set.

compatibility_method instance-attribute

compatibility_method = compatibility_method

A method accepting a Graph and two variant nodes to check whether the two given variants are compatible (by returning True).

constraints property writable

constraints: List[Node]

List of constraint nodes.

disabled_elements property

disabled_elements: List[str]

Disabled elements with no applicable variant. See self.constraints and self.applicability_method.

element_nums property

element_nums: List[int]

Number of applicable variants per element.

elements property

elements: List[str]

Enabled elements with at least one applicable variant. See self.constraints and self.applicability_method.

graph instance-attribute

graph = graph

Graph containing compatibility data between different variants.

interface_compatibility instance-attribute

interface_compatibility = interface_compatibility

When set, the compatibility of two variants is only checked when they share an interface.

interface_method instance-attribute

interface_method = interface_method

An optional method accepting a Graph and two variant nodes that returns whether the two given variants have an interface. Acts as a guard for whether compatibility checking is necessary when interface_compatibility is set.

score_method instance-attribute

score_method = score_method

A method accepting a tuple of variant nodes and returning a single performance score. See get_score_method.

variants property writable

variants: Dict[str, List[Node]]

Variants as lists by element name.

variants_list property

variants_list: List[Node]

Flat list of applicable variants sorted by (enabled) element.

get_compatibility_matrix

1
2
3
get_compatibility_matrix(
    variants: Optional[List[Node]] = None,
) -> Union[np.ndarray, List[List[float]]]

Compatibility matrix between variants.

Parameters:

Name Type Description Default
variants Optional[List[Node]]

Optional list of variants to return the compatibility matrix for. Also see compatibility_method.

None

Returns:

Type Description
Union[ndarray, List[List[float]]]

Compatibility matrix as a list of lists or numpy array (if numpy is

Union[ndarray, List[List[float]]]

available).

Source code in ragraph/analysis/compatibility/__init__.py
def get_compatibility_matrix(
    self, variants: Optional[List[Node]] = None
) -> Union["np.ndarray", List[List[float]]]:
    """Compatibility matrix between variants.

    Arguments:
        variants: Optional list of variants to return the compatibility matrix for.
            Also see `compatibility_method`.

    Returns:
        Compatibility matrix as a list of lists or numpy array (if numpy is
        available).
    """
    variants = self._variants_list if variants is None else variants
    dim = len(variants)
    mat = (
        np.zeros((dim, dim)) if HAVE_NUMPY else [[0.0 for j in range(dim)] for i in range(dim)]
    )
    for i in range(dim):
        var1 = variants[i]
        for j in range(i + 1, dim):
            if self.is_compatible(var1, variants[j]):
                mat[i][j] = 1.0
                mat[j][i] = 1.0
    return mat

get_config_score

get_config_score(config: Tuple[Node, ...]) -> float

Score a configuration. Does NOT check if the supplied config is valid!

Parameters:

Name Type Description Default
config Tuple[Node, ...]

Configuration as a tuple of variant nodes.

required

Returns:

Type Description
float

Configuration score.

Source code in ragraph/analysis/compatibility/__init__.py
def get_config_score(self, config: Tuple[Node, ...]) -> float:
    """Score a configuration. Does NOT check if the supplied config is valid!

    Arguments:
        config: Configuration as a tuple of variant nodes.

    Returns:
        Configuration score.
    """
    return self.score_method(config)

get_plot_graph

get_plot_graph() -> Graph

Get a plot ready graph for the current compatibility analysis problem.

Source code in ragraph/analysis/compatibility/__init__.py
def get_plot_graph(self) -> Graph:
    """Get a plot ready graph for the current compatibility analysis problem."""
    g = Graph(add_parents=True, add_children=True)
    variants = deepcopy(self.variants_list)
    for variant in variants:
        g.add_node(variant)

    for i, el1 in enumerate(self.elements[:-1]):
        variants1 = self.variants[el1]
        for el2 in self.elements[i + 1 :]:
            variants2 = self.variants[el2]
            for var1 in variants1:
                for var2 in variants2:
                    e1 = Edge(
                        g[var1.name],
                        g[var2.name],
                        kind="compatibility",
                        labels=[],
                    )
                    if self.has_interface(var1, var2):
                        e1.labels.append("interface")
                        e1.labels.append(
                            "compatible" if self.is_compatible(var1, var2) else "incompatible"
                        )
                    else:
                        e1.labels.append("no interface")
                        e1.labels.append("compatible")
                        e1.weights["scale"] = 0.25
                    e2 = Edge(
                        g[var2.name],
                        g[var1.name],
                        kind="compatibility",
                        labels=e1.labels.copy(),
                        weights=e1.weights.copy(),
                    )
                    g.add_edge(e1)
                    g.add_edge(e2)
    return g

get_ranked_configurations

1
2
3
get_ranked_configurations(
    descending: bool = True,
) -> List[Tuple[Tuple[Node, ...], float]]

Get the feasible configurations, sorted by score.

Parameters:

Name Type Description Default
descending bool

Whether highest scores should go first.

True

Returns:

Type Description
List[Tuple[Tuple[Node, ...], float]]

Sorted tuples of configurations and scores.

Source code in ragraph/analysis/compatibility/__init__.py
def get_ranked_configurations(
    self, descending: bool = True
) -> List[Tuple[Tuple[Node, ...], float]]:
    """Get the feasible configurations, sorted by score.

    Arguments:
        descending: Whether highest scores should go first.

    Returns:
        Sorted tuples of configurations and scores.
    """
    return sorted(self.yield_scored_configurations(), key=lambda x: x[-1], reverse=descending)

has_interface

has_interface(var1: Node, var2: Node) -> bool

Whether two variants have an interface that needs a compatibility check.

Source code in ragraph/analysis/compatibility/__init__.py
def has_interface(self, var1: Node, var2: Node) -> bool:
    """Whether two variants have an interface that needs a compatibility check."""
    return (self.interface_method is None) or self.interface_method(self.graph, var1, var2)

is_applicable

is_applicable(variant: Node) -> bool

Whether a variant is applicable to the currently set of constraints.

Source code in ragraph/analysis/compatibility/__init__.py
def is_applicable(self, variant: Node) -> bool:
    """Whether a variant is applicable to the currently set of constraints."""
    if self.constraints:
        return all(
            self.applicability_method(self.graph, variant, constraint)
            for constraint in self.constraints
        )
    return True

is_compatible

is_compatible(var1: Node, var2: Node) -> bool

Whether the two given variants are compatible according to self.applicability_method

Source code in ragraph/analysis/compatibility/__init__.py
def is_compatible(self, var1: Node, var2: Node) -> bool:
    """Whether the two given variants are compatible according to
    [`self.applicability_method`][ragraph.analysis.compatibility.CompatibilityAnalysis.applicability_method]
    """

    if self.interface_compatibility:
        return (
            self.compatibility_method(self.graph, var1, var2)
            if self.has_interface(var1, var2)
            else True
        )

    return self.compatibility_method(self.graph, var1, var2)

plot

plot(**mdm_args) -> go.Figure

Visualize the compatibility analysis problem.

Source code in ragraph/analysis/compatibility/__init__.py
def plot(self, **mdm_args) -> "go.Figure":
    """Visualize the compatibility analysis problem."""
    g = self.get_plot_graph()
    variants = [g[n.name] for n in self.variants_list]

    # Merge styles if provided.
    if "style" in mdm_args:
        style = deepcopy(self.style)
        style.update(mdm_args["style"])
        mdm_args["style"] = style
    else:
        mdm_args["style"] = self.style

    return mdm(leafs=variants, edges=g.edges, **mdm_args)

write_csv

1
2
3
4
5
write_csv(
    path: Union[str, Path],
    scored: bool = True,
    limit: int = -1,
) -> int

Write feasible configurations and optional scores to a CSV file.

Source code in ragraph/analysis/compatibility/__init__.py
def write_csv(self, path: Union[str, Path], scored: bool = True, limit: int = -1) -> int:
    """Write feasible configurations and optional scores to a CSV file."""
    path = Path(path)
    num = 0
    with open(path, mode="w+", encoding="utf-8") as f:
        f.write(";".join(self._elements))

    def append(content: str):
        with open(path, mode="a", encoding="utf-8") as f:
            f.write(content)

    if scored:
        gen = self.yield_scored_configurations()
        append(";score\n")
        if limit > 0:
            while num < limit:
                cfg, score = next(gen)
                append(";".join(n.name for n in cfg))
                append(f";{score}\n")
                num += 1
        else:
            for cfg, score in gen:
                append(";".join(n.name for n in cfg))
                append(f";{score}\n")
                num += 1
    else:
        cfgs = self.yield_feasible_configurations()
        append("\n")
        if limit > 0:
            while num < limit:
                cfg = next(cfgs)
                append(";".join(n.name for n in cfg))
                append("\n")
                num += 1
        else:
            for cfg in cfgs:
                append(";".join(n.name for n in cfg))
                append("\n")
                num += 1
    return num

yield_feasible_configurations

1
2
3
yield_feasible_configurations() -> (
    Generator[Tuple[Node, ...], None, None]
)

Yield the feasible configurations for this problem.

Source code in ragraph/analysis/compatibility/__init__.py
def yield_feasible_configurations(self) -> Generator[Tuple[Node, ...], None, None]:
    """Yield the feasible configurations for this problem."""
    variants = self._variants_list
    compat = self.get_compatibility_matrix(variants)
    configs = yield_feasible_configurations(compat, self._element_nums)
    for config in configs:
        yield tuple(variants[idx] for idx in config)

yield_scored_configurations

1
2
3
yield_scored_configurations() -> (
    Generator[Tuple[Tuple[Node, ...], float], None, None]
)

Yield the feasible configurations and their scores.

Source code in ragraph/analysis/compatibility/__init__.py
def yield_scored_configurations(
    self,
) -> Generator[Tuple[Tuple[Node, ...], float], None, None]:
    """Yield the feasible configurations and their scores."""
    for config in self.yield_feasible_configurations():
        yield config, self.get_config_score(config)

get_applicability_method

1
2
3
4
get_applicability_method(
    applicability_kind: Optional[str] = "applicability",
    inapplicability_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]

Get a method dat signals whether an element is applicable when a certain constraint is active.

Parameters:

Name Type Description Default
applicability_kind Optional[str]

Optional Edge kind that signals variant applicability.

'applicability'
inapplicability_kind Optional[str]

Optional Edge kind that signals variant inapplicability.

None

Returns:

Type Description
Callable[[Graph, Node, Node], bool]

Method of checking applicability of an element for a constraint. Takes a Graph

Callable[[Graph, Node, Node], bool]

containing Edge data and an element and a constraint node to check.

Source code in ragraph/analysis/compatibility/__init__.py
def get_applicability_method(
    applicability_kind: Optional[str] = "applicability",
    inapplicability_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]:
    """Get a method dat signals whether an element is applicable when a certain
    constraint is active.

    Arguments:
        applicability_kind: Optional Edge kind that signals variant applicability.
        inapplicability_kind: Optional Edge kind that signals variant inapplicability.

    Returns:
        Method of checking applicability of an element for a constraint. Takes a Graph
        containing Edge data and an element and a constraint node to check.
    """
    if applicability_kind is None and inapplicability_kind is None:
        raise ValueError("Applicability and inapplicability kind can't both be None.")
    if applicability_kind and inapplicability_kind:
        raise ValueError("Applicability and inapplicability kind can't both be set.")

    def is_applicable(graph: Graph, element: Node, constraint: Node) -> bool:
        """Check whether these two nodes are compatible."""
        applicable = (
            True
            if applicability_kind is None
            else any(e.kind == applicability_kind for e in graph[element.name, constraint.name])
            or any(e.kind == applicability_kind for e in graph[constraint.name, element.name])
        )
        if not applicable:
            return False
        inapplicable = (
            False
            if inapplicability_kind is None
            else any(e.kind == inapplicability_kind for e in graph[element.name, constraint.name])
            or any(e.kind == inapplicability_kind for e in graph[constraint.name, element.name])
        )
        return not inapplicable

    return is_applicable

get_compatibility_method

1
2
3
4
get_compatibility_method(
    compatibility_kind: Optional[str] = "compatibility",
    incompatibility_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]

Get a method dat signals whether two variants are compatible.

Parameters:

Name Type Description Default
compatibility_kind Optional[str]

Optional Edge kind that signals variant compatibility.

'compatibility'
incompatibility_kind Optional[str]

Optional Edge kind that signals variant incompatibility.

None

Returns:

Type Description
Callable[[Graph, Node, Node], bool]

Method of checking compatibility of two variants as nodes. Takes a Graph

Callable[[Graph, Node, Node], bool]

containing Edge data and an A and a B node to check.

Source code in ragraph/analysis/compatibility/__init__.py
def get_compatibility_method(
    compatibility_kind: Optional[str] = "compatibility",
    incompatibility_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]:
    """Get a method dat signals whether two variants are compatible.

    Arguments:
        compatibility_kind: Optional Edge kind that signals variant compatibility.
        incompatibility_kind: Optional Edge kind that signals variant incompatibility.

    Returns:
        Method of checking compatibility of two variants as nodes. Takes a Graph
        containing Edge data and an A and a B node to check.
    """
    if compatibility_kind is None and incompatibility_kind is None:
        raise ValueError("Compatibility and incompatibility kind can't both be None.")
    if compatibility_kind and incompatibility_kind:
        raise ValueError("Compatibility and incompatibility kind can't both be set.")

    def is_compatible(graph: Graph, var1: Node, var2: Node) -> bool:
        """Check whether these two nodes are compatible."""
        compatible = (
            True
            if compatibility_kind is None
            else any(e.kind == compatibility_kind for e in graph[var1.name, var2.name])
            or any(e.kind == compatibility_kind for e in graph[var2.name, var1.name])
        )
        if not compatible:
            return False
        incompatible = (
            False
            if incompatibility_kind is None
            else any(e.kind == incompatibility_kind for e in graph[var1.name, var2.name])
            or any(e.kind == incompatibility_kind for e in graph[var2.name, var1.name])
        )
        return not incompatible

    return is_compatible

get_configuration_score

1
2
3
4
get_configuration_score(
    variant_scores: Iterable[float],
    aggregation: str = "sum",
) -> float

Score a configuration by aggregating variant scores by either taking the sum or product.

Parameters:

Name Type Description Default
variant_scores Iterable[float]

List of scores for all selected variants.

required

Returns:

Type Description
float

Config score.

Source code in ragraph/analysis/compatibility/__init__.py
def get_configuration_score(variant_scores: Iterable[float], aggregation: str = "sum") -> float:
    """Score a configuration by aggregating variant scores by either taking the
    sum or product.

    Arguments:
        variant_scores: List of scores for all selected variants.

    Returns:
        Config score.
    """
    if aggregation == "sum":
        return sum(variant_scores)
    return prod(variant_scores)

get_interface_method

1
2
3
get_interface_method(
    interface_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]

Get a basic interface checking method. Will check for the existence of edges between the ascendants of the variants (optionally filtered with an edge kind).

Source code in ragraph/analysis/compatibility/__init__.py
def get_interface_method(
    interface_kind: Optional[str] = None,
) -> Callable[[Graph, Node, Node], bool]:
    """Get a basic interface checking method. Will check for the existence of edges
    between the ascendants of the variants (optionally filtered with an edge kind).
    """

    def has_interface(graph: Graph, var1: Node, var2: Node) -> bool:
        return any(
            e
            for e in graph.edges_between_all(var1.ancestor_gen, var2.ancestor_gen)
            if interface_kind is None or e.kind == interface_kind
        )

    return has_interface

get_score_method

1
2
3
4
5
get_score_method(
    variant_agg: str = "sum",
    config_agg: str = "sum",
    weights: Optional[List[str]] = None,
) -> Callable[[Tuple[Node, ...]], float]

Get a configuration scoring method.

Parameters:

Name Type Description Default
variant_agg str

Variant node weights aggregation method. Either "sum" or "product".

'sum'
config_agg str

Variant nodes' score aggregation method. Either "sum" or "product".

'sum'
weights Optional[List[str]]

Optional selection of node weights to take into account.

None

Returns:

Type Description
Callable[[Tuple[Node, ...]], float]

A method accepting a tuple of variant nodes and returning a single

Callable[[Tuple[Node, ...]], float]

performance score.

Source code in ragraph/analysis/compatibility/__init__.py
def get_score_method(
    variant_agg: str = "sum",
    config_agg: str = "sum",
    weights: Optional[List[str]] = None,
) -> Callable[[Tuple[Node, ...]], float]:
    """Get a configuration scoring method.

    Arguments:
        variant_agg: Variant node weights aggregation method. Either "sum" or "product".
        config_agg: Variant nodes' score aggregation method. Either "sum" or "product".
        weights: Optional selection of node weights to take into account.

    Returns:
        A method accepting a tuple of variant nodes and returning a single
        performance score.
    """

    def get_score(config: Tuple[Node, ...]) -> float:
        """Get the performance score of a configuration of element variants."""
        variant_scores = [get_variant_score(n, weights, variant_agg) for n in config]
        config_score = get_configuration_score(variant_scores, config_agg)
        return config_score

    return get_score

get_variant_score

1
2
3
4
5
get_variant_score(
    variant: Node,
    weights: Optional[List[str]] = None,
    aggregation: str = "sum",
) -> float

Score a single variant node as an aggregation of selected weights.

Parameters:

Name Type Description Default
variant Node

Variant node.

required
weights Optional[List[str]]

Optional list of weights to take into account.

None
aggregation str

Aggregation method, either "sum" or "product".

'sum'

Returns:

Type Description
float

Variant score.

Source code in ragraph/analysis/compatibility/__init__.py
def get_variant_score(
    variant: Node, weights: Optional[List[str]] = None, aggregation: str = "sum"
) -> float:
    """Score a single variant node as an aggregation of selected weights.

    Arguments:
        variant: Variant node.
        weights: Optional list of weights to take into account.
        aggregation: Aggregation method, either "sum" or "product".

    Returns:
        Variant score.
    """
    values = [variant.weights[k] for k in weights] if weights else variant.weights.values()
    if aggregation == "sum":
        return sum(values)
    return prod(values)

is_feasible

1
2
3
4
is_feasible(
    compat: Union[np.ndarray, List[List[Union[bool, int]]]],
    config: Tuple[int, ...],
) -> bool

Return whether this configuration is feasible based on a compatibility matrix.

Source code in ragraph/analysis/compatibility/__init__.py
def is_feasible(
    compat: Union["np.ndarray", List[List[Union[bool, int]]]], config: Tuple[int, ...]
) -> bool:
    """Return whether this configuration is feasible based on a compatibility matrix."""
    coords = combinations(config, 2)
    return all(compat[row][col] > 0 for row, col in coords)

bdd

Binary Decision Diagram compatibility calculation

Binary Decision Diagram (BDD) implementation of the compatibility problem.

_recursive

1
2
3
4
5
_recursive(
    operation: str,
    bdd: _bdd.BDD,
    items: List[_bdd.Function],
) -> _bdd.Function

Construct a recursive operation function over all items.

Source code in ragraph/analysis/compatibility/bdd.py
def _recursive(operation: str, bdd: _bdd.BDD, items: List[_bdd.Function]) -> _bdd.Function:
    """Construct a recursive operation function over all items."""
    if len(items) == 1:
        return items[0]
    return bdd.apply(operation, items[0], _recursive(operation, bdd, items[1:]))

_yield_ranges

1
2
3
_yield_ranges(
    nums: List[int], start: int = 0
) -> Generator[range, None, None]

Generate matrix index ranges.

Source code in ragraph/analysis/compatibility/bdd.py
def _yield_ranges(nums: List[int], start: int = 0) -> Generator[range, None, None]:
    """Generate matrix index ranges."""
    for i in nums:
        yield range(start, start + i)
        start += i

yield_feasible_configurations

1
2
3
4
yield_feasible_configurations(
    compat: Union[np.ndarray, List[List[Union[bool, int]]]],
    comp_variant_nums: List[int],
) -> Generator[Tuple[int, ...], None, None]

Get the feasible configurations based on a compatibility matrix between different variants of elements and the "bucket" sizes (number of variants of each element).

Parameters:

Name Type Description Default
compat Union[ndarray, List[List[Union[bool, int]]]]

Compatibility matrix (square) between different variants of elements. Size is determined by the total number of variants.

required
comp_variant_nums List[int]

The number of variants for each element. The matrix has to be sorted accordingly.

required

Returns:

Type Description
Generator[Tuple[int, ...], None, None]

Feasible configurations as a tuple with a variant's (absolute) index for

Generator[Tuple[int, ...], None, None]

each element.

Source code in ragraph/analysis/compatibility/bdd.py
def yield_feasible_configurations(
    compat: Union["np.ndarray", List[List[Union[bool, int]]]],
    comp_variant_nums: List[int],
) -> Generator[Tuple[int, ...], None, None]:
    """Get the feasible configurations based on a compatibility matrix between different
    variants of elements and the "bucket" sizes (number of variants of each element).

    Arguments:
        compat: Compatibility matrix (square) between different variants of elements.
            Size is determined by the total number of variants.
        comp_variant_nums: The number of variants for each element. The matrix has to
            be sorted accordingly.

    Returns:
        Feasible configurations as a tuple with a variant's (absolute) index for
        each element.
    """
    bdd = _bdd.BDD()
    dim = len(compat)

    # All constraints that should be met.
    constraints = []

    # Variant toggles
    toggles = [bdd.add_var(i) for i in range(dim)]

    # Variant incompatibilities
    for i in range(dim):
        row = bdd.var(i)
        for j in range(i + 1, dim):
            if compat[i][j]:
                continue
            col = bdd.var(j)
            constraints.append(bdd.apply("=>", row, ~col))

    # Element picking constraints.
    ranges = _yield_ranges(comp_variant_nums)
    for rng in ranges:
        if rng.stop - rng.start == 1:
            constraints.append(bdd.var(rng.start))
            continue
        # Adding this xor guarantees an uneven number is selected.
        constraints.append(_recursive("xor", bdd, [bdd.var(i) for i in rng]))
        # Adding the ands guarantees no duo is selected.
        ands = [
            bdd.apply("=>", bdd.var(i), ~bdd.var(j))
            for i in range(rng.start, rng.stop - 1)
            for j in range(i + 1, rng.stop)
        ]
        constraints.append(_recursive("and", bdd, ands))

    # Construct system and yield solutions.
    system = _recursive("and", bdd, constraints)
    for config in bdd.pick_iter(system, care_vars=toggles):
        yield tuple(k for k, v in config.items() if v)