1 # ts2vg's visibility graphs of time series, in Bend2, on every CPU thread. 2 # 3 # bend-vg's command line takes ts2vg's options with the input first: 4 # bend_vg INPUT [-o OUTPUT] [-x XS] [-t TYPE] [-d DIRECTED] [-w WEIGHTED] [-p LIMIT] [-m MODE] 5 # where XS, which ts2vg's command line lacks, holds the x of each value. 6 import Base 7 import ./src/graph.bend as G 8 import ./src/parallel.bend as Par 9 import ./src/series.bend as Series 10 import ./src/report.bend as Report 11 import ./src/vg.bend as VG 12 13 type Mode is Data: 14 EdgeList{} 15 DegreeSequence{} 16 DegreeDistribution{} 17 DegreeCounts{} 18 19 type Flag is Data: 20 Output{} 21 XFile{} 22 GraphType{} 23 Directed{} 24 Weighted{} 25 Limit{} 26 OutputMode{} 27 28 type Entry<-A: Data> is Data: 29 Entry{name: String, value: A} 30 31 type Config is Data: 32 Config{output: String, xs: String, g: G.Graph, dir: G.Direction, k: G.Weight, limit: U32, mode: Mode} 33 Usage{msg: String} 34 35 type Outcome is Data: 36 Ok{text: String} 37 Err{msg: String} 38 39 def usage() -> String: 40 "usage: bend_vg INPUT [-o OUTPUT] [-x XS] [-t natural|horizontal] [-d left_to_right|top_to_bottom] [-w WEIGHT] [-p LIMIT] [-m el|ds|dd|dc]" 41 42 # Option names 43 44 def flags() -> List<&2, Entry>: 45 [Entry{"-o", Output{}}, Entry{"--output", Output{}}, Entry{"-x", XFile{}}, Entry{"--xs", XFile{}}, Entry{"-t", GraphType{}}, Entry{"--type", GraphType{}}, 46 Entry{"-d", Directed{}}, Entry{"--directed", Directed{}}, Entry{"-w", Weighted{}}, Entry{"--weighted", Weighted{}}, 47 Entry{"-p", Limit{}}, Entry{"--penetrable_limit", Limit{}}, Entry{"-m", OutputMode{}}, Entry{"--outputmode", OutputMode{}}] 48 49 def graphs() -> List<&2, Entry>: 50 [Entry{"natural", G.Natural{}}, Entry{"horizontal", G.Horizontal{}}] 51 52 def directions() -> List<&2, Entry>: 53 [Entry{"left_to_right", G.LeftToRight{}}, Entry{"top_to_bottom", G.TopToBottom{}}] 54 55 def weights() -> List<&2, Entry>: 56 [Entry{"distance", G.Distance{}}, Entry{"sq_distance", G.SqDistance{}}, Entry{"v_distance", G.VDistance{}}, 57 Entry{"abs_v_distance", G.AbsVDistance{}}, Entry{"h_distance", G.HDistance{}}, Entry{"abs_h_distance", G.AbsHDistance{}}, 58 Entry{"slope", G.Slope{}}, Entry{"abs_slope", G.AbsSlope{}}, Entry{"angle", G.Angle{}}, Entry{"abs_angle", G.AbsAngle{}}, 59 Entry{"num_penetrations", G.NumPenetrations{}}] 60 61 def modes() -> List<&2, Entry>: 62 [Entry{"el", EdgeList{}}, Entry{"ds", DegreeSequence{}}, Entry{"dd", DegreeDistribution{}}, Entry{"dc", DegreeCounts{}}] 63 64 def lookup.hit(-A: Data, hit: Bool, v: A, rest: Maybe<&2, A>) -> Maybe<&2, A>: 65 match hit: 66 case True{}: 67 Some{v} 68 case False{}: 69 rest 70 71 def lookup.entry(-A: Data, +s: String, e: Entry, rest: Maybe<&2, A>) -> Maybe<&2, A>: 72 Entry{name, v} = e 73 lookup.hit(A, String.eq(s, name), v, rest) 74 75 def lookup(-A: Data, +s: String, table: List<&2, Entry>) -> Maybe<&2, A>: 76 match table: 77 case Nil{}: 78 None{} 79 case Con{e, rest}: 80 lookup.entry(A, s, e, lookup(A, s, rest)) 81 82 # Options 83 84 def defaults() -> Config: 85 Config{"", "", G.Natural{}, G.Undirected{}, G.Unweighted{}, 0, EdgeList{}} 86 87 def bad(name: String, value: String) -> Config: 88 Usage{"invalid value for " ++ name ++ ": " ++ value} 89 90 def with_graph(found: Maybe<&2, G.Graph>, name: String, value: String, o: String, x: String, d: G.Direction, k: G.Weight, l: U32, m: Mode) -> Config: 91 match found: 92 case Some{g}: 93 Config{o, x, g, d, k, l, m} 94 case None{}: 95 bad(name, value) 96 97 def with_direction(found: Maybe<&2, G.Direction>, name: String, value: String, o: String, x: String, g: G.Graph, k: G.Weight, l: U32, m: Mode) -> Config: 98 match found: 99 case Some{d}: 100 Config{o, x, g, d, k, l, m} 101 case None{}: 102 bad(name, value) 103 104 def with_weight(found: Maybe<&2, G.Weight>, name: String, value: String, o: String, x: String, g: G.Graph, d: G.Direction, l: U32, m: Mode) -> Config: 105 match found: 106 case Some{k}: 107 Config{o, x, g, d, k, l, m} 108 case None{}: 109 bad(name, value) 110 111 def with_limit(found: Maybe<&2, U32>, name: String, value: String, o: String, x: String, g: G.Graph, d: G.Direction, k: G.Weight, m: Mode) -> Config: 112 match found: 113 case Some{l}: 114 Config{o, x, g, d, k, l, m} 115 case None{}: 116 bad(name, value) 117 118 def with_mode(found: Maybe<&2, Mode>, name: String, value: String, o: String, x: String, g: G.Graph, d: G.Direction, k: G.Weight, l: U32) -> Config: 119 match found: 120 case Some{m}: 121 Config{o, x, g, d, k, l, m} 122 case None{}: 123 bad(name, value) 124 125 def set(cfg: Config, flag: Maybe<&2, Flag>, +name: String, +value: String) -> Config: 126 match cfg flag: 127 case Usage{msg} _: 128 Usage{msg} 129 case Config{o, x, g, d, k, l, m} None{}: 130 Usage{"unknown option: " ++ name} 131 case Config{o, x, g, d, k, l, m} Some{Output{}}: 132 Config{value, x, g, d, k, l, m} 133 case Config{o, x, g, d, k, l, m} Some{XFile{}}: 134 Config{o, value, g, d, k, l, m} 135 case Config{o, x, g, d, k, l, m} Some{GraphType{}}: 136 with_graph(lookup(G.Graph, value, graphs()), name, value, o, x, d, k, l, m) 137 case Config{o, x, g, d, k, l, m} Some{Directed{}}: 138 with_direction(lookup(G.Direction, value, directions()), name, value, o, x, g, k, l, m) 139 case Config{o, x, g, d, k, l, m} Some{Weighted{}}: 140 with_weight(lookup(G.Weight, value, weights()), name, value, o, x, g, d, l, m) 141 case Config{o, x, g, d, k, l, m} Some{Limit{}}: 142 with_limit(U32.read(value), name, value, o, x, g, d, k, m) 143 case Config{o, x, g, d, k, l, m} Some{OutputMode{}}: 144 with_mode(lookup(Mode, value, modes()), name, value, o, x, g, d, k, l) 145 146 def parse(opts: List, cfg: Config) -> Config: 147 match opts: 148 case Nil{}: 149 cfg 150 case Con{+name, Con{value, rest}}: 151 parse(rest, set(cfg, lookup(Flag, name, flags()), name, value)) 152 case Con{name, Nil{}}: 153 Usage{"missing value for " ++ name} 154 155 # The graph and what to print of it 156 157 def output(mode: Mode, +k: G.Weight, es: G.Edges, +n: U32) -> String: 158 match mode: 159 case EdgeList{}: 160 Report.edges_text(Report.is_weighted(k), es, "") 161 case DegreeSequence{}: 162 Report.degree_sequence(n, Par.degrees(es, n)) 163 case DegreeDistribution{}: 164 Report.degree_counts(True{}, n, Par.degrees(es, n)) 165 case DegreeCounts{}: 166 Report.degree_counts(False{}, n, Par.degrees(es, n)) 167 168 def only_degrees(mode: Mode) -> Bool: 169 match mode: 170 case EdgeList{}: 171 False{} 172 case _: 173 True{} 174 175 def graph_of(+g: G.Graph, +dir: G.Direction, +k: G.Weight, +limit: U32, ys: Array, xs: G.Xs, +n: U32) -> G.Edges: 176 Par.build(VG.Options{g, dir, k, G.neg_inf(), G.inf(), limit}, VG.Series{n, ys, xs}) 177 178 # Where x comes from: the index, or the values of the -x file, last first. 179 type XSource is Data: 180 ByIndex{} 181 Given{rev: List<&2, F32>} 182 183 def xs_of(x: XSource, +n: U32) -> G.Xs: 184 match x: 185 case ByIndex{}: 186 G.Index{} 187 case Given{rev}: 188 G.At{Series.ys(rev, n)} 189 190 def report.built(weighted_degrees: Bool, +g: G.Graph, +dir: G.Direction, +k: G.Weight, +limit: U32, mode: Mode, rev: List<&2, F32>, x: XSource, +n: U32) -> Outcome: 191 match weighted_degrees: 192 case True{}: 193 Err{"Building with 'only_degrees' is only supported for unweighted graphs."} 194 case False{}: 195 Ok{output(mode, k, graph_of(g, dir, k, limit, Series.ys(rev, n), xs_of(x, n), n), n)} 196 197 # x as the index is an F32, which holds every index below 2^24 exactly. 198 def report.sized(too_long: Bool, +g: G.Graph, +dir: G.Direction, +k: G.Weight, +limit: U32, +mode: Mode, rev: List<&2, F32>, +n: U32) -> Outcome: 199 match too_long: 200 case True{}: 201 Err{"bend_vg takes at most 16777216 values, since x is the index as an F32"} 202 case False{}: 203 report.built(only_degrees(mode) && Report.is_weighted(k), g, dir, k, limit, mode, rev, ByIndex{}, n) 204 205 def report.xs(x: Series.XsCheck, +g: G.Graph, +dir: G.Direction, +k: G.Weight, +limit: U32, +mode: Mode, rev: List<&2, F32>, +n: U32) -> Outcome: 206 match x: 207 case Series.XsBad{msg}: 208 Err{msg} 209 case Series.XsOk{xrev}: 210 report.built(only_degrees(mode) && Report.is_weighted(k), g, dir, k, limit, mode, rev, Given{xrev}, n) 211 212 # The x coordinates of the -x file, if one was given. 213 type XsIn is Data: 214 NoXs{} 215 XsRead{p: Series.Parsed} 216 217 def report(p: Series.Parsed, x: XsIn, +g: G.Graph, +dir: G.Direction, +k: G.Weight, +limit: U32, +mode: Mode) -> Outcome: 218 match p x: 219 case Series.Bad{line} _: 220 Err{"line " ++ U32.show(line) ++ " holds no number"} 221 case Series.Values{rev, +n} NoXs{}: 222 report.sized(U32.is_gt(n, 16777216), g, dir, k, limit, mode, rev, n) 223 case Series.Values{rev, +n} XsRead{xp}: 224 report.xs(Series.xs(xp, n), g, dir, k, limit, mode, rev, n) 225 226 # Files 227 228 def write_text.done(m: File & Result<&1, &1, U32 & String, Unit>) -> IO(Unit): 229 (f, r) = m 230 do IO: 231 done : Unit <- IO.pass(Unit, r) 232 File.close(f) 233 234 def write_text(path: String, text: String) -> IO(Unit): 235 do IO: 236 f : File <- IO.try(File, File.open(path, "w")) 237 m : File & Result<&1, &1, U32 & String, Unit> <- File.write(f, text) 238 write_text.done(m) 239 240 def emit.to(stdout: Bool, path: String, text: String) -> IO(Unit): 241 match stdout: 242 case True{}: 243 IO.write(text) 244 case False{}: 245 write_text(path, text) 246 247 def emit(+path: String, outcome: Outcome) -> IO(Unit): 248 match outcome: 249 case Ok{text}: 250 emit.to(String.is_empty(path), path, text) 251 case Err{msg}: 252 IO.die(Unit, 1, msg) 253 254 def read_xs(none: Bool, path: String) -> IO(XsIn): 255 match none: 256 case True{}: 257 IO.pure(XsIn, NoXs{}) 258 case False{}: 259 do IO: 260 text : String <- Series.read_text(path) 261 return XsRead{Series.parse(text)} 262 263 def run(input: String, cfg: Config) -> IO(Unit): 264 match cfg: 265 case Usage{msg}: 266 IO.die(Unit, 2, msg ++ "\n" ++ usage()) 267 case Config{+out, +xpath, g, dir, k, limit, mode}: 268 do IO: 269 text : String <- Series.read_text(input) 270 x : XsIn <- read_xs(String.is_empty(xpath), xpath) 271 emit(out, report(Series.parse(text), x, g, dir, k, limit, mode)) 272 273 def start(args: List) -> IO(Unit): 274 match args: 275 case Con{prog, Con{input, opts}}: 276 run(input, parse(opts, defaults())) 277 case _: 278 IO.die(Unit, 2, usage()) 279 280 def main() -> IO(Unit): 281 do IO: 282 args : List <- IO.args() 283 start(args)