Advanced Cardinality Choice Modeling Guide¶
This notebook provides an overview on how to model choices with advanced cardinality, for example:
- Selection choices with more possibilities than "select 1 of n options"
- Connection choices that:
- Derive target nodes from source nodes
- Remove unconnected source/target nodes from the graph
- Use these features to provide a more elegant way of modeling advanced selection cardinality choices
Refer to the theory and guide for more background information.
For reference, we display all the possible nodes and edges in the DSG here:
from adsg_core.render import DSGRenderer
DSGRenderer.render_legend()
Selection Choices with Advanced-Cardinality¶
Selection choices can be used to define more advanced cardinality choices than "choose 1 of n", by using derivation edges to derive other options.
For example, to model a "take 2 of 3" scenario, intermediary nodes can be used to define all possible options of selecting 2 of 3 option nodes:
from adsg_core import BasicDSG, NamedNode
# Create the DSG
dsg = BasicDSG()
# Define the nodes in the set to be selected
nodes_in_set = [NamedNode(f'Node {i}') for i in range(3)]
# Define the distinct options of the selection node, one for each combination of selecting 2 of 3:
# Select nodes 1 and 2, select nodes 1 and 3, or select nodes 2 and 3.
option_nodes = [NamedNode(f'Opt {i}') for i in range(3)]
dsg.add_edges([
(option_nodes[0], nodes_in_set[0]), (option_nodes[0], nodes_in_set[1]), # Option 1 selects node 1 and 2
(option_nodes[1], nodes_in_set[0]), (option_nodes[1], nodes_in_set[2]), # Option 2 selects node 1 and 3
(option_nodes[2], nodes_in_set[1]), (option_nodes[2], nodes_in_set[2]), # Option 3 selects node 2 and 3
])
# Define the selection choice
start_node = NamedNode('Selection')
dsg.add_selection_choice('Select 2 of 3', start_node, option_nodes)
dsg = dsg.set_start_nodes({start_node})
# Show the DSG (also works outside a notebook)
dsg.render()
dsg.render_legend(['NODE', 'NODE_START', 'EDGE_DERIVE', 'CHOICE_SEL'])
Rendering all possible graphs shows that there are indeed 3 possible ways for selecting the 2 of 3 nodes in the set.
dsg.render_all()
Rendering 3 instances
In general, using intermediary nodes to define each option for selecting the actual option nodes is always a possibility if you want to use selection nodes.
Another example is the choice of how many to select of an ordered set:
dsg = BasicDSG()
# Define the nodes of the ordered set
n = 5
nodes_in_set = [NamedNode(f'Node {i}') for i in range(n)]
# Define intermediary nodes for select n of this set
option_nodes = [nodes_in_set[0]]
for n_select in range(1, n):
option_node = NamedNode(f'Opt {len(option_nodes)}')
option_nodes.append(option_node)
dsg.add_edges([(option_node, node_in_set) for node_in_set in nodes_in_set[:n_select+1]])
# Define the selection choice
start_node = NamedNode('Selection')
dsg.add_selection_choice(f'Select n of {n}', start_node, option_nodes)
dsg = dsg.set_start_nodes({start_node})
# Show the DSG (also works outside a notebook)
dsg.render()
Using itertools to Define Options¶
Scenarios such as "select k of n" with a fixed or variable k can be implemented using itertools to iterate over all possible combinations.
The same "2 of 3" example as before, now using itertools:
import itertools
from adsg_core import BasicDSG, NamedNode
def get_dsg_k_of_n(k, n):
# Create the DSG
dsg = BasicDSG()
# Define the nodes in the set to be selected
nodes_in_set = [NamedNode(f'Node {i}') for i in range(n)]
# Define the distinct options of the selection node, one for each combination of selecting 2 of 3:
# Select nodes 1 and 2, select nodes 1 and 3, or select nodes 2 and 3.
option_nodes = []
for selected_nodes_in_set in itertools.combinations(nodes_in_set, k):
option_node = NamedNode(f'Opt {len(option_nodes)}')
option_nodes.append(option_node)
dsg.add_edges([(option_node, node_in_set) for node_in_set in selected_nodes_in_set])
print(f'There are {len(option_nodes)} ways to select {k} of {n}.')
# Define the selection choice
start_node = NamedNode('Selection')
dsg.add_selection_choice(f'Select {k} of {n}', start_node, option_nodes)
return dsg.set_start_nodes({start_node})
# Create DSG for "2 of 3"
dsg = get_dsg_k_of_n(k=2, n=3)
# Show the DSG (also works outside a notebook)
dsg.render()
There are 3 ways to select 2 of 3.
The number of intermediate option nodes grows quickly with k or n
get_dsg_k_of_n(k=2, n=4).render()
get_dsg_k_of_n(k=3, n=6).render()
get_dsg_k_of_n(k=4, n=8);
There are 6 ways to select 2 of 4.
There are 20 ways to select 3 of 6.
There are 70 ways to select 4 of 8.
Using Connection Choices for Advanced Selection Cardinality¶
Connection choices are designed to make it easy to define advanced cardinality choices out of the box, however do not influence node selection by themselves. Two helper functionalities are provided to make available the power of connection choices for node selection too:
- Deriving target nodes from source nodes, so that target nodes do not have to be derived from other sources.
- Removing unconnected source and/or target nodes from the graph, so that connection choices can also be used for node selection.
To demonstrate this, consider the "k of n" combinations scenario of before:
from adsg_core import BasicDSG, ConnectorNode, GraphProcessor
def get_dsg_k_of_n_using_conn_choice(k, n):
# Create the DSG
dsg = BasicDSG()
# Define the source connector node to have k connections
source_node = ConnectorNode(f'Select {k}', deg_list=[k])
# Define the nodes in the set to be selected:
# Each can either be connected to or not, and will be removed if unconnected
target_nodes = [ConnectorNode(f'Node {i}', deg_list=[0, 1], remove_if_unconnected=True) for i in range(n)]
# Define the connection choice
# We set `derive_tgt_nodes=True` to make sure that the target nodes are indeed derived from the source node
dsg.add_connection_choice(f'Select {k} of {n}', [source_node], target_nodes, derive_tgt_nodes=True)
# Count the number of possible graphs
dsg = dsg.set_start_nodes({source_node})
processor = GraphProcessor(dsg)
n_graphs = processor.get_n_valid_designs()
print(f'There are {n_graphs} ways to select {k} of {n}.')
return dsg
# Create DSG for "2 of 3"
dsg = get_dsg_k_of_n_using_conn_choice(k=2, n=3)
# Some additional nodes are automatically added:
# - A "collector" node that collects derivation edges from source nodes and derives all target nodes
# - Selection choice nodes for selecting whether each of the target node exists
# - A non-selection node used as the option for not selecting a target node
dsg.render()
dsg.render_legend(['NODE_CONNECTOR', 'EDGE_DERIVE', 'EDGE_CONNECT', 'CHOICE_SEL', 'CHOICE_CONN'])
There are 3 ways to select 2 of 3.
# Again, there are 3 ways to select the target nodes
dsg.render_all([0])
Rendering 1 of 3 instances
get_dsg_k_of_n_using_conn_choice(k=2, n=4).render()
get_dsg_k_of_n_using_conn_choice(k=3, n=6).render()
get_dsg_k_of_n_using_conn_choice(k=4, n=8);
There are 6 ways to select 2 of 4.
There are 20 ways to select 3 of 6.
There are 70 ways to select 4 of 8.
This example shows that both the code to define the node selection scenario with advanced cardinality and the resulting DSG is more elegant when using connection choices with selection than when directly using selection choices.
Additionally, the full power of connection choices is available. For example, multiple source nodes can derive and select target nodes, different number of connections may be established, and parallel connections can be used.