예제 #1
0
def tbn1(random=False):

    #CPTs
    aa = [0.6, 0.4]
    bb = [[0.2, 0.8], [0.75, 0.25]]
    cc = [[0.9, 0.1], [0.1, 0.9]]
    dd = [[[0.95, 0.05], [0.9, 0.1]], [[0.8, 0.2], [0.0, 1.0]]]
    ee = [[0.7, 0.3], [0.0, 1.0]]

    if random: bb = dd = None

    #nodes
    a = Node('a', values=('b', 'g'), cpt=aa)
    b = Node('b', parents=[a], testing=True, cpt1=bb, cpt2=bb)
    c = Node('c', parents=[a], testing=True, cpt1=cc, cpt2=cc)
    d = Node('d', parents=[b, c], testing=True, cpt1=dd, cpt2=dd)
    e = Node('e', parents=[c], cpt=ee)

    #TBN
    n = TBN('tbn1')
    n.add(a)
    n.add(
        c)  # intentionally c then b (selecting b can now depend on evidence e)
    n.add(b)
    n.add(d)
    n.add(e)

    return n
예제 #2
0
def kidney_tbn():

    #nodes
    l = Node('L', values=['y', 'n'])
    t = Node('T', values=['A', 'B'])
    s = Node('S', values=['y', 'n'], parents=[l, t], testing=True)

    #TBN
    n = TBN('kidney tbn')
    n.add(l)
    n.add(t)
    n.add(s)

    return n
예제 #3
0
def bn0():

    #CPTs
    aa = [0.6, 0.4]
    bb = [[0.9, 0.1], [0.2, 0.8]]

    #nodes
    a = Node('a', cpt=aa)
    b = Node('b', parents=[a], cpt=bb)

    #TBN
    n = TBN('bn0')
    n.add(a)
    n.add(b)

    return n
예제 #4
0
def kidney_full():

    #CPTs
    ll = [0.49, 0.51]
    tt = [[0.77, .23], [0.24, 0.76]]
    ss = [[[0.73, 0.27], [0.69, 0.31]], [[0.93, 0.07], [0.87, 0.13]]]

    #nodes
    l = Node('L', values=['y', 'n'], cpt=ll)
    t = Node('T', values=['A', 'B'], parents=[l], cpt=tt)
    s = Node('S', values=['y', 'n'], parents=[l, t], cpt=ss)

    #TBN
    n = TBN('kidney true model')
    n.add(l)
    n.add(t)
    n.add(s)

    return n
예제 #5
0
def bn3():

    #CPTs
    aa = [0.4, 0.6]
    bb = [[0.9, 0.1], [0.1, 0.9]]
    cc = [[0.3, 0.7], [0.8, 0.2]]

    #nodes
    a = Node('a', values=('t', 'f'), cpt=aa)
    b = Node('b', parents=[a], cpt=bb)
    c = Node('c', parents=[a], cpt=cc)

    #TBN
    n = TBN('bn3')
    n.add(a)
    n.add(b)
    n.add(c)

    return n
예제 #6
0
def bn2():

    #CPTs
    aa = [0.6, 0.4]
    bb = [[0.9, 0.1], [0.2, 0.8]]
    cc = [[0.3, 0.7], [0.5, 0.5]]

    #nodes
    a = Node('a', cpt=aa)
    b = Node('b', parents=[a], cpt=bb)
    c = Node('c', parents=[b], cpt=cc)

    #TBN
    n = TBN('bn2')
    n.add(a)
    n.add(b)
    n.add(c)

    return n
예제 #7
0
def bn4():

    #CPTs
    aa = [0.2, 0.8]
    bb = [0.7, 0.1, 0.2]
    cc = [[[0.3,0.7],[0.5,0.5]], \
          [[0.8,0.2],[0.4,0.6]], \
          [[0.1,0.9],[0.7,0.3]]]

    #nodes
    a = Node('a', cpt=aa)
    b = Node('b', values=('r', 'b', 'g'), cpt=bb)
    c = Node('c', parents=[b, a], cpt=cc)

    #TBN
    n = TBN('bn4')
    n.add(a)
    n.add(b)
    n.add(c)

    return n
예제 #8
0
def tbn3(random=False):

    #CPTs
    aa = [0.4, 0.6]
    bb = [[0.9, 0.1], [0.1, 0.9]]
    cc = [[0.3, 0.7], [0.8, 0.2]]

    if random: cc = None

    #nodes
    a = Node('a', values=('t', 'f'), cpt=aa)
    b = Node('b', parents=[a], cpt=bb)
    c = Node('c', parents=[a], testing=True, cpt1=cc, cpt2=cc)

    #TBN
    n = TBN('tbn3')
    n.add(a)
    n.add(b)
    n.add(c)

    return n
예제 #9
0
def tbn2(random=False):

    #CPTs
    aa = [0.6, 0.4]
    bb = [[0.9, 0.1], [0.2, 0.8]]
    cc = [[0.3, 0.7], [0.5, 0.5]]

    if random: bb = cc = None

    #nodes
    a = Node('a', cpt=aa)
    b = Node('b', parents=[a], testing=True, cpt1=bb, cpt2=bb)
    c = Node('c', parents=[b], testing=True, cpt1=cc, cpt2=cc)

    #TBN
    n = TBN('tbn2')
    n.add(a)
    n.add(b)
    n.add(c)

    return n
예제 #10
0
def bn1():

    #CPTs
    aa = [0.6, 0.4]
    bb = [[0.2, 0.8], [0.75, 0.25]]
    cc = [[0.9, 0.1], [0.1, 0.9]]
    dd = [[[0.95, 0.05], [0.9, 0.1]], [[0.8, 0.2], [0.0, 1.0]]]
    ee = [[0.7, 0.3], [0.0, 1.0]]

    #nodes
    a = Node('a', values=('b', 'g'), cpt=aa)
    b = Node('b', parents=[a], cpt=bb)
    c = Node('c', parents=[a], cpt=cc)
    d = Node('d', parents=[b, c], cpt=dd)
    e = Node('e', parents=[c], cpt=ee)

    #TBN
    n = TBN('bn1')
    n.add(a)
    n.add(b)
    n.add(c)
    n.add(d)
    n.add(e)

    return n
예제 #11
0
def fn2_chain(size, card=2):
    assert size >= 2 and card >= 2

    net = TBN('fn2_chain')
    bvalues = ('v0', 'v1')
    values = tuple('v%d' % i for i in range(card))

    x1 = Node('x', values=bvalues, parents=[])
    y1 = Node('y', values=bvalues, parents=[])
    z1 = Node('z1', values=values, parents=[x1, y1], testing=True)

    net.add(x1)
    net.add(y1)
    net.add(z1)

    for i in range(2, size + 1):
        last = (i == size)
        name = 'z' if last else 'z%d' % i
        values = bvalues if last else values
        p_name = 'z%d' % (i - 1)
        parent = net.node(p_name)
        z = Node(name, values=values, parents=[parent], testing=True)
        net.add(z)

    #net.dot(view=True)
    return (net, 'x', 'y', 'z')
예제 #12
0
def getHMM(size, card, param=False, transition=None, emission=None):
    # define an HMM model of size nodes with card hidden states
    assert size >= 2 and card >= 2
    if param:
        assert transition is not None and emission is not None
        u.input_check(np.array(transition).shape == (card, card), f'wrong size for transition matrix')
        u.input_check(np.array(emission).shape == (card, card), f'wrong size for matrix')
        # check size for transition and emission matrix
    hmm = TBN(f'hmm_{size}')
    values = ['v' + str(i) for i in range(card)]
    hidden_nodes = [] 
    # store list of created hidden nodes
    for i in range(size):
        # add hidden node
        if i == 0:
            uniform_cpt = [1./card] * card;
            hidden_i = Node('h_0', values=values, parents=[], cpt=uniform_cpt)
            hmm.add(hidden_i)
            # notice that H0 is uniform if parametrized
            hidden_nodes.append(hidden_i)
        else:
            hidden_i = Node('h_' + str(i), values=values, parents=[hidden_nodes[i - 1]], cpt_tie="transition", cpt=transition)
            hmm.add(hidden_i)
            hidden_nodes.append(hidden_i)
        # add evidence node
        evidence_i = Node('e_' + str(i), values=values, parents=[hidden_nodes[i]], cpt_tie="emission", cpt=emission)
        hmm.add(evidence_i)
        # finish creating the hmm
    #hmm.dot(view=True)
    print("Finish creating HMM_{} with cardinality {}".format(size, card))
    return hmm
예제 #13
0
def chain(testing=False):

    n0 = Node('S', parents=[])
    n1 = Node('n1', parents=[n0], testing=testing)
    n2 = Node('n2', parents=[n1], testing=testing)
    n3 = Node('M', parents=[n2], testing=testing)
    n4 = Node('n4', parents=[n3], testing=testing)
    n5 = Node('n5', parents=[n4], testing=testing)
    n6 = Node('E', parents=[n5], testing=testing)

    net = TBN('chain')
    net.add(n0)
    net.add(n1)
    net.add(n2)
    net.add(n3)
    net.add(n4)
    net.add(n5)
    net.add(n6)

    return net
def getTestingHMM(size, card, N, testing=False, param=False, transition1=None, transition2=None, emission=None):
    # define an N order testing HMM model of length size with cardinality hidden states
    assert size >= 2 and card >= 2 and N >= 1
    if param:
        u.input_check(np.array(transition1).shape == (card,) * (N + 1), "wrong size for transition matrix")
        u.input_check(np.array(emission).shape == (card, card), "wrong size for emission matrix")
        if testing:
            u.input_check(np.array(transition2).shape == (card,) * (N + 1), "wrong size for transition2 matrix")
        # check the size of transition and emission probabilities
    hmm = TBN(f'thmm_{N}_{size}')
    values = ['v' + str(i) for i in range(card)]
    hidden_nodes = []
    # store list of hidden nodes
    # add first N hidden nodes
    for i in range(N):
        name = 'h_' + str(i)
        parents = [hidden_nodes[j] for j in range(i)]
        cpt = (1./card) * np.ones(shape=(card,)*(i+1))
        # create a uniform conditional cpt
        hidden_i = Node(name, values=values, parents=parents, cpt=cpt)
        # the first N nodes cannot be testing
        hmm.add(hidden_i)
        hidden_nodes.append(hidden_i)
        # add hidden nodes
    # add the subsequent hidden nodes
    for i in range(N, size):
        name = 'h_' + str(i)
        parents = [hidden_nodes[j] for j in range(i-N, i)]
        if not testing:
            hidden_i = Node(name, values=values, parents=parents, cpt=transition1, cpt_tie="transition")
        else:
            hidden_i = Node(name, values=values, parents=parents, testing=True, cpt1=transition1, cpt2=transition2, cpt_tie="transition")
        hmm.add(hidden_i)
        hidden_nodes.append(hidden_i)
    # add evidence
    for i in range(size):
        name = 'e_' + str(i)
        parents = [hidden_nodes[i]]
        evidence_i = Node(name, values=values, parents=parents, cpt=emission, cpt_tie="emission")
        hmm.add(evidence_i)
    # finish defining the hmm
    # hmm.dot(view=True)
    print("Finish creating a {}-order testing hmm of length {} and cardinality {}".format(N, size, card))
    return hmm
예제 #15
0
def get(net1, hard_evd_nodes, trainable_tbn, elm_method, elm_wait):
    assert net1._for_inference

    #net1.dot(fname='tbn_pre_decouple.gv', view=True)
    u.show(f'  Decoupling tbn:')
    elm_order, cliques1, max_binary_rank1, stats = net1.elm_order(
        elm_method, elm_wait)
    u.show('   ', stats)

    # cutting edges outgoing form hard evidence nodes
    cut_edges = lambda n: len(n.children) >= 1 and n in hard_evd_nodes and \
                              (n.parents or len(n.children) >= 2)
    cut_edges_set = set(n for n in net1.nodes if cut_edges(n))

    # replicating functional cpts
    # if both duplicate and cut_edges trigger, use cut_edges as it is more effective
    duplicate = lambda n: n not in cut_edges_set and len(n.children) >= 2 \
                            and n.is_functional(trainable_tbn)

    duplicate_set = set(n for n in net1.nodes if duplicate(n))

    # perhaps decoupling does nothing
    if not duplicate_set and not cut_edges_set:
        u.show('    nothing to decouple')
        return net1, elm_order, (max_binary_rank1, max_binary_rank1
                                 )  # no decoupling possible

    # we will decouple
    net2 = TBN(f'{net1.name}__decoupled')
    net2._decoupling_of = net1

    # -when creating a clone c(n) in net2 for node n in net1, we need to look up the
    #  parents of c(n) in net2.
    # -this is done by calling get_image(p) on each parent p of node n
    # -the length of images[p] equals the number of times get_image(p) will be called
    # -members of images[p] may not be distinct depending on the replication strategey
    images = {}

    def get_image(n):
        return images[n].pop()

    # -when we have hard evidence on node n (net1), we create a replica r (net2) of n
    # for each child of n, which copies evidence on n into the cpts of its children.
    # -maps node r (net2) to node n (net1) that it is copying evidence from
    evidence_from = {}

    # maps node n (net1) to a tuple (c_1,...,c_k) where k is the number of clones that
    # node n will have in nets2, and c_i is the number of children for clone i in net2
    ccounts = {}

    # fully replicated(i): one i-replica for each c-replica, where c is child of i in net1
    # partial replicated(i): one i-replica for each child c of i in net1
    fully_replicated = lambda i: all(ccount == 1 for ccount in ccounts[i])
    replicas_count = lambda i: len(ccounts[i]
                                   )  # number of replicas node i has in net2

    # compute the number of replicas in net2 for each node in net1 (fill ccounts)
    for n in reversed(net1.nodes):  # bottom up
        ccounts[n] = []
        cparents = set()
        for c in n.children:
            cparents |= set(c.parents)
        #replicate_node = any(cparents <= clique for clique in cliques1)
        #replicate_node = all(p in duplicate_set for p in n.parents)
        replicate_node = True
        if n in duplicate_set and replicate_node:
            # replicate node n
            for c in n.children:
                if True:  #not fully_replicated(c):
                    # replicate node n for each replica of child c
                    ccounts[n].extend([1] * replicas_count(c))
                else:
                    # replicate node n for each child c
                    ccounts[n].append(replicas_count(c))
        else:  # do not replicate node n
            # n could be in cut_edges_set, but ccounts will not be used in that case
            duplicate_set.discard(n)
            children_replicas_count = sum(
                replicas_count(c) for c in n.children)
            ccounts[n].append(children_replicas_count)

    # cutting edges takes priority over decoupling as it is more effective
    for n in net1.nodes:  # visiting parents before children
        if n in cut_edges_set:  # disconnect n from its children
            assert n not in duplicate_set
            n._clamped = True  # flag set in original network (net1)
            parents = [get_image(p) for p in n.parents]
            master = clone_node(n, n.name, parents)
            net2.add(master)
            images[n] = []
            # master not added to images as it will not be a parent of any node in net2
            for i, c in enumerate(n.children):
                for j in range(replicas_count(
                        c)):  # j iterates over replicas of child c
                    # these clones will be removed after elimination order is computed
                    # clones are not testing even if master is testing
                    clone = Node(f'{n.name}_evd{i}_{j}',
                                 values=master.values,
                                 parents=[])
                    net2.add(clone)
                    evidence_from[clone] = master
                    images[n].append(
                        clone
                    )  # children of n will reference clones, not master
        elif n in duplicate_set:  # duplicate node n and its functional cpt
            images[n] = []
            for i, ccount in enumerate(ccounts[n]):
                assert ccount > 0  # number of children each clone will have in net2
                parents = [get_image(p) for p in n.parents]
                clone = clone_node(n, f'{n.name}_fcpt{i}', parents)
                if i > 0: clone._master = False  # clone() sets this to True
                net2.add(clone)
                images[n].extend([clone] * ccount)
        else:  # just copy node n from net1 to net2
            (ccount,
             ) = ccounts[n]  # number of children clone will have in net2
            parents = [get_image(p) for p in n.parents]
            clone = clone_node(n, n.name, parents)
            net2.add(clone)
            images[n] = [clone] * ccount

    assert not net2._for_inference
    assert len(images) == len(net1.nodes)
    assert len(images) <= len(net2.nodes)
    assert all(v == [] for v in images.values())

    #net2.dot(fname='tbn_post_decouple.gv', view=True)
    elm_order, _, max_binary_rank2, stats = net2.elm_order(
        elm_method, elm_wait)
    u.show('   ', stats)

    if not duplicate_set:
        elm_order = [n._original for n in elm_order if n not in evidence_from]
        # only clamping took place, so we only care about elimination order
        # return original network with _clamped flag set for some nodes
        return net1, elm_order, (max_binary_rank1, max_binary_rank2)

    if cut_edges_set:  # some variables were clamped
        # get rid of auxiliary evidence nodes from elimination order
        elm_order = [n for n in elm_order if n not in evidence_from]
        # need to restore net2 by getting rid of auxiliary evidence nodes
        # and restoring children of clamped nodes
        net2.nodes = [n for n in net2.nodes if n not in evidence_from]
        replace = lambda n: evidence_from[n] if n in evidence_from else n
        for n in net2.nodes:
            n._parents = tuple(replace(p) for p in n.parents)
            n._family = tuple(replace(f) for f in n.family)

    u.show(f'    node growth: {len(net2.nodes)/len(net1.nodes):.1f}')

    return net2, elm_order, (max_binary_rank1, max_binary_rank2)
예제 #16
0
def get(size, output, testing, use_bk, tie_parameters):
    assert output in ('label', 'height', 'width', 'row', 'col')

    net = TBN(f'rectangle_{size}_{size}')
    irange = range(size)  # [0,..,size-1]
    srange = range(1, size + 1)  # [1,..,size]

    ### 1. row and column origins (only roots) are _unconstrained_

    # cpts: shape (size)
    uniform = lambda values: [1. / len(values)] * len(values)

    # nodes
    orn = Node('row', values=irange, parents=[], cpt=uniform(irange))
    ocn = Node('col', values=irange, parents=[], cpt=uniform(irange))
    net.add(orn)
    net.add(ocn)

    ### 2. height and width are _constrained_ by row and column origins

    # cpts: shape (size, size)
    constraint = lambda p, n, size=size: p + n <= size

    # nodes
    h = Node('height',
             values=srange,
             parents=[orn],
             cpt=constraint,
             fixed_zeros=use_bk)
    w = Node('width',
             values=srange,
             parents=[ocn],
             cpt=constraint,
             fixed_zeros=use_bk)
    net.add(h)
    net.add(w)

    ### 3. type is determined by height and width (except when height=weight)

    # cpt: shape (size,size,2)
    constraint = lambda h, w, t: h >= w if t == 'tall' else w >= h

    # nodes
    t = Node('label',
             values=('tall', 'wide'),
             parents=[h, w],
             cpt=constraint,
             fixed_cpt=use_bk)
    net.add(t)

    ### 4. row(i) is _determined_ by row origin and height: whether row i has an on-pixel
    ###    col(i) is _determined_ by col origin and width:  whether col i has an on-pixel

    row = {}  # maps row to node
    col = {}  # maps col to node

    for i in irange:
        fn = lambda o, s, i=i: (o <= i and i < o + s)
        row[i] = Node(f'r_{i}',
                      parents=[orn, h],
                      cpt=fn,
                      fixed_cpt=use_bk,
                      functional=True)
        col[i] = Node(f'c_{i}',
                      parents=[ocn, w],
                      cpt=fn,
                      fixed_cpt=use_bk,
                      functional=True)
        net.add(row[i])
        net.add(col[i])

    ### 5. pixels are _determined_ by row and column

    # cpt for pixel (i,j): shape (2,2,2)
    function = lambda r, c: r and c

    # nodes
    inputs = []
    pname = lambda i, j: f'pixel_{i}_{j}'

    # evidence nodes must be generated row, then column to match data generation
    tie = 'pixel' if tie_parameters else None
    for i in irange:
        r = row[i]
        for j in irange:
            c = col[j]
            n = pname(i, j)
            tie = tie if not testing else None
            p = Node(n,
                     parents=[r, c],
                     cpt=function,
                     testing=testing,
                     cpt_tie=tie)
            net.add(p)
            inputs.append(n)

    #net.dot(view=True)
    return (net, inputs)
예제 #17
0
def get(vcount, scount, pcount, fcount, back, testing):
    assert vcount >= 2 and scount >= 2
    assert fcount >= 0 and fcount <= vcount and pcount < vcount
    assert back < vcount and pcount <= back

    # decide values and parents first
    i2values = {}
    i2parents = {}
    parents_pool = []

    for i in range(vcount):
        sc = np.random.randint(2, scount + 1)  # number of values
        pc = np.random.randint(1 + min(i, pcount))  # number of parents
        assert sc >= 2 and sc <= scount
        assert pc <= pcount

        i2values[i] = range(sc)
        i2parents[i] = list(np.random.choice(parents_pool, pc, replace=False))

        parents_pool.append(i)
        if len(parents_pool) > back:
            parents_pool.pop(0)

    # choose functional variables randomly
    candidates = [i for i, parents in i2parents.items()
                  if parents]  # exclude roots
    fcount = min(fcount, len(candidates))
    functional_indices = set(
        np.random.choice(candidates, fcount, replace=False))

    # construct bn
    bn = TBN(f'random-{vcount}-{scount}-{pcount}-{fcount}-{back}')
    if testing:  # construct a tbn equivalent to bn
        bn2 = TBN(f'random-{vcount}-{scount}-{pcount}-{fcount}-{back}-Testing')
        i2node2 = {}

    i2node = {}  # maps node number to node object
    for i in range(vcount):
        values = i2values[i]
        parents = [i2node[k] for k in i2parents[i]]
        functional = i in functional_indices
        assert not functional or parents  # roots cannot be functional

        cpt_shape = [p.card for p in parents]
        cpt_shape.append(len(values))
        cpt = __random_cpt(cpt_shape, functional)

        # tbn
        if testing:
            parents2 = [i2node2[k] for k in i2parents[i]]
            if parents2 and not functional and np.random.random(
            ) >= .5:  # testing
                node = Node(f'v{i}',
                            values=values,
                            parents=parents2,
                            cpt1=cpt,
                            cpt2=cpt)
            else:  # regular
                node = Node(f'v{i}', values=values, parents=parents2, cpt=cpt)
            bn2.add(node)
            i2node2[i] = node
        # bn
        node = Node(f'v{i}', values=values, parents=parents, cpt=cpt)
        bn.add(node)
        i2node[i] = node

    roots = [node.name for node in bn.nodes if not node.parents]
    leaves = [node.name for node in bn.nodes if not node.children]

    if testing:
        return bn, roots, leaves, bn2
    return bn, roots, leaves
예제 #18
0
def get(size,digits,testing,use_bk,tie_parameters,remove_common=False):
    assert size >= 7
    assert len(digits) >= 2
    assert all(d in (0,1,2,3,4,5,6,7,8,9) for d in digits)
    assert u.sorted(digits)

    # height is multiple of 7: 3 horizontal segments, 4 horizontal spaces
    # width  is multiple of 4: 2 vertical segments,   2 vertical spaces
    h_inc = 7
    w_inc = 4 
    
    net = TBN('digits')
    
    ###
    ### master START
    ###
    
    # nodes:
    # d digit
    # r upper-row (of bounding rectangle)
    # c left-col  (of bounding rectangle)
    # h height    (of bounding rectangle)
    # w width     (of bounding rectangle)
    # t thickness (of lines)
        
    # values
    dvals  = digits                # for digits (root)
    rvals  = range(0,size-h_inc+1) # for upper-row of digit (root)
    cvals  = range(0,size-w_inc+1) # for left-column of digit (root)
    srange = range(1,size+1)       # height, width, thickness (will be pruned)
    
    # constraints and functions
    # w is _constrained_ by c (length of segments)
    uniform = lambda values: [1./len(values)]*len(values)
    wct     = lambda c, w, w_inc=w_inc, size=size: (w % w_inc) == 0 and w <= size-c 
    
    # nodes
    dn  = Node('d', values=dvals,  parents=[],   cpt=uniform(dvals))
    rn  = Node('r', values=rvals,  parents=[],   cpt=uniform(rvals))
    cn  = Node('c', values=cvals,  parents=[],   cpt=uniform(cvals))
    wn  = Node('w', values=srange, parents=[cn], cpt=wct, fixed_zeros=use_bk)
    
    for n in [dn,rn,cn,wn]: net.add(n)
        
    ###
    ### segments
    ###
    
    # seven segments: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G
    # 1,2,4,5 are vertical
    # 0,3,6   are horizontal
    # https://en.wikipedia.org/wiki/Seven-segment_display
    
    # each digit corresponds to some segments (activated segments)
    segments  = (0,1,2,3,4,5,6)
    vsegments = (1,2,4,5) # vertical segments
    hsegments = (0,3,6)   # horizontal segments
    # map digits to segments
    dsegments = {0:'012345', 1:'12',     2:'01346', 3:'01236',   4:'1256', 
                 5:'02356',  6:'023456', 7:'012',   8:'0123456', 9:'012356'}
    
    # segments needed for the given digits
    segments  = tuple(s for s in segments if any(str(s) in dsegments[d] for d in digits))
    if remove_common:
        common   = tuple(s for s in segments if all(str(s) in dsegments[d] for d in digits))
        segments = tuple(s for s in segments if s not in common)
        u.show('Removing common segments',common)
    vsegments = tuple(s for s in vsegments if s in segments)
    hsegments = tuple(s for s in hsegments if s in segments)
    
    # nodes:
    # a segment is a rectangle
    # a segment s has upper-row srn[s], left-column scn[s], height shn[s], width swn[s],
    # whether segment is activated san[s], whether it will render in row i, sirn[s,i], 
    # whether it will render in column i, sicn[s,i], and whether it will render in 
    # pixel i,j, spn[s,i,j]
    
    # values
    irange = range(size)     # for segment row and column
    srange = range(1,size+1) # for segment height and width
                       
    # san[s] is whether segment s is activated given digit (True,False)
    # san[s] is a _function_ of node dn
    
    san = {} # maps segment to node
    for s in segments:
        fn   = lambda d, s=s, dsegments=dsegments: str(s) in dsegments[d]
        node = Node(f's{s}', parents=[dn], cpt=fn, fixed_cpt=use_bk, functional=True)
        net.add(node)
        san[s] = node
    
    # srn[s] is upper-row   for segment s
    # scn[s] is left-column for segment s
    # srn[s] and scn[s] are _functions_ of nodes rn and cn of master
    
    srn    = {} # maps segment to node
    scn    = {} # maps segment to node
    shifts = [(0,0), (0,3), (3,3), (6,0), (3,0), (0,0), (3,0)]
    # rshift: shift rn (digit) down  to get srn (segment)
    # cshift: shift cn (digit) right to get scn (segment)
        
    fn   = lambda r: r + 3
    sr3n = Node(f'sr3', values=irange, parents=[rn], cpt=fn, fixed_cpt=use_bk, functional=True)
    fn   = lambda r: r + 6
    sr6n = Node(f'sr6', values=irange, parents=[rn], cpt=fn, fixed_cpt=use_bk, functional=True)
    fn   = lambda c: c + 3
    sc3n = Node(f'sc3', values=irange, parents=[cn], cpt=fn, fixed_cpt=use_bk, functional=True)
    for n in (sr3n,sr6n,sc3n): net.add(n)
    
    rsn = {0:rn, 3:sr3n, 6:sr6n}
    csn = {0:cn, 3:sc3n}
    
    for s in segments: 
        rshift, cshift = shifts[s]
        srn[s] = rsn[rshift]
        scn[s] = csn[cshift]

    # sirn[s,i] is whether segment s will render in row i (True,False)
    # sicn[s,i] is whether segment s will render in col i (True,False)
    # sirn[s,i] is a _function_ of srn[s] and wn
    # sicn[s,i] is a _function_of  scn[s] and wn
    
    sirn = {} # maps (segment,row) to node
    sicn = {} # maps (segment,col) to node
    
    for s in segments:
        for i in irange: 
            name = f'in_r{s}_{i}'
            if s in vsegments: # vertical segment
                pa = [srn[s],wn]
                fn = lambda r, h, i=i: r <= i and i < r+h
            else:
                pa = [srn[s]]
                fn = lambda r, i=i: r == i
            node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True)
            net.add(node)
            sirn[(s,i)] = node
 
            name = f'in_c{s}_{i}'
            if s not in vsegments: # horizontal
                pa = [scn[s],wn]
                fn = lambda c, w, i=i: c <= i and i < c+w
            else:
                pa = [scn[s]]
                fn = lambda c, i=i: c == i
            node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True)
            net.add(node)
            sicn[s,i] = node
            
    # spn[s,i,j] is whether segment s will render in pixel i,j (True,False)
    # spn[s,i,j] is a _function_ of san[s] and sirn[s,i] and sicn[s,j]
    
    spn = {} # maps (segment,row,col) to node
    fn  = lambda a, r, c: a and r and c
    
    for s in segments:
        for i in irange:
            for j in irange:
                name = f'p{s}_{i}_{j}'
                pa   = [san[s],sirn[(s,i)],sicn[(s,j)]]
                node = Node(name, parents=pa, cpt=fn, fixed_cpt=use_bk, functional=True)
                net.add(node)
                spn[(s,i,j)] = node

    ###
    ### master END
    ###
    
    # image pixels: whether pixel i,j will render in image (iff some segment renders)
    
    output = dn.name
    inputs = []
    fn     = lambda *inputs: any(inputs) # or-gate
    
    tie = 'pixel' if tie_parameters else None
    for i in irange:
        for j in irange:
            name = f'p_{i}_{j}'
            pa   = [spn[(s,i,j)] for s in segments]
            tie  = tie if not testing else None
            pn   = Node(name, parents=pa, cpt=fn, testing=testing, cpt_tie=tie)
            net.add(pn)
            inputs.append(name)
    
    return (net, inputs, output)