bend_visgraph.bend

bend-visgraph@2.0.0.0 · 283 lines · 41 definitions

1# ts2vg's visibility graphs of time series, in Bend2, on every CPU thread.
2# Docs, tests and benchmark: https://github.com/aaaxn/bend-visgraph
3#
4# bend-visgraph's command line takes ts2vg's options with the input first:
5#   bend_visgraph INPUT [-o OUTPUT] [-x XS] [-t TYPE] [-d DIRECTED] [-w WEIGHTED] [-p LIMIT] [-m MODE]
6# where XS, which ts2vg's command line lacks, holds the x of each value.
7import Base
8import bend-visgraph@2.0.0.0/visgraph.bend as VG
9import bend-visgraph@2.0.0.0/parallel.bend as Par
10import bend-visgraph@2.0.0.0/src/series.bend as Series
11import bend-visgraph@2.0.0.0/src/report.bend as Report
12
13type Mode is Data:
14  EdgeList{}
15  DegreeSequence{}
16  DegreeDistribution{}
17  DegreeCounts{}
18
19type Flag is Data:
20  Output{}
21  XFile{}
22  GraphType{}
23  Directed{}
24  Weighted{}
25  Limit{}
26  OutputMode{}
27
28type Entry<-A: Data> is Data:
29  Entry{name: String, value: A}
30
31type Config is Data:
32  Config{output: String, xs: String, g: VG.Visibility, dir: VG.Direction, k: VG.Weight, limit: U32, mode: Mode}
33  Usage{msg: String}
34
35type Outcome is Data:
36  Ok{text: String}
37  Err{msg: String}
38
39def usage() -> String:
40  "usage: bend_visgraph 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
44def flags() -> List<&2, Entry<Flag>>:
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
49def graphs() -> List<&2, Entry<VG.Visibility>>:
50  [Entry{"natural", VG.Natural{}}, Entry{"horizontal", VG.Horizontal{}}]
51
52def directions() -> List<&2, Entry<VG.Direction>>:
53  [Entry{"left_to_right", VG.LeftToRight{}}, Entry{"top_to_bottom", VG.TopToBottom{}}]
54
55def weights() -> List<&2, Entry<VG.Weight>>:
56  [Entry{"distance", VG.Distance{}}, Entry{"sq_distance", VG.SqDistance{}}, Entry{"v_distance", VG.VDistance{}},
57   Entry{"abs_v_distance", VG.AbsVDistance{}}, Entry{"h_distance", VG.HDistance{}}, Entry{"abs_h_distance", VG.AbsHDistance{}},
58   Entry{"slope", VG.Slope{}}, Entry{"abs_slope", VG.AbsSlope{}}, Entry{"angle", VG.Angle{}}, Entry{"abs_angle", VG.AbsAngle{}},
59   Entry{"num_penetrations", VG.NumPenetrations{}}]
60
61def modes() -> List<&2, Entry<Mode>>:
62  [Entry{"el", EdgeList{}}, Entry{"ds", DegreeSequence{}}, Entry{"dd", DegreeDistribution{}}, Entry{"dc", DegreeCounts{}}]
63
64def 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
71def lookup.entry(-A: Data, +s: String, e: Entry<A>, rest: Maybe<&2, A>) -> Maybe<&2, A>:
72  Entry{name, v} = e
73  lookup.hit(A, String.eq(s, name), v, rest)
74
75def lookup(-A: Data, +s: String, table: List<&2, Entry<A>>) -> 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
84def defaults() -> Config:
85  Config{"", "", VG.Natural{}, VG.Undirected{}, VG.Unweighted{}, 0, EdgeList{}}
86
87def bad(name: String, value: String) -> Config:
88  Usage{"invalid value for " ++ name ++ ": " ++ value}
89
90def with_graph(found: Maybe<&2, VG.Visibility>, name: String, value: String, o: String, x: String, d: VG.Direction, k: VG.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
97def with_direction(found: Maybe<&2, VG.Direction>, name: String, value: String, o: String, x: String, g: VG.Visibility, k: VG.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
104def with_weight(found: Maybe<&2, VG.Weight>, name: String, value: String, o: String, x: String, g: VG.Visibility, d: VG.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
111def with_limit(found: Maybe<&2, U32>, name: String, value: String, o: String, x: String, g: VG.Visibility, d: VG.Direction, k: VG.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
118def with_mode(found: Maybe<&2, Mode>, name: String, value: String, o: String, x: String, g: VG.Visibility, d: VG.Direction, k: VG.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
125def 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(VG.Visibility, 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(VG.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(VG.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
146def parse(opts: List<String>, 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
157def output(mode: Mode, +k: VG.Weight, g: VG.Graph, +n: U32) -> String:
158  match mode:
159    case EdgeList{}:
160      Report.edge_list(Report.is_weighted(k), g)
161    case DegreeSequence{}:
162      Report.degree_sequence(n, Par.degrees(g))
163    case DegreeDistribution{}:
164      Report.degree_counts(True{}, n, Par.degrees(g))
165    case DegreeCounts{}:
166      Report.degree_counts(False{}, n, Par.degrees(g))
167
168def only_degrees(mode: Mode) -> Bool:
169  match mode:
170    case EdgeList{}:
171      False{}
172    case _:
173      True{}
174
175def graph_of(+g: VG.Visibility, +dir: VG.Direction, +k: VG.Weight, +limit: U32, ys: Array<F32>, xs: VG.Xs, +n: U32) -> VG.Graph:
176  Par.build(VG.Options{g, dir, k, VG.neg_inf(), VG.inf(), limit}, VG.Series{n, ys, xs})
177
178# Where x comes from: the index, or the values of the -x file, last first.
179type XSource is Data:
180  ByIndex{}
181  Given{rev: List<&2, F32>}
182
183def xs_of(x: XSource, +n: U32) -> VG.Xs:
184  match x:
185    case ByIndex{}:
186      VG.Index{}
187    case Given{rev}:
188      VG.At{Series.ys(rev, n)}
189
190def report.built(weighted_degrees: Bool, +g: VG.Visibility, +dir: VG.Direction, +k: VG.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.
198def report.sized(too_long: Bool, +g: VG.Visibility, +dir: VG.Direction, +k: VG.Weight, +limit: U32, +mode: Mode, rev: List<&2, F32>, +n: U32) -> Outcome:
199  match too_long:
200    case True{}:
201      Err{"bend_visgraph 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
205def report.xs(x: Series.XsCheck, +g: VG.Visibility, +dir: VG.Direction, +k: VG.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.
213type XsIn is Data:
214  NoXs{}
215  XsRead{p: Series.Parsed}
216
217def report(p: Series.Parsed, x: XsIn, +g: VG.Visibility, +dir: VG.Direction, +k: VG.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
228def write_text.done(m: File & Result<&1, &1, U32 & String, Unit>) -> IO(Unit):
229  (f, r) = m
230  do IO<Unit>:
231    done : Unit <- IO.pass(Unit, r)
232    File.close(f)
233
234def write_text(path: String, text: String) -> IO(Unit):
235  do IO<Unit>:
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
240def 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
247def 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
254def 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<XsIn>:
260        text : String <- Series.read_text(path)
261        return XsRead{Series.parse(text)}
262
263def 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<Unit>:
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
273def start(args: List<String>) -> 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
280def main() -> IO(Unit):
281  do IO<Unit>:
282    args : List<String> <- IO.args()
283    start(args)