[1990] | 1 | (**
|
---|
| 2 | Experimental channel assigning program using an evolutionary algorithm.
|
---|
| 3 |
|
---|
| 4 | The scoring function is simple:
|
---|
| 5 | - the score of a combination of two interfaces is 1 if there's two or more
|
---|
| 6 | channels between them, -1 if there's 1 channel between them, -5 if
|
---|
| 7 | there's no channels between them (they're on adjacent channels) and -10
|
---|
| 8 | if they're on the same channel
|
---|
| 9 | - the score for a node is the sum of the scores of all the combinations of
|
---|
| 10 | interfaces for that node, minus any node-specific penalties (eg.
|
---|
| 11 | channels 7, 8 and 9 are unusable and score -1000 on node x), scaled by
|
---|
| 12 | the number of interfaces to give more weight to larger nodes (the
|
---|
| 13 | assumption being that larger nodes are more important nodes)
|
---|
| 14 | - the total score of the network is the sum of the score of the nodes,
|
---|
| 15 | minus any network-wide penalties (eg. the omni's for node x and node y
|
---|
| 16 | can see eachother, so they should be apart)
|
---|
| 17 |
|
---|
[1991] | 18 | special cases:
|
---|
| 19 | - on node x, interfaces y and z are well separated and can live on the
|
---|
| 20 | same channel
|
---|
[1993] | 21 | - the link between x on y and z on w is very important, make sure it is
|
---|
| 22 | well separated on both ends of the link
|
---|
[1991] | 23 | - interface x on node y will bother interface z on node w and need to
|
---|
| 24 | be on separate channels
|
---|
| 25 | - on node x, channels y, z and w are not usable
|
---|
| 26 | - node x y and z should be treated as one node
|
---|
| 27 | - if at all possible, do not put accesspoints above channel 11
|
---|
| 28 |
|
---|
[1990] | 29 | - install an O'Caml compiler. /usr/ports/lang/ocaml/ in FreeBSD, ocaml in
|
---|
| 30 | Debian.
|
---|
| 31 |
|
---|
| 32 | - compile with
|
---|
| 33 |
|
---|
| 34 | $ ocamlopt -o foo str.cmxa channelga.ml
|
---|
| 35 |
|
---|
| 36 | - run with
|
---|
| 37 |
|
---|
| 38 | $ ./foo f
|
---|
| 39 |
|
---|
| 40 | where f is the result of running prepconf.py on a file with a list of
|
---|
| 41 | paths to the wleiden.conf's to consider.
|
---|
[1997] | 42 |
|
---|
| 43 | Lodewijk Voge <lvoge@cs.vu.nl>
|
---|
[1990] | 44 | *)
|
---|
| 45 |
|
---|
| 46 | (* a few constants suitable for twiddling *)
|
---|
| 47 | (** How large a population should the system maintain? *)
|
---|
[1991] | 48 | let population_size = 20
|
---|
[1990] | 49 | (** How long should the system iterate after an improvement? *)
|
---|
[1991] | 50 | and max_stagnant_iterations = 2000
|
---|
[1993] | 51 | (** What is the chance for an ESSID-to-channel assignment to mutate to a
|
---|
[1990] | 52 | random channel? *)
|
---|
[1993] | 53 | and mutation_rate = 0.1
|
---|
| 54 | (** The most basic score table *)
|
---|
[1997] | 55 | and scoretable = [ (<=) 2, 1;
|
---|
| 56 | (==) 2, -30;
|
---|
| 57 | (==) 1, -70;
|
---|
| 58 | (==) 0, -100 ]
|
---|
[1973] | 59 |
|
---|
[1979] | 60 | (* the type definitions. note that Caml has trouble with mutually recursive
|
---|
| 61 | data structures. you can define them, you just can't ever instantiate them.
|
---|
| 62 | this is why the fields in wi are all loose references by way of strings *)
|
---|
[1973] | 63 | type wi = {
|
---|
[1979] | 64 | wi_name: string;
|
---|
| 65 | wi_nodename: string;
|
---|
[1998] | 66 | wi_essid: string
|
---|
| 67 | }
|
---|
[1979] | 68 | type group = {
|
---|
| 69 | group_essid: string;
|
---|
[1998] | 70 | mutable group_wis: wi list
|
---|
| 71 | }
|
---|
[1979] | 72 | type node = {
|
---|
| 73 | node_name: string;
|
---|
[1998] | 74 | node_wis: wi list
|
---|
| 75 | }
|
---|
[1990] | 76 | (** A configuration is an assignment of groups, identified by essid, to a
|
---|
| 77 | channel, plus a score. The code should be careful not to use the score
|
---|
| 78 | between mutating an re-evaluating. *)
|
---|
| 79 | type configuration = {
|
---|
| 80 | mutable score: int;
|
---|
[1998] | 81 | conf: (string, int) Hashtbl.t
|
---|
| 82 | }
|
---|
| 83 | type 'a maybe = Nothing | Just of 'a
|
---|
[1973] | 84 |
|
---|
[1990] | 85 | (** The global nodes hash, mapping from node name to node struct. *)
|
---|
| 86 | let nodes = Hashtbl.create 4
|
---|
| 87 | (** The global groups hash, mapping from essid to group struct. *)
|
---|
| 88 | let groups = Hashtbl.create 4
|
---|
[1973] | 89 |
|
---|
[1993] | 90 | (* Now the hashes for the special cases *)
|
---|
| 91 | (** Hash mapping from nodename to a list of winame's indicating the wi's that
|
---|
| 92 | don't interfere with eachother for the given node *)
|
---|
[1991] | 93 | let nointerference = Hashtbl.create 4
|
---|
[1993] | 94 | (** List of (nodename1, winame1, nodename2, winame2) tuples indicating a very
|
---|
| 95 | important link that should be well-separated on both ends *)
|
---|
| 96 | let importantlinks = ref []
|
---|
| 97 | (** Hash mapping from nodename to a list of unusable channels for that node *)
|
---|
[1991] | 98 | let unusable = Hashtbl.create 4
|
---|
[1993] | 99 | (** List of (nodename1, winame1, nodename2, winame2) tuples indicating two
|
---|
| 100 | interfering interfaces on different nodes *)
|
---|
| 101 | let interference = ref []
|
---|
[1991] | 102 |
|
---|
[1993] | 103 | (** Run the given diff against the given scoretable and return the score *)
|
---|
[1991] | 104 | let rec runtable t diff =
|
---|
| 105 | match t with
|
---|
| 106 | [] -> assert false
|
---|
| 107 | | (cond, s)::xs -> if (cond diff) then s
|
---|
| 108 | else runtable xs diff
|
---|
| 109 |
|
---|
[1979] | 110 | (* some convenience functions *)
|
---|
[1973] | 111 |
|
---|
[1990] | 112 | (** Function composition. *)
|
---|
| 113 | let compose f g = fun x -> f(g(x))
|
---|
| 114 | let ($) = compose
|
---|
| 115 | (** Turn two individual values into a tuple *)
|
---|
| 116 | let maketuple a b = (a, b)
|
---|
| 117 | (** Shorthand for List.hd *)
|
---|
| 118 | let head = List.hd
|
---|
| 119 | (** Shorthand for List.tail *)
|
---|
| 120 | let tail = List.tl
|
---|
[1999] | 121 | let even x = (x mod 2) == 0
|
---|
| 122 | (*let shuffle = Array.sort (fun _ _ -> 1 - Random.int(2))*)
|
---|
[1990] | 123 | let just x = match x with
|
---|
| 124 | Nothing -> assert false
|
---|
| 125 | | Just s -> s
|
---|
| 126 | (** Given a hashtable, return all the keys as a list *)
|
---|
| 127 | let keys t = Hashtbl.fold (fun k d a -> k::a) t []
|
---|
| 128 | (** Given a hashtable, return all the values as a list *)
|
---|
| 129 | let values t = Hashtbl.fold (fun k d a -> d::a) t []
|
---|
| 130 | (** Copy one array into the other *)
|
---|
| 131 | let copyarray src dest = Array.blit src 0 dest 0 (Array.length src)
|
---|
| 132 |
|
---|
[1993] | 133 | (** Is the given element in the given list? uses compare, so it works on
|
---|
| 134 | strings as well *)
|
---|
| 135 | let in_list l e = try
|
---|
| 136 | let _ = List.find (fun x -> (compare e x) == 0) l in
|
---|
[1991] | 137 | true
|
---|
| 138 | with Not_found -> false
|
---|
| 139 |
|
---|
[1990] | 140 | (** Given a list, return a list of pairs with all possible combinations of
|
---|
[1973] | 141 | items from the given list *)
|
---|
| 142 | let rec combinations l =
|
---|
| 143 | match l with
|
---|
| 144 | [] -> []
|
---|
[1990] | 145 | | x::xs -> (List.map (maketuple x) xs)@(combinations xs)
|
---|
[1973] | 146 |
|
---|
[1990] | 147 | (** Given a configuration and two wi's, return the score *)
|
---|
[1991] | 148 | let wi_score c unusable nointerference wi1 wi2 =
|
---|
[1979] | 149 | let channel1 = c wi1.wi_essid in
|
---|
| 150 | let channel2 = c wi2.wi_essid in
|
---|
[1973] | 151 | let diff = abs (channel1 - channel2) in
|
---|
[1991] | 152 | let is_unusable = in_list unusable in
|
---|
| 153 | if (is_unusable channel1) || (is_unusable channel2) then -10000
|
---|
[1993] | 154 | else if (in_list nointerference wi1.wi_name) &&
|
---|
| 155 | (in_list nointerference wi2.wi_name) then 1
|
---|
[1998] | 156 | else runtable scoretable diff
|
---|
[1973] | 157 |
|
---|
[1998] | 158 | (** Given a configuration and a node, return the score. this is simply the sum
|
---|
| 159 | of the scores of all the combinations of interfaces, written down as a fold
|
---|
| 160 | for efficiency *)
|
---|
[1973] | 161 | let node_score c n =
|
---|
[1991] | 162 | let nointerference_ = try Hashtbl.find nointerference n.node_name
|
---|
| 163 | with Not_found -> [] in
|
---|
| 164 | let unusable_ = try Hashtbl.find unusable n.node_name
|
---|
| 165 | with Not_found -> [] in
|
---|
[1998] | 166 | let f a (wi1, wi2) = a + (wi_score c unusable_ nointerference_ wi1 wi2) in
|
---|
| 167 | let base_score = List.fold_left f 0 (combinations n.node_wis) in
|
---|
[1990] | 168 | base_score * (List.length n.node_wis)
|
---|
[1973] | 169 |
|
---|
[1993] | 170 | (** Score the given pair of interferent interfaces against the given
|
---|
| 171 | configuration *)
|
---|
| 172 | let score_interference c (nodename1, winame1, nodename2, winame2) =
|
---|
[1998] | 173 | let node1 = Hashtbl.find nodes nodename1 in
|
---|
| 174 | let node2 = Hashtbl.find nodes nodename2 in
|
---|
| 175 | let f winame = fun wi -> (compare wi.wi_name winame) == 0 in
|
---|
| 176 | let wi1 = List.find (f winame1) node1.node_wis in
|
---|
| 177 | let wi2 = List.find (f winame2) node2.node_wis in
|
---|
[1991] | 178 | let channel1 = c wi1.wi_essid in
|
---|
| 179 | let channel2 = c wi2.wi_essid in
|
---|
| 180 | let diff = abs (channel1 - channel2) in
|
---|
[1993] | 181 | let res = runtable scoretable diff in
|
---|
| 182 | res
|
---|
[1991] | 183 |
|
---|
[1990] | 184 | (** Given a list of nodes and a configuration, return the score for the whole
|
---|
[1993] | 185 | configuration. This is the sum of the scores for all nodes, plus the sum
|
---|
| 186 | of the scores for all user-specified interferent pairs of interfaces. *)
|
---|
[1990] | 187 | let score_configuration ns c =
|
---|
[1991] | 188 | let mapper = Hashtbl.find c in
|
---|
[1998] | 189 | let f1 a n = a + (node_score mapper n) in
|
---|
| 190 | let nodescores = List.fold_left f1 0 ns in
|
---|
| 191 | let f2 a i = a + (score_interference mapper i) in
|
---|
| 192 | let interference_score = List.fold_left f2 0 !interference in
|
---|
| 193 | nodescores + interference_score
|
---|
[1973] | 194 |
|
---|
[1980] | 195 | (** Return a random configuration. For some reason, if this function accesses
|
---|
| 196 | the global 'groups' hash instead of getting it passed in from above, that
|
---|
| 197 | hash is empty. *)
|
---|
[1990] | 198 | let random_configuration groups evaluate =
|
---|
| 199 | let h = Hashtbl.create 30 in
|
---|
| 200 | Hashtbl.iter (fun k _ -> Hashtbl.add h k (1 + (Random.int 12))) groups;
|
---|
| 201 | { score = (evaluate h); conf = h }
|
---|
[1973] | 202 |
|
---|
[1979] | 203 | let print_conf conf =
|
---|
[1998] | 204 | let channel wi = string_of_int (Hashtbl.find conf wi.wi_essid) in
|
---|
| 205 | let print_wi wi = wi.wi_name ^ ": " ^ (channel wi) in
|
---|
| 206 | let wis node = List.fold_left (fun a wi -> a ^ " " ^ (print_wi wi))
|
---|
| 207 | "" node.node_wis in
|
---|
| 208 | let cmpnode a b = compare (a.node_name) (b.node_name) in
|
---|
| 209 | let sorted_nodes = List.sort cmpnode (values nodes) in
|
---|
| 210 | let print_node n = print_string (n.node_name ^ ": " ^ (wis n) ^ "\n") in
|
---|
| 211 | List.iter print_node sorted_nodes
|
---|
[1979] | 212 |
|
---|
[1999] | 213 | (** n-point crossover operator. pick n points along the length of the parents,
|
---|
| 214 | produce a child by copying from one parent, switching parents when hitting a
|
---|
| 215 | chosen crossover point *)
|
---|
| 216 | let crossover n c1 c2 =
|
---|
| 217 | let keys1 = keys (c1.conf) in
|
---|
| 218 | let numkeys1 = List.length keys1 in
|
---|
| 219 | let pickpoint i = (if even i then c1.conf else c2.conf),
|
---|
| 220 | (if i < n then (Random.int numkeys1) else numkeys1) in
|
---|
| 221 | let crosspoints = Array.init (n + 1) pickpoint in
|
---|
| 222 | let result = Hashtbl.create (List.length keys1) in
|
---|
| 223 | let i = ref 0 in
|
---|
| 224 | Array.sort (fun a b -> compare (snd a) (snd b)) crosspoints;
|
---|
| 225 | Array.iter (fun (h, p) -> while !i < p do
|
---|
| 226 | let key = List.nth keys1 !i in
|
---|
| 227 | Hashtbl.add result key (Hashtbl.find h key);
|
---|
| 228 | incr i
|
---|
| 229 | done) crosspoints;
|
---|
| 230 | assert ((List.length (keys result)) == (List.length keys1));
|
---|
| 231 | { score = 0; conf = result }
|
---|
| 232 |
|
---|
[1990] | 233 | (** Generalized evolutionary algorithm driver.
|
---|
| 234 | initialize: () -> configuration array
|
---|
| 235 | recombine:
|
---|
| 236 | mutate: configuration array -> configuration array
|
---|
| 237 | evaluate: configuration array -> configuration array
|
---|
| 238 | select: configuration array -> configuration array
|
---|
| 239 |
|
---|
| 240 | and the result is the best configuration *)
|
---|
| 241 | let evolutionary_algorithm initialize recombine mutate evaluate select =
|
---|
| 242 | let population = (evaluate $ initialize) () in
|
---|
| 243 | let last_high_score = ref population.(0).score in
|
---|
[1973] | 244 | let iterations_since_new_high_score = ref 0 in
|
---|
[1979] | 245 | let generation = ref 0 in
|
---|
[1973] | 246 | let all_nodes = values nodes in
|
---|
[1999] | 247 | (*let iterate = recombine $ mutate $ evaluate $ select in*)
|
---|
| 248 | let iterate = select $ evaluate $ mutate $ recombine in
|
---|
[1998] | 249 | while !iterations_since_new_high_score < max_stagnant_iterations do
|
---|
| 250 | let newpop = iterate population in
|
---|
[1999] | 251 | assert ((Array.length newpop) == population_size);
|
---|
[1990] | 252 | copyarray newpop population;
|
---|
| 253 | let high_score = population.(0).score in
|
---|
[1973] | 254 | if high_score > !last_high_score then begin
|
---|
| 255 | last_high_score := high_score;
|
---|
| 256 | iterations_since_new_high_score := 0
|
---|
| 257 | end;
|
---|
[1979] | 258 | assert (!last_high_score >= high_score);
|
---|
| 259 | if (!generation mod 10) == 0 then begin
|
---|
| 260 | print_int !generation;
|
---|
| 261 | print_string ": ";
|
---|
| 262 | print_int !last_high_score;
|
---|
| 263 | print_newline();
|
---|
| 264 | end;
|
---|
| 265 | incr iterations_since_new_high_score;
|
---|
| 266 | incr generation
|
---|
[1998] | 267 | done;
|
---|
| 268 | population.(0)
|
---|
[1973] | 269 |
|
---|
[1993] | 270 | (** BEGIN PARSING CODE *)
|
---|
| 271 |
|
---|
| 272 | (** Given a filename, return a list of all the lines in the file with the given
|
---|
| 273 | filename. Don't count on the order of the lines in the result. *)
|
---|
| 274 | let snarf_lines fname =
|
---|
| 275 | let infile = open_in fname in
|
---|
| 276 | let result = ref [] in
|
---|
| 277 | try
|
---|
| 278 | while true do
|
---|
| 279 | result := (input_line infile)::!result
|
---|
| 280 | done;
|
---|
| 281 | !result (* never gets here *)
|
---|
| 282 | with End_of_file -> !result
|
---|
| 283 |
|
---|
| 284 | (** Read the main input from the given filename *)
|
---|
| 285 | let parse_file fname =
|
---|
| 286 | let spacere = Str.regexp " " in
|
---|
[1998] | 287 | (** Given the name of the node currently being parsed, parse the given
|
---|
| 288 | tuple that consists of a wi name and an essid. *)
|
---|
[1993] | 289 | let parse_pair nodename (wname, essid) =
|
---|
[1998] | 290 | let new_wi = { wi_name = wname;
|
---|
| 291 | wi_nodename = nodename;
|
---|
| 292 | wi_essid = essid} in
|
---|
| 293 | let _ = try
|
---|
[1993] | 294 | let group = Hashtbl.find groups essid in
|
---|
| 295 | group.group_wis <- new_wi::group.group_wis;
|
---|
[1998] | 296 | with Not_found ->
|
---|
| 297 | let group = { group_essid = essid;
|
---|
| 298 | group_wis = [ new_wi ] } in
|
---|
[1993] | 299 | Hashtbl.add groups essid group in
|
---|
| 300 | new_wi in
|
---|
| 301 | let parse_fields fields =
|
---|
| 302 | let nodename = head fields in
|
---|
[1998] | 303 | let rec makepairs l =
|
---|
| 304 | match l with
|
---|
| 305 | [] -> []
|
---|
| 306 | | x::[] -> assert false
|
---|
| 307 | | a::b::xs -> (a, b)::(makepairs xs) in
|
---|
| 308 | let wis = List.map (parse_pair nodename)
|
---|
| 309 | (makepairs (tail fields)) in
|
---|
[1993] | 310 | let sorted_wis = List.sort compare wis in
|
---|
| 311 | let node = { node_name = nodename; node_wis = sorted_wis } in
|
---|
| 312 | Hashtbl.add nodes nodename node in
|
---|
| 313 | List.iter (parse_fields $ (Str.split spacere)) (snarf_lines fname)
|
---|
| 314 |
|
---|
| 315 | (* the parsers for the special case components *)
|
---|
| 316 |
|
---|
| 317 | (** The first field is the nodename, the rest are interface names *)
|
---|
| 318 | let parse_nointerference fs = Hashtbl.add nointerference (head fs) (tail fs)
|
---|
| 319 | (** Read four fields from the given list and add them as a tuple to the given
|
---|
| 320 | list reference *)
|
---|
| 321 | let parse_quadruplet l fs =
|
---|
| 322 | let f = List.nth fs in
|
---|
| 323 | l := (f 0, f 1, f 2, f 3)::!l
|
---|
| 324 | (** The first field is the nodename, the rest are channels.*)
|
---|
| 325 | let parse_unusable fs =
|
---|
| 326 | let channels = List.map int_of_string (tail fs) in
|
---|
| 327 | Hashtbl.add unusable (head fs) channels
|
---|
| 328 | (** The first field is the supernode name, the rest are the names of the
|
---|
| 329 | subnodes. Construct a new node for the supernode and remove the subnodes
|
---|
| 330 | from the nodes hash *)
|
---|
| 331 | let parse_supernode fs =
|
---|
| 332 | let nodename = head fs in
|
---|
| 333 | let subnodenames = tail fs in
|
---|
| 334 | let subnodes = List.map (Hashtbl.find nodes) subnodenames in
|
---|
| 335 | List.iter (Hashtbl.remove nodes) subnodenames;
|
---|
| 336 | let prefixed_wis n = List.map (fun w -> { w with wi_name = n.node_name ^ "." ^ w.wi_name}) n.node_wis in
|
---|
| 337 | let wis = List.fold_left (fun a s -> a@(prefixed_wis s)) [] subnodes in
|
---|
| 338 | let node = { node_name = nodename; node_wis = wis } in
|
---|
| 339 | Hashtbl.add nodes nodename node
|
---|
| 340 |
|
---|
| 341 | let parse_special_conf fname =
|
---|
| 342 | let spacere = Str.regexp " " in
|
---|
[1997] | 343 | let functable = [ "nointerference", parse_nointerference;
|
---|
| 344 | "important", parse_quadruplet importantlinks;
|
---|
| 345 | "interference", parse_quadruplet interference;
|
---|
| 346 | "unusable", parse_unusable;
|
---|
| 347 | "supernode", parse_supernode ] in
|
---|
[1993] | 348 | let do_line fs = (List.assoc (head fs) functable) (tail fs) in
|
---|
| 349 | try
|
---|
| 350 | List.iter (do_line $ Str.split spacere) (snarf_lines fname)
|
---|
| 351 | with x -> ()
|
---|
| 352 |
|
---|
| 353 | (** END PARSING CODE *)
|
---|
| 354 |
|
---|
[1990] | 355 | let main =
|
---|
| 356 | parse_file Sys.argv.(1);
|
---|
[1993] | 357 | parse_special_conf "special.conf";
|
---|
[1990] | 358 | Random.self_init();
|
---|
| 359 | let all_nodes = values nodes in
|
---|
| 360 | let evaluate_hash = score_configuration all_nodes in
|
---|
| 361 | let initialize () = Array.init population_size (fun _ -> random_configuration groups evaluate_hash) in
|
---|
[1999] | 362 | let recombine pop = pop in
|
---|
| 363 | (*
|
---|
| 364 | let numoffspring = Random.int population_size in
|
---|
| 365 | let children = Array.init numoffspring (fun _ ->
|
---|
| 366 | let father = pop.(Random.int population_size) in
|
---|
| 367 | let mother = pop.(Random.int population_size) in
|
---|
| 368 | crossover 2 father mother) in
|
---|
| 369 | Array.append pop children in *)
|
---|
[1998] | 370 | let maxchannel essid =
|
---|
| 371 | let group = Hashtbl.find groups essid in
|
---|
| 372 | if (List.length group.group_wis) == 1 then 11
|
---|
| 373 | else 13 in
|
---|
[1990] | 374 | let mutate_conf conf =
|
---|
[1998] | 375 | Hashtbl.iter (fun essid _ ->
|
---|
| 376 | let f = Random.float 1.0 in
|
---|
| 377 | if (f < mutation_rate) then
|
---|
| 378 | let channel = 1 + (Random.int (maxchannel essid)) in
|
---|
| 379 | Hashtbl.replace conf essid channel) conf in
|
---|
| 380 | let mutate population =
|
---|
| 381 | let mutants = Array.map (fun c -> let hash = Hashtbl.copy c.conf in
|
---|
| 382 | mutate_conf hash;
|
---|
| 383 | { score = evaluate_hash hash;
|
---|
| 384 | conf = hash}) population in
|
---|
| 385 | Array.append population mutants in
|
---|
| 386 | let evaluate population =
|
---|
| 387 | Array.iter (fun c -> c.score <- evaluate_hash c.conf) population;
|
---|
| 388 | population in
|
---|
[1999] | 389 | let select p =
|
---|
| 390 | Array.sort (fun a b -> compare b.score a.score) p;
|
---|
| 391 | (*shuffle p;*)
|
---|
| 392 | Array.sub p 0 population_size in
|
---|
[1990] | 393 | let best = evolutionary_algorithm initialize recombine mutate evaluate select in
|
---|
| 394 | print_conf best.conf;;
|
---|
| 395 |
|
---|
[1973] | 396 | main
|
---|