def manuscript_full_json(passage_or_id, hs_hsnr_id): """Endpoint. Serve information about a manuscript. :param string hs_hsnr_id: The hs, hsnr or id of the manuscript. """ auth() hs_hsnr_id = request.args.get('ms_id') or hs_hsnr_id with current_app.config.dba.engine.begin() as conn: passage = Passage(conn, passage_or_id) ms = Manuscript(conn, hs_hsnr_id) rg_id = passage.request_rg_id(request) if ms.ms_id is None: return cache( make_json_response(None, 400, 'Bad request: No such manuscript.')) json = ms.to_json() json['length'] = ms.get_length(rg_id) # Get the attestation(s) of the manuscript (may be uncertain eg. a/b/c) res = execute( conn, """ SELECT labez, clique, labez_clique, certainty FROM apparatus_view_agg WHERE ms_id = :ms_id AND pass_id = :pass_id """, dict(parameters, ms_id=ms.ms_id, pass_id=passage.pass_id)) row = res.fetchone() if row is not None: json['labez'], json['clique'], json['labez_clique'], json[ 'certainty'] = row # Get the affinity of the manuscript to all manuscripts res = execute( conn, """ SELECT avg (a.affinity) as aa, percentile_cont(0.5) WITHIN GROUP (ORDER BY a.affinity) as ma FROM affinity a WHERE a.ms_id1 = :ms_id1 AND a.rg_id = :rg_id """, dict(parameters, ms_id1=ms.ms_id, rg_id=rg_id)) json['aa'], json['ma'] = 0.0, 0.0 row = res.fetchone() if row is not None: json['aa'], json['ma'] = row # Get the affinity of the manuscript to MT # # For a description of mt and mtp see the comment in # ActsMsListValPh3.pl and # http://intf.uni-muenster.de/cbgm/actsPh3/guide_en.html#Ancestors res = execute( conn, """ SELECT a.affinity as mt, a.equal::float / c.length as mtp FROM affinity a JOIN ms_ranges c ON (a.ms_id1, a.rg_id) = (c.ms_id, c.rg_id) WHERE a.ms_id1 = :ms_id1 AND a.ms_id2 = 2 AND a.rg_id = :rg_id """, dict(parameters, ms_id1=ms.ms_id, rg_id=rg_id)) json['mt'], json['mtp'] = 0.0, 0.0 row = res.fetchone() if row is not None: json['mt'], json['mtp'] = row return cache(make_json_response(json))
def relatives_csv(passage_or_id, hs_hsnr_id): """Output a table of the nearest relatives of a manuscript. Output a table of the nearest relatives/ancestors/descendants of a manuscript and what they attest. """ auth() type_ = request.args.get('type') or 'rel' limit = int(request.args.get('limit') or 0) labez = request.args.get('labez') or 'all' mode = request.args.get('mode') or 'sim' include = request.args.getlist('include[]') or [] fragments = request.args.getlist('fragments[]') or [] view = 'affinity_view' if mode == 'rec' else 'affinity_p_view' where = '' if type_ == 'anc': where = ' AND older < newer' if type_ == 'des': where = ' AND older >= newer' if labez == 'all': where += " AND labez !~ '^z'" elif labez == 'all+lac': pass else: where += " AND labez = '%s'" % labez if 'fragments' in fragments: frag_where = '' else: frag_where = 'AND aff.common > aff.ms1_length / 2' limit = '' if limit == 0 else ' LIMIT %d' % limit with current_app.config.dba.engine.begin() as conn: passage = Passage(conn, passage_or_id) ms = Manuscript(conn, hs_hsnr_id) rg_id = passage.request_rg_id(request) exclude = get_excluded_ms_ids(conn, include) # Get the X most similar manuscripts and their attestations res = execute( conn, """ /* get the LIMIT closest ancestors for this node */ WITH ranks AS ( SELECT ms_id1, ms_id2, rank () OVER (ORDER BY affinity DESC, common, older, newer DESC, ms_id2) AS rank, affinity FROM {view} aff WHERE ms_id1 = :ms_id1 AND aff.rg_id = :rg_id AND ms_id2 NOT IN :exclude AND newer > older {frag_where} ORDER BY affinity DESC ) SELECT r.rank, aff.ms_id2 as ms_id, ms.hs, ms.hsnr, aff.ms2_length, aff.common, aff.equal, aff.older, aff.newer, aff.unclear, aff.common - aff.equal - aff.older - aff.newer - aff.unclear as norel, CASE WHEN aff.newer < aff.older THEN '' WHEN aff.newer = aff.older THEN '-' ELSE '>' END as direction, aff.affinity, a.labez, a.certainty FROM {view} aff JOIN apparatus_view_agg a ON aff.ms_id2 = a.ms_id JOIN manuscripts ms ON aff.ms_id2 = ms.ms_id LEFT JOIN ranks r ON r.ms_id2 = aff.ms_id2 WHERE aff.ms_id2 NOT IN :exclude AND aff.ms_id1 = :ms_id1 AND aff.rg_id = :rg_id AND aff.common > 0 AND a.pass_id = :pass_id {where} {frag_where} ORDER BY affinity DESC, r.rank, newer DESC, older DESC, hsnr {limit} """, dict(parameters, where=where, frag_where=frag_where, ms_id1=ms.ms_id, hsnr=ms.hsnr, pass_id=passage.pass_id, rg_id=rg_id, limit=limit, view=view, exclude=exclude)) Relatives = collections.namedtuple( 'Relatives', 'rank ms_id hs hsnr length common equal older newer unclear norel direction affinity labez certainty' ) return csvify(Relatives._fields, list(map(Relatives._make, res)))
def textflow(passage_or_id): """ Output a stemma of manuscripts. """ labez = request.args.get('labez') or '' hyp_a = request.args.get('hyp_a') or 'A' connectivity = int(request.args.get('connectivity') or 10) width = float(request.args.get('width') or 0.0) fontsize = float(request.args.get('fontsize') or 10.0) mode = request.args.get('mode') or 'sim' include = request.args.getlist('include[]') or [] fragments = request.args.getlist('fragments[]') or [] checks = request.args.getlist('checks[]') or [] var_only = request.args.getlist('var_only[]') or [] cliques = request.args.getlist('cliques[]') or [] fragments = 'fragments' in fragments checks = 'checks' in checks var_only = 'var_only' in var_only # Panel: Coherence at Variant Passages (GraphViz) cliques = 'cliques' in cliques # consider or ignore cliques leaf_z = 'Z' in include # show leaf z nodes in global textflow? view = 'affinity_view' if mode == 'rec' else 'affinity_p_view' global_textflow = not ((labez != '') or var_only) rank_z = False # include z nodes in ranking? if global_textflow: connectivity = 1 rank_z = True if connectivity == 21: connectivity = 9999 labez_where = '' frag_where = '' z_where = '' if labez != '': labez_where = 'AND app.cbgm AND app.labez = :labez' if hyp_a != 'A': labez_where = 'AND app.cbgm AND (app.labez = :labez OR (app.ms_id = 1 AND :hyp_a = :labez))' if not fragments: frag_where = 'AND a.common > a.ms1_length / 2' if not rank_z: z_where = "AND app.labez !~ '^z' AND app.certainty = 1.0" group_field = 'labez_clique' if cliques else 'labez' with current_app.config.dba.engine.begin() as conn: passage = Passage(conn, passage_or_id) rg_id = passage.request_rg_id(request) exclude = get_excluded_ms_ids(conn, include) # nodes query # # get all nodes or all nodes (hypothetically) attesting labez res = execute( conn, """ SELECT ms_id FROM apparatus app WHERE pass_id = :pass_id AND ms_id NOT IN :exclude {labez_where} {z_where} """, dict(parameters, exclude=exclude, pass_id=passage.pass_id, labez=labez, hyp_a=hyp_a, labez_where=labez_where, z_where=z_where)) nodes = {row[0] for row in res} if not nodes: nodes = {-1} # avoid SQL syntax error # rank query # # query to get the closest ancestors for every node with rank <= connectivity query = """ SELECT ms_id1, ms_id2, rank FROM ( SELECT ms_id1, ms_id2, rank () OVER (PARTITION BY ms_id1 ORDER BY affinity DESC, common, older, newer DESC, ms_id2) AS rank FROM {view} a WHERE ms_id1 IN :nodes AND a.rg_id = :rg_id AND ms_id2 NOT IN :exclude AND newer > older {frag_where} ) AS r WHERE rank <= :connectivity ORDER BY rank """ res = execute( conn, query, dict(parameters, nodes=tuple(nodes), exclude=exclude, rg_id=rg_id, pass_id=passage.pass_id, view=view, labez=labez, connectivity=connectivity, frag_where=frag_where, hyp_a=hyp_a)) Ranks = collections.namedtuple('Ranks', 'ms_id1 ms_id2 rank') ranks = list(map(Ranks._make, res)) # Initially build an unconnected graph with one node for each # manuscript. We will connect the nodes later. Finally we will remove # unconnected nodes. graph = nx.DiGraph() dest_nodes = {r.ms_id1 for r in ranks} src_nodes = {r.ms_id2 for r in ranks} res = execute( conn, """ SELECT ms.ms_id, ms.hs, ms.hsnr, a.labez, a.clique, a.labez_clique, a.certainty FROM apparatus_view_agg a JOIN manuscripts ms USING (ms_id) WHERE pass_id = :pass_id AND ms_id IN :ms_ids """, dict(parameters, ms_ids=tuple(src_nodes | dest_nodes | nodes), pass_id=passage.pass_id)) Mss = collections.namedtuple( 'Mss', 'ms_id hs hsnr labez clique labez_clique certainty') mss = list(map(Mss._make, res)) for ms in mss: attrs = {} attrs['hs'] = ms.hs attrs['hsnr'] = ms.hsnr attrs[ 'labez'] = ms.labez if ms.certainty == 1.0 else 'zw ' + ms.labez attrs['clique'] = ms.clique attrs[ 'labez_clique'] = ms.labez_clique if ms.certainty == 1.0 else 'zw ' + ms.labez_clique attrs['ms_id'] = ms.ms_id attrs['label'] = ms.hs attrs['certainty'] = ms.certainty attrs['clickable'] = '1' if ms.ms_id == 1 and hyp_a != 'A': attrs['labez'] = hyp_a[0] attrs['clique'] = '' attrs['labez_clique'] = hyp_a[0] # FIXME: attrs['shape'] = SHAPES.get (attrs['labez'], SHAPES['a']) graph.add_node(ms.ms_id, **attrs) # Connect the nodes # # Step 1: If the node has internal parents, keep only the top-ranked # internal parent. # # Step 2: If the node has no internal parents, keep the top-ranked # parents for each external attestation. # # Assumption: ranks are sorted top-ranked first def is_z_node(n): labez = n['labez'] cert = n['certainty'] return (labez[0] == 'z') or (cert < 1.0) tags = set() for step in (1, 2): for r in ranks: a1 = graph.nodes[r.ms_id1] if not r.ms_id2 in graph.nodes: continue a2 = graph.nodes[r.ms_id2] if not (global_textflow) and is_z_node(a2): # disregard zz / zw continue if step == 1 and a1[group_field] != a2[group_field]: # differing attestations are handled in step 2 continue if r.ms_id1 in tags: # an ancestor of this node that lays within the node's # attestation was already seen. we need not look into other # attestations continue if str(r.ms_id1) + a2[group_field] in tags: # an ancestor of this node that lays within this attestation # was already seen. we need not look into further nodes continue # add a new parent if r.rank > 1: graph.add_edge(r.ms_id2, r.ms_id1, rank=r.rank, headlabel=r.rank) else: graph.add_edge(r.ms_id2, r.ms_id1) if a1[group_field] == a2[group_field]: # tag: has ancestor node within the same attestation tags.add(r.ms_id1) else: # tag: has ancestor node with this other attestation tags.add(str(r.ms_id1) + a2[group_field]) if not leaf_z: remove_z_leaves(graph) # the if clause fixes #83 graph.remove_nodes_from([ n for n in nx.isolates(graph) if graph.nodes[n]['labez'] != labez ]) if var_only: # Panel: Coherence at Variant Passages (GraphViz) # # if one predecessor is within the same attestation then remove all # other predecessors that are not within the same attestation for n in graph: within = False attestation_n = graph.nodes[n][group_field] for p in graph.predecessors(n): if graph.nodes[p][group_field] == attestation_n: within = True break if within: for p in graph.predecessors(n): if graph.nodes[p][group_field] != attestation_n: graph.remove_edge(p, n) # remove edges between nodes within the same attestation for u, v in list(graph.edges()): if graph.nodes[u][group_field] == graph.nodes[v][group_field]: graph.remove_edge(u, v) # remove now isolated nodes graph.remove_nodes_from(list(nx.isolates(graph))) # unconstrain backward edges (yields a better GraphViz layout) for u, v in graph.edges(): if graph.nodes[u][group_field] > graph.nodes[v][group_field]: graph.adj[u][v]['constraint'] = 'false' else: for n in graph: # Use a different label if the parent's labez_clique differs from this # node's labez_clique. pred = list(graph.predecessors(n)) attrs = graph.nodes[n] if not pred: attrs['label'] = "%s: %s" % (attrs['labez_clique'], attrs['hs']) for p in pred: if attrs['labez_clique'] != graph.nodes[p]['labez_clique']: attrs['label'] = "%s: %s" % (attrs['labez_clique'], attrs['hs']) graph.adj[p][n]['style'] = 'dashed' if checks: for rank in congruence(conn, passage): try: graph.adj[rank.ms_id1][rank.ms_id2]['style'] = 'bold' except KeyError: pass if var_only: dot = helpers.nx_to_dot_subgraphs(graph, group_field, width, fontsize) else: dot = helpers.nx_to_dot(graph, width, fontsize) return dot