|
| 1 | +/** |
| 2 | + * Graph Spec Analyzer - Advanced Properties |
| 3 | + * |
| 4 | + * Compute advanced graph properties including embedding, rooting, temporal, |
| 5 | + * layering, edge ordering, ports, observability, operational semantics, |
| 6 | + * and measure semantics. |
| 7 | + */ |
| 8 | + |
| 9 | +import type { |
| 10 | + AnalyzerGraph, |
| 11 | + ComputePolicy |
| 12 | +} from './types'; |
| 13 | +import { |
| 14 | + unique |
| 15 | +} from './types'; |
| 16 | + |
| 17 | +export const computeEmbedding = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "abstract" } | { kind: "geometric_metric_space" } | { kind: "spatial_coordinates"; dims: 2 | 3 } => { |
| 18 | + // Convention: if every vertex has pos {x,y} or {x,y,z}, treat as spatial_coordinates. |
| 19 | + const poss = g.vertices.map(v => v.attrs?.[policy.posKey]); |
| 20 | + const allHavePos = poss.length > 0 && poss.every(p => typeof p === "object" && p != null); |
| 21 | + if (!allHavePos) return { kind: "abstract" }; |
| 22 | + |
| 23 | + const dims = poss.map(p => { |
| 24 | + const o = p as Record<string, unknown>; |
| 25 | + const hasX = typeof o.x === "number"; |
| 26 | + const hasY = typeof o.y === "number"; |
| 27 | + const hasZ = typeof o.z === "number"; |
| 28 | + if (hasX && hasY && hasZ) return 3 as const; |
| 29 | + if (hasX && hasY) return 2 as const; |
| 30 | + return null; |
| 31 | + }); |
| 32 | + |
| 33 | + if (dims.some(d => d == null)) return { kind: "abstract" }; |
| 34 | + const uniq = unique(dims as Array<2 | 3>); |
| 35 | + if (uniq.length === 1) return { kind: "spatial_coordinates", dims: uniq[0] }; |
| 36 | + // mixed dims -> fall back |
| 37 | + return { kind: "abstract" }; |
| 38 | +}; |
| 39 | + |
| 40 | +export const computeRooting = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "unrooted" } | { kind: "rooted" } | { kind: "multi_rooted" } => { |
| 41 | + // Convention: |
| 42 | + // - rooted if exactly one vertex has attrs[rootKey] === true |
| 43 | + // - multi_rooted if >1 |
| 44 | + // - unrooted otherwise |
| 45 | + const roots = g.vertices.filter(v => v.attrs?.[policy.rootKey] === true); |
| 46 | + if (roots.length === 1) return { kind: "rooted" }; |
| 47 | + if (roots.length > 1) return { kind: "multi_rooted" }; |
| 48 | + return { kind: "unrooted" }; |
| 49 | +}; |
| 50 | + |
| 51 | +export const computeTemporal = (g: AnalyzerGraph, policy: ComputePolicy): |
| 52 | + | { kind: "static" } |
| 53 | + | { kind: "dynamic_structure" } |
| 54 | + | { kind: "temporal_edges" } |
| 55 | + | { kind: "temporal_vertices" } |
| 56 | + | { kind: "time_ordered" } => { |
| 57 | + // Convention: |
| 58 | + // - temporal_vertices if any vertex has attrs[timeKey] |
| 59 | + // - temporal_edges if any edge has attrs[timeKey] |
| 60 | + // - time_ordered if both and values look ordered (numbers) |
| 61 | + // - static otherwise |
| 62 | + const vTimes = g.vertices.map(v => v.attrs?.[policy.timeKey]); |
| 63 | + const eTimes = g.edges.map(e => e.attrs?.[policy.timeKey]); |
| 64 | + const anyV = vTimes.some(t => t != null); |
| 65 | + const anyE = eTimes.some(t => t != null); |
| 66 | + |
| 67 | + const allNumericV = anyV && vTimes.every(t => t == null || typeof t === "number"); |
| 68 | + const allNumericE = anyE && eTimes.every(t => t == null || typeof t === "number"); |
| 69 | + |
| 70 | + if (anyV && anyE && allNumericV && allNumericE) return { kind: "time_ordered" }; |
| 71 | + if (anyV) return { kind: "temporal_vertices" }; |
| 72 | + if (anyE) return { kind: "temporal_edges" }; |
| 73 | + return { kind: "static" }; |
| 74 | +}; |
| 75 | + |
| 76 | +export const computeLayering = (g: AnalyzerGraph, policy: ComputePolicy): |
| 77 | + | { kind: "single_layer" } |
| 78 | + | { kind: "multi_layer" } |
| 79 | + | { kind: "multiplex" } |
| 80 | + | { kind: "interdependent" } => { |
| 81 | + // Convention: |
| 82 | + // - multi_layer if vertices or edges have a layer label and >1 unique layers |
| 83 | + const layers: Array<string | number> = []; |
| 84 | + for (const v of g.vertices) { |
| 85 | + const lv = v.attrs?.[policy.layerKey]; |
| 86 | + if (typeof lv === "string" || typeof lv === "number") layers.push(lv); |
| 87 | + } |
| 88 | + for (const e of g.edges) { |
| 89 | + const le = e.attrs?.[policy.layerKey]; |
| 90 | + if (typeof le === "string" || typeof le === "number") layers.push(le); |
| 91 | + } |
| 92 | + const uniq = unique(layers.map(String)); |
| 93 | + return uniq.length > 1 ? { kind: "multi_layer" } : { kind: "single_layer" }; |
| 94 | +}; |
| 95 | + |
| 96 | +export const computeEdgeOrdering = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "unordered" } | { kind: "ordered" } => { |
| 97 | + const orders = g.edges.map(e => e.attrs?.[policy.edgeOrderKey]); |
| 98 | + if (!orders.every(x => typeof x === "number")) return { kind: "unordered" }; |
| 99 | + return { kind: "ordered" }; |
| 100 | +}; |
| 101 | + |
| 102 | +export const computePorts = (g: AnalyzerGraph, policy: ComputePolicy): { kind: "none" } | { kind: "port_labelled_vertices" } => { |
| 103 | + // Convention: vertex.attrs[portKey] exists => ports |
| 104 | + const anyPorts = g.vertices.some(v => v.attrs?.[policy.portKey] != null); |
| 105 | + return anyPorts ? { kind: "port_labelled_vertices" } : { kind: "none" }; |
| 106 | +}; |
| 107 | + |
| 108 | +export const computeObservability = (g: AnalyzerGraph): { kind: "fully_specified" } | { kind: "partially_observed" } | { kind: "latent_or_inferred" } => { |
| 109 | + // Convention: if any vertex/edge has attrs.latent===true => latent_or_inferred |
| 110 | + // else if any has attrs.observed===false => partially_observed |
| 111 | + // else fully_specified |
| 112 | + const anyLatent = |
| 113 | + g.vertices.some(v => v.attrs?.["latent"] === true) || |
| 114 | + g.edges.some(e => e.attrs?.["latent"] === true); |
| 115 | + |
| 116 | + if (anyLatent) return { kind: "latent_or_inferred" }; |
| 117 | + |
| 118 | + const anyUnobserved = |
| 119 | + g.vertices.some(v => v.attrs?.["observed"] === false) || |
| 120 | + g.edges.some(e => e.attrs?.["observed"] === false); |
| 121 | + |
| 122 | + if (anyUnobserved) return { kind: "partially_observed" }; |
| 123 | + |
| 124 | + return { kind: "fully_specified" }; |
| 125 | +}; |
| 126 | + |
| 127 | +export const computeOperationalSemantics = (g: AnalyzerGraph): |
| 128 | + | { kind: "structural_only" } |
| 129 | + | { kind: "annotated_with_functions" } |
| 130 | + | { kind: "executable" } => { |
| 131 | + // Convention: if any vertex/edge has attrs.exec===true => executable |
| 132 | + // else if any has attrs.fn present => annotated_with_functions |
| 133 | + // else structural_only |
| 134 | + const anyExec = |
| 135 | + g.vertices.some(v => v.attrs?.["exec"] === true) || |
| 136 | + g.edges.some(e => e.attrs?.["exec"] === true); |
| 137 | + if (anyExec) return { kind: "executable" }; |
| 138 | + |
| 139 | + const anyFn = |
| 140 | + g.vertices.some(v => typeof v.attrs?.["fn"] === "string") || |
| 141 | + g.edges.some(e => typeof e.attrs?.["fn"] === "string"); |
| 142 | + if (anyFn) return { kind: "annotated_with_functions" }; |
| 143 | + |
| 144 | + return { kind: "structural_only" }; |
| 145 | +}; |
| 146 | + |
| 147 | +export const computeMeasureSemantics = (g: AnalyzerGraph): { kind: "none" } | { kind: "metric" } | { kind: "cost" } | { kind: "utility" } => { |
| 148 | + // Convention: if weights exist => metric/cost/utility is unknown; choose "metric" |
| 149 | + // If attrs.cost exists => cost; attrs.utility exists => utility |
| 150 | + const anyCost = |
| 151 | + g.edges.some(e => typeof e.attrs?.["cost"] === "number") || |
| 152 | + g.vertices.some(v => typeof v.attrs?.["cost"] === "number"); |
| 153 | + if (anyCost) return { kind: "cost" }; |
| 154 | + |
| 155 | + const anyUtility = |
| 156 | + g.edges.some(e => typeof e.attrs?.["utility"] === "number") || |
| 157 | + g.vertices.some(v => typeof v.attrs?.["utility"] === "number"); |
| 158 | + if (anyUtility) return { kind: "utility" }; |
| 159 | + |
| 160 | + const anyWeight = |
| 161 | + g.edges.some(e => typeof e.weight === "number") || |
| 162 | + g.edges.some(e => Array.isArray(e.attrs?.["weightVector"])); |
| 163 | + if (anyWeight) return { kind: "metric" }; |
| 164 | + |
| 165 | + return { kind: "none" }; |
| 166 | +}; |
0 commit comments