Beispiel #1
0
def compute_trapspaces_that_intersect_subspace(primes: dict, subspace: dict, type_: str, fname_asp: str = None, representation: str = "dict", max_output: int = 1000) -> List[dict]:
    """
    Computes trap spaces that have non-empty intersection with *subspace*

    **arguments**:
        * *primes*: prime implicants
        * *subspace*: a subspace in dict format
        * *type_*: either "min", "max", "all" or "percolated"
        * *fname_asp*: file name or *None*
        * *representation*: either "str" or "dict", the representation of the trap spaces
        * *max_output*: maximum number of returned solutions

    **returns**:
        * *trap_spaces*: the trap spaces that have non-empty intersection with *subspace*

    **example**::

        >>> compute_trapspaces_that_intersect_subspace(primes, {"v1":1,"v2":0,"v3":0})
    """
    
    assert (len(primes) >= len(subspace))
    assert (type(subspace) in [dict, str])
    
    if type(subspace) == str:
        subspace = subspace2dict(primes, subspace)
    
    relevant_primes = active_primes(primes, subspace)
    
    bounds = None
    if type_ == "max":
        bounds = (1, "n")

    tspaces = potassco_handle(primes=relevant_primes, type_=type_, bounds=bounds, project=[], max_output=max_output, fname_asp=fname_asp, representation=representation)
    
    if not tspaces:
        answer = {}
        
        if representation == "str":
            answer = subspace2str(primes, answer)
        
        return [answer]
    
    if len(subspace) == len(primes) and type_ == "min":
        if len(tspaces) > 1:
            log.error("the smallest trap space containing a state (or other space) must be unique!")
            log.error(f"found {len(tspaces)} smallest tspaces.")
            log.error(tspaces)
            raise Exception
        
        return [tspaces.pop()]
    
    return tspaces
def test_completeness():
    bnet = "\n".join([
        "v0,   v0", "v1,   v2", "v2,   v1", "v3,   v1&v0", "v4,   v2",
        "v5,   v3&!v6", "v6,   v4&v5"
    ])
    primes = bnet2primes(bnet)

    assert completeness(primes, "asynchronous")
    assert not completeness(primes, "synchronous")

    answer, example = completeness_with_counterexample(primes, "synchronous")
    example = state2str(example)
    stg = primes2stg(primes, "synchronous")

    for x in compute_trap_spaces(primes, "min"):
        x = subspace2str(primes, x)

        states = list_states_in_subspace(primes, x)
        states = [state2str(x) for x in states]

        assert not has_path(stg, example, states)

    bnet = "\n".join([
        "v1, !v1&v2&v3 | v1&!v2&!v3", "v2, !v1&!v2 | v1&v3",
        "v3, !v1&v3 | v1&v2", "v4, 1", "v5, v4"
    ])
    primes = bnet2primes(bnet)

    assert not completeness(primes, "asynchronous")

    answer, example = completeness_with_counterexample(primes, "asynchronous")

    assert len(example) == len(primes)
    assert completeness(primes, "synchronous")

    bnet = "\n".join(
        ["v1, !v1&v2&v3 | v1&!v2&!v3", "v2, !v1&!v2 | v1&v3", "v3, v2 | v3"])
    primes = bnet2primes(bnet)

    assert completeness(primes, "asynchronous")
    assert completeness(primes, "synchronous")

    bnet = "\n".join([
        "v1,   !v2", "v2,   v1", "v3,   v1", "v4,   v2", "v5,   v6",
        "v6,   v4&v5", "v7,   v2", "v8,   v5", "v9,   v6&v10", "v10,  v9&v7"
    ])
    primes = bnet2primes(bnet)

    assert completeness(primes, "synchronous")
Beispiel #3
0
def list_input_combinations(primes: dict,
                            format: str = "dict"
                            ) -> Union[List[str], List[dict]]:
    """
    A generator for all possible input combinations of *primes*.
    Returns the empty dictionary if there are no inputs.

    **arguments**:
        * *primes*: prime implicants
        * *format*: format of returned subspaces, "str" or "dict"

    **returns**:
        * *subspaces*: input combination in desired format

    **example**::

        >>> for x in list_input_combinations(primes, "str"): print(x)
        0--0--
        0--1--
        1--0--
        1--1--
    """

    if format not in ["str", "dict"]:
        log.error(f"format must be in ['str', 'dict']: format={format}")
        raise Exception

    inputs = find_inputs(primes)
    if not inputs:
        return [{}]

    subspaces = []
    if format == "dict":
        for x in itertools.product(*len(inputs) * [[0, 1]]):
            subspaces.append(dict(zip(inputs, x)))

    else:
        for x in itertools.product(*len(inputs) * [[0, 1]]):
            x = dict(zip(inputs, x))
            x = subspace2str(primes, x)
            subspaces.append(x)

    return subspaces
Beispiel #4
0
def add_style_mintrapspaces(primes: dict,
                            stg: networkx.DiGraph,
                            max_output: int = 100):
    """
    A convenience function that combines :ref:`add_style_subspaces` and :ref:`trap_spaces <trap_spaces>`.
    It adds a *dot* subgraphs for every minimal trap space to *stg* - subgraphs that already exist are overwritten.

    **arguments**:
        * *primes*: prime implicants
        * *stg*: state transition graph
        * *MaxOutput*: maximal number of minimal trap spaces, see :ref:`trap_spaces <sec:trap_spaces>`

    **example**:

        >>> add_style_mintrapspaces(primes, stg)
    """

    states = stg.nodes()

    for tspace in compute_trap_spaces(primes, "min", max_output=max_output):
        subgraph = networkx.DiGraph()
        subgraph.add_nodes_from([
            x for x in list_states_in_subspace(primes, tspace) if x in states
        ])
        if not subgraph.nodes():
            continue

        subgraph.graph["color"] = "black"
        if len(tspace) == len(primes):
            subgraph.graph["label"] = "steady state"
        else:
            subgraph.graph["label"] = "min trap space %s" % subspace2str(
                primes, tspace)

        if not stg.graph["subgraphs"]:
            stg.graph["subgraphs"] = []

        for x in list(stg.graph["subgraphs"]):
            if sorted(x.nodes()) == sorted(subgraph.nodes()):
                stg.graph["subgraphs"].remove(x)

        stg.graph["subgraphs"].append(subgraph)
Beispiel #5
0
def create_attractor_report(primes: dict,
                            fname_txt: Optional[str] = None) -> str:
    """
    Creates an attractor report for the network defined by *primes*.

    **arguments**:
        * *primes*: prime implicants
        * *fname_txt*: the name of the report file or *None*

    **returns**:
        * *txt*: attractor report as text

    **example**::
         >>> create_attractor_report(primes, "report.txt")
    """

    min_trap_spaces = compute_trap_spaces(primes, "min")
    steady = sorted(x for x in min_trap_spaces if len(x) == len(primes))
    cyclic = sorted(x for x in min_trap_spaces if len(x) < len(primes))
    width = max([12, len(primes)])

    lines = ["", ""]
    lines += ["### Attractor Report"]
    lines += [
        f" * created on {datetime.date.today().strftime('%d. %b. %Y')} using pyboolnet {VERSION}, see https://github.com/hklarner/pyboolnet"
    ]
    lines += [""]

    lines += ["### Steady States"]
    if not steady:
        lines += [" * there are no steady states"]
    else:
        lines += ["| " + "steady state".ljust(width) + " |"]
        lines += ["| " + width * "-" + " | "]

    for x in steady:
        lines += ["| " + subspace2str(primes, x).ljust(width) + " |"]

    lines += [""]
    lines += ["### Asynchronous STG"]
    answer = completeness(primes, "asynchronous")
    lines += [f" * completeness: {answer}"]

    if not cyclic:
        lines += [" * there are only steady states"]
    else:
        lines += [""]
        line = "| " + "trapspace".ljust(width) + " | univocal  | faithful  |"
        lines += [line]
        lines += ["| " + width * "-" + " | --------- | --------- |"]

    for x in cyclic:
        line = "| " + subspace2str(primes, x).ljust(width)
        line += " | " + str(univocality(primes, "asynchronous", x)).ljust(9)
        line += " | " + str(faithfulness(primes, "asynchronous",
                                         x)).ljust(9) + " |"
        lines += [line]

    lines += [""]

    lines += ["### Synchronous STG"]
    lines += [f" * completeness: {completeness(primes, 'synchronous')}"]

    if not cyclic:
        lines += [" * there are only steady states"]
    else:
        lines += [""]
        line = "| " + "trapspace".ljust(width) + " | univocal  | faithful  |"
        lines += [line]
        lines += ["| " + width * "-" + "  | --------- | --------- |"]

    for x in cyclic:
        line = "| " + (subspace2str(primes, x)).ljust(width)
        line += " | " + str(univocality(primes, "synchronous", x)).ljust(9)
        line += " | " + str(faithfulness(primes, "synchronous",
                                         x)).ljust(9) + " |"
        lines += [line]

    lines += [""]

    bnet = []

    for row in primes2bnet(primes=primes).split("\n"):
        row = row.strip()
        if not row:
            continue

        t, f = row.split(",")
        bnet.append((t.strip(), f.strip()))

    t_width = max([7] + [len(x) for x, _ in bnet])
    f_width = max([7] + [len(x) for _, x in bnet])
    lines += ["### Network"]
    t, f = bnet.pop(0)
    lines += ["| " + t.ljust(t_width) + " | " + f.ljust(f_width) + " |"]
    lines += ["| " + t_width * "-" + " | " + f_width * "-" + " |"]
    for t, f in bnet:
        lines += ["| " + t.ljust(t_width) + " | " + f.ljust(f_width) + " |"]

    lines += ["", ""]
    text = "\n".join(lines)

    if fname_txt:
        with open(fname_txt, "w") as f:
            f.writelines(text)

        log.info(f"created {fname_txt}")

    return text
Beispiel #6
0
def compute_attractors(primes: dict,
                       update: str,
                       fname_json: Optional[str] = None,
                       check_completeness: bool = True,
                       check_faithfulness: bool = True,
                       check_univocality: bool = True,
                       max_output: int = 1000) -> dict:
    """
    computes all attractors of *primes* including information about completeness, univocality, faithfulness

    **arguments**:
      * *primes*: prime implicants
      * *update*: the update strategy, one of *"asynchronous"*, *"synchronous"*, *"mixed"*
      * *fname_json*: json file name to save result
      * *check_completeness*: enable completeness check
      * *check_faithfulness*: enable faithfulness check
      * *check_univocality*: enable univocality check

    **returns**:
        * *attractors*: attractor data

    **example**::
      >>> attractors = compute_attractors(primes, update, "attractors.json")
    """

    assert update in UPDATE_STRATEGIES
    assert primes

    attractors = dict()
    attractors["primes"] = copy_primes(primes)
    attractors["update"] = update

    min_tspaces = compute_trap_spaces(primes=primes,
                                      type_="min",
                                      max_output=max_output)

    if check_completeness:
        log.info("attractors.completeness(..)")
        if completeness(primes, update, max_output=max_output):
            attractors["is_complete"] = "yes"
        else:
            attractors["is_complete"] = "no"
        log.info(f"{attractors['is_complete']}")
    else:
        attractors["is_complete"] = "unknown"

    attractors["attractors"] = []

    for i, mints in enumerate(min_tspaces):

        mints_obj = dict()
        mints_obj["str"] = subspace2str(primes=primes, subspace=mints)
        mints_obj["dict"] = mints
        mints_obj["prop"] = subspace2proposition(primes=primes, subspace=mints)

        log.info(
            f" working on minimal trapspace {i+1}/{len(min_tspaces)}: {mints_obj['str']}"
        )

        if check_univocality:
            log.info("attractors.univocality(..)")
            if univocality(primes=primes, update=update, trap_space=mints):
                mints_obj["is_univocal"] = "yes"
            else:
                mints_obj["is_univocal"] = "no"
            log.info(f" {mints_obj['is_univocal']}")
        else:
            mints_obj["is_univocal"] = "unknown"

        if check_faithfulness:
            log.info("attractors.faithfulness(..)")
            if faithfulness(primes=primes, update=update, trap_space=mints):
                mints_obj["is_faithful"] = "yes"
            else:
                mints_obj["is_faithful"] = "no"
            log.info(f"{mints_obj['is_faithful']}")
        else:
            mints_obj["is_faithful"] = "unknown"

        log.info("attractors.find_attractor_state_by_randomwalk_and_ctl(..)")
        state = find_attractor_state_by_randomwalk_and_ctl(primes=primes,
                                                           update=update,
                                                           initial_state=mints)

        state_obj = dict()
        state_obj["str"] = state2str(state)
        state_obj["dict"] = state
        state_obj["prop"] = subspace2proposition(primes, state)

        attractor_obj = dict()
        attractor_obj["min_trap_space"] = mints_obj
        attractor_obj["state"] = state_obj
        attractor_obj["is_steady"] = len(mints) == len(primes)
        attractor_obj["is_cyclic"] = len(mints) != len(primes)

        attractors["attractors"].append(attractor_obj)

    attractors["attractors"] = tuple(
        sorted(attractors["attractors"], key=lambda x: x["state"]["str"]))

    if fname_json:
        write_attractors_json(attractors, fname_json)

    return attractors
Beispiel #7
0
def list_reachable_states(primes: dict, update: str, initial_states: List[str],
                          memory: int):
    """
    Performs a depth-first search in the transition system defined by *primes* and *update* to list all states that
    are reachable from the *inital states*. *Memory* specifies the maximum number of states that can be kept in
    memory as "already explored" before the algorithm terminates.

    **arguments**:
        * *primes*: prime implicants
        * *update*: update strategy (either asynchronous or snchronous)
        * *initial_states*: a list of initial states
        * *memory*: maximal number of states memorized before search is stopped

    **returns**:
        * *reachable_states*: a list of all states explored

    **example**::

        >>> initial_states = ["1000", "1001"]
        >>> update = "asynchronous"
        >>> memory = 1000
        >>> states = list_reachable_states(primes, update, initial_states, memory)
        >>> print(len(states))
        287
    """

    if not initial_states:
        return []

    if type(initial_states) in [dict, str]:
        initial_states = [initial_states]

    initial_states = [subspace2str(primes, x) for x in initial_states]

    assert update in ["asynchronous", "synchronous"]

    if update == "asynchronous":
        transition_func = lambda state: successors_asynchronous(primes, state)
    else:
        transition_func = lambda state: [successor_synchronous(primes, state)]

    explored = set([])
    stack = set(initial_states)

    memory_reached = False
    counter = 0

    while stack:
        state = stack.pop()
        new_states = set([state2str(x) for x in transition_func(state)])
        not_explored = new_states.difference(explored)
        stack.update(not_explored)
        explored.add(state2str(state))
        counter += 1

        if len(explored) > memory:
            memory_reached = True
            break

    log.info(f"states explored: {counter}")
    if memory_reached:
        log.info(
            f"result incomplete. stack size at termination: {len(stack)} increase memory parameter"
        )

    return explored
Beispiel #8
0
def add_style_subspaces(primes: dict, stg: networkx.DiGraph, subspaces):
    """
    Adds a *dot* subgraph for every subspace in *subspace* to *stg* - or overwrites them if they already exist.
    Nodes that belong to the same *dot* subgraph are contained in a rectangle and treated separately during layout computations.
    To add custom labels or fillcolors to a subgraph supply a tuple consisting of the
    subspace and a dictionary of subgraph attributes.

    .. note::

        *subgraphs* must satisfy the following property:
        Any two subgraphs have either empty intersection or one is a subset of the other.
        The reason for this requirement is that *dot* can not draw intersecting subgraphs.

    **arguments**:
        * *primes*: prime implicants
        * *stg*: state transition graph
        * *subspaces*: list of subspaces in string or dict representation

    **example**:

        >>> subspaces = [{"v1":0},{"v1":0,"v3":1},{"v1":1,"v2":1}]
        >>> add_style_subspaces(primes, stg, subspaces)
        >>> subspaces = ["0--","0-1","11-"]
        >>> add_style_subspaces(primes, stg, subspaces)
    """

    if not stg.graph["subgraphs"]:
        stg.graph["subgraphs"] = []

    for x in subspaces:
        attr = None

        if type(x) is not dict and len(x) == 2 and type(x[1]) is dict:
            subspace, attr = x

            if type(subspace) is str:
                subspace = subspace2dict(primes, subspace)
            elif type(subspace) != dict:
                raise Exception("Invalid Argument 'Subspaces'")

        else:
            if type(x) is str:
                subspace = subspace2dict(primes, x)
            elif type(x) is dict:
                subspace = x
            else:
                raise Exception("Invalid Argument 'Subspaces'")

        subgraph = networkx.DiGraph()
        subgraph.add_nodes_from(list_states_in_subspace(primes, subspace))
        subgraph.graph["color"] = "black"
        subgraph.graph["label"] = "subspace %s" % subspace2str(
            primes, subspace)
        if attr:
            subgraph.graph.update(attr)

        for x in list(stg.graph["subgraphs"]):
            if sorted(x.nodes()) == sorted(subgraph.nodes()):
                stg.graph["subgraphs"].remove(x)

        stg.graph["subgraphs"].append(subgraph)
Beispiel #9
0
def potassco_handle(primes: dict,
                    type_: str,
                    bounds: tuple,
                    project: List[str],
                    max_output: int,
                    fname_asp: str,
                    representation: str,
                    extra_lines=None):
    """
    Returns a list of trap spaces using the Potassco_ ASP solver :ref:`[Gebser2011]<Gebser2011>`.
    """

    if type_ not in TRAP_SPACE_TYPES:
        log.error(f"unknown trap space type: type={type_}")
        raise Exception

    if representation not in ["str", "dict"]:
        log.error(
            f"unknown trap space representation: representation={representation}"
        )
        raise Exception

    if bounds:
        bounds = tuple([len(primes) if x == "n" else x for x in bounds])

    params_clasp = ["--project"]

    if type_ == "max":
        params_clasp += [
            "--enum-mode=domRec", "--heuristic=Domain", "--dom-mod=5,16"
        ]

    elif type_ == "min":
        params_clasp += [
            "--enum-mode=domRec", "--heuristic=Domain", "--dom-mod=3,16"
        ]

    asp_text = primes2asp(primes=primes,
                          fname_asp=fname_asp,
                          bounds=bounds,
                          project=project,
                          type_=type_,
                          extra_lines=extra_lines)

    try:
        if not fname_asp:
            cmd_gringo = [CMD_GRINGO]
            proc_gringo = subprocess.Popen(cmd_gringo,
                                           stdin=subprocess.PIPE,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE)
            cmd_clasp = [CMD_CLASP, f"--models={max_output}"] + params_clasp
            proc_clasp = subprocess.Popen(cmd_clasp,
                                          stdin=proc_gringo.stdout,
                                          stdout=subprocess.PIPE,
                                          stderr=subprocess.PIPE)

            proc_gringo.stdin.write(asp_text.encode())
            proc_gringo.stdin.close()

            output, error = proc_clasp.communicate()
            error = error.decode()
            output = output.decode()

        else:
            cmd_gringo = [CMD_GRINGO, fname_asp]
            proc_gringo = subprocess.Popen(cmd_gringo,
                                           stdin=subprocess.PIPE,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE)
            cmd_clasp = [CMD_CLASP, f"--models={max_output}"] + params_clasp
            proc_clasp = subprocess.Popen(cmd_clasp,
                                          stdin=proc_gringo.stdout,
                                          stdout=subprocess.PIPE,
                                          stderr=subprocess.PIPE)

            output, error = proc_clasp.communicate()
            error = error.decode()
            output = output.decode()

    except Exception as e:
        log.error(asp_text)
        log.error(e)
        log.error("call to gringo and / or clasp failed.")

        if fname_asp:
            log.info(f"command: {' '.join(cmd_gringo + ['|'] + cmd_clasp)}")

        raise Exception

    if "ERROR" in error:
        log.error("call to gringo and / or clasp failed.")
        if fname_asp:
            log.error(f"asp file: {asp_text}")
        log.error(f"command: {' '.join(cmd_gringo + ['|'] + cmd_clasp)}")
        log.error(f"error: {error}")
        raise Exception

    log.debug(asp_text)
    log.debug(f"cmd_gringo={' '.join(cmd_gringo)}")
    log.debug(f"cmd_clasp={' '.join(cmd_clasp)}")
    log.debug(error)
    log.debug(output)

    lines = output.split("\n")
    result = []

    if type_ == "circuits":
        while lines and len(result) < max_output:
            line = lines.pop(0)

            if line[:6] == "Answer":
                line = lines.pop(0)

                tspace = [x for x in line.split() if "hit" in x]
                tspace = [x[4:-1].split(",") for x in tspace]
                tspace = [(x[0][1:-1], int(x[1])) for x in tspace]

                perc = [x[12:-2] for x in line.split() if "perc" in x]
                perc = [x for x in tspace if x[0] in perc]
                perc = dict(perc)

                circ = [x for x in tspace if x[0] not in perc]
                circ = dict(circ)

                result.append((circ, perc))

    else:
        while lines and len(result) < max_output:
            line = lines.pop(0)

            if line[:6] == "Answer":
                line = lines.pop(0)
                d = [x[4:-1].split(",") for x in line.split()]
                d = [(x[0][1:-1], int(x[1])) for x in d]
                result.append(dict(d))

    if len(result) == max_output:
        log.info(f"there are possibly more than {max_output} trap spaces.")
        log.info("increase MaxOutput to find out.")

    if representation == "str":
        if type_ == "circuits":
            result = [(subspace2str(primes, x), subspace2str(primes, y))
                      for x, y in result]
        else:
            result = [subspace2str(primes, x) for x in result]

    return result
def create_commitment_piechart(diagram: networkx.DiGraph,
                               fname_image: str,
                               color_map: Optional[dict] = None,
                               title: Optional[str] = None):
    """
    Creates the commitment pie chart for the commitment diagram using matplotlib.
    The pieces of the chart represent states that can reach the exact same subset of *Attractors*.

    **arguments**:
        * *diagram*: commitment diagram, see :ref:`commitment_compute_diagram`
        * *fname_image*: name of the output image
        * *color_map*: assignment of diagram nodes to colors for custom colors
        * *title*: optional title of plot

    **example**::

        >>> primes = get_primes("xiao_wnt5a")
        >>> attractors = compute_attractors(primes, update)
        >>> diagram = compute_commitment_diagram(attractors)
        >>> create_commitment_piechart(diagram, "commitment_piechart.pdf")
    """

    primes = diagram.graph["primes"]
    total = pyboolnet.state_space.size_state_space(primes)
    is_small_network = total <= 1024
    indices = sorted(diagram,
                     key=lambda x: len(diagram.nodes[x]["attractors"]))

    labels = []
    for x in indices:
        label = sorted(
            subspace2str(primes, y) for y in diagram.nodes[x]["attractors"])
        labels.append("\n".join(label))

    sizes = [diagram.nodes[x]["size"] for x in indices]
    figure = matplotlib.pyplot.figure()

    if color_map:
        colors = [color_map[x] for x in indices]
    else:
        colors = [
            matplotlib.pyplot.cm.rainbow(1. * x / (len(diagram) + 1))
            for x in range(len(diagram) + 2)
        ][1:-1]

    for i, x in enumerate(indices):
        if "fillcolor" in diagram.nodes[x]:
            colors[i] = diagram.nodes[x]["fillcolor"]

    if is_small_network:
        auto_percent = lambda p: f"{{{p * total / 100:.0f}}}"
    else:
        auto_percent = lambda p: f"{{{p:1.1f}}}%"

    stuff = matplotlib.pyplot.pie(sizes,
                                  explode=None,
                                  labels=labels,
                                  colors=colors,
                                  autopct=auto_percent,
                                  shadow=True,
                                  startangle=140)
    patches = stuff[0]

    for i, patch in enumerate(patches):
        patch.set_ec("black")

    matplotlib.pyplot.axis("equal")

    if not title:
        title = "Commitment Sets"

    matplotlib.pyplot.title(title, y=1.08)
    matplotlib.pyplot.tight_layout()

    figure.savefig(fname_image, bbox_inches='tight')
    log.info(f"created {fname_image}")
    matplotlib.pyplot.close(figure)
def commitment_diagram2image(diagram: networkx.DiGraph,
                             fname_image: str,
                             style_inputs: bool = True,
                             style_edges: bool = False,
                             style_ranks: bool = True,
                             first_index: int = 1) -> networkx.DiGraph:
    """
    Creates the image file *fname_image* for the basin diagram given by *diagram*.
    The flag *StyleInputs* can be used to highlight which basins belong to which input combination.
    *style_edges* adds edge labels that indicate the size of the "border" (if *compute_border* was enabled in :ref:`commitment_compute_diagram`)
    and the size of the states of the source basin that can reach the target basin.

    **arguments**:
        * *diagram*: a commitment diagram
        * *fname_image*: file name of image
        * *style_inputs*: whether basins should be grouped by input combinations
        * *style_edges*: whether edges should be size of border / reachable states
        * *style_ranks*: style that places nodes with the same number of reachable attractors on the same rank (level)
        * *first_index*: first index of attractor names

    **returns**::
        * *styled_diagram*: the styled commitment diagram

    **example**::

        >>> attractors = compute_attractors(primes, update)
        >>> compute_phenotype_diagram(attractors)
        >>> commitment_diagram2image(diagram, "diagram.pdf")
    """

    primes = diagram.graph["primes"]
    size_total = pyboolnet.state_space.size_state_space(primes)
    size_per_input_combination = pyboolnet.state_space.size_state_space(
        primes, fixed_inputs=True)
    is_small_network = size_total <= 1024

    digraph = networkx.DiGraph()
    digraph.graph["node"] = {
        "shape": "rect",
        "style": "filled",
        "color": "none"
    }
    digraph.graph["edge"] = {}

    if style_inputs:
        digraph.graph["node"]["fillcolor"] = "grey95"
    else:
        digraph.graph["node"]["fillcolor"] = "lightgray"

    attractors = [x["attractors"] for _, x in diagram.nodes(data=True)]
    attractors = [x for x in attractors if len(x) == 1]
    attractors = set(subspace2str(primes, x[0]) for x in attractors)
    attractors = sorted(attractors)

    labels = {}
    # "labels" is used for node labels
    # keys:
    # head = reachable attractors
    # size = number of states in % (depends on StyleInputs)

    for node, data in diagram.nodes(data=True):
        labels[node] = {}
        digraph.add_node(node)

        if len(data["attractors"]) == 1:
            digraph.nodes[node]["fillcolor"] = "cornflowerblue"

            attr = subspace2str(primes, data["attractors"][0])
            index = attractors.index(attr) + first_index
            labels[node][
                "head"] = f'A{index} = <font face="Courier New">{attr}</font>'

        else:
            head = sorted(
                f"A{attractors.index(subspace2str(primes, x)) + first_index}"
                for x in data["attractors"])
            head = divide_list_into_similar_length_lists(head)
            head = [",".join(x) for x in head]
            labels[node]["head"] = "<br/>".join(head)

        if "fillcolor" in data:
            digraph.nodes[node]["fillcolor"] = data["fillcolor"]

    for source, target, data in diagram.edges(data=True):
        digraph.add_edge(source, target)

        if style_edges:
            edge_label = []

            #perc = 100.* data["EX_size"] / Diagram.nodes[source]["size"]
            #edge_label.append("EX: %s%%"%perc2str(perc))

            if "EF_size" in data:
                #perc = 100.* data["EF_size"] / Diagram.nodes[source]["size"]
                #edge_label.append("EF: %s%%"%perc2str(perc))

                if data["EF_size"] < diagram.nodes[source]["size"]:
                    digraph.adj[source][target]["color"] = "lightgray"

            #result.adj[source][target]["label"] = "<%s>"%("<br/>".join(edge_label))

    for x in diagram.nodes():
        if is_small_network:
            labels[x]["size"] = f"states: {diagram.nodes[x]['size']}"
        else:
            perc = 100. * diagram.nodes[x]["size"] / size_total
            labels[x]["size"] = f"states: {perc2str(perc)}%"

    subgraphs = []
    if style_inputs:
        for inputs in list_input_combinations(primes):
            if not inputs:
                continue

            nodes = [
                x for x in diagram.nodes() if dicts_are_consistent(
                    inputs, diagram.nodes[x]["attractors"][0])
            ]
            label = subspace2str(primes, inputs)
            subgraphs.append((nodes, {
                "label": f"inputs: {label}",
                "color": "none",
                "fillcolor": "lightgray"
            }))

            for x in nodes:
                perc = 100. * diagram.nodes[x][
                    "size"] / size_per_input_combination
                labels[x]["size"] = f"states: {perc2str(perc)}%"

        if subgraphs:
            digraph.graph["subgraphs"] = []
            add_style_subgraphs(digraph, subgraphs)

    for x in diagram.nodes():
        digraph.nodes[x]['label'] = f"<{'<br/>'.join(labels[x].values())}>"

    if style_ranks:
        if subgraphs:
            to_rank = digraph.graph["subgraphs"]
        else:
            to_rank = [digraph]

        for graph in to_rank:
            ranks = {}
            for node, data in diagram.nodes(data=True):
                if node not in graph:
                    continue

                size = len(data["attractors"])
                if size not in ranks:
                    ranks[size] = []

                ranks[size].append(node)

            ranks = list(ranks.items())
            ranks.sort(key=lambda x: x[0])

            for _, names in ranks:
                names = [f'"{x}"' for x in names]
                graph.graph[f"{{rank = same; {'; '.join(names)};}}"] = ""

    if fname_image:
        digraph2image(digraph, fname_image, layout_engine="dot")

    return digraph