From 32fadd634d259595a961556fd5e6349895569dec Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH] refactor(tools): the grading tool in smaller functions As Sonar asks, and a regular expression with no backtracking. The results stay the same byte for byte on baltyk_skm1, baltyk_cargo, td and lk357. --- tools/track_uneven_grade.py | 348 +++++++++++++++++++++--------------- 1 file changed, 208 insertions(+), 140 deletions(-) diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index d643f94f8..1994559d0 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -43,7 +43,7 @@ STRAY_PER_MM, DIP_SHARE, DIP_REACH = 0.00145, 0.4, 3.0 GOLDEN = math.pi * (3.0 - math.sqrt(5.0)) UP_WAVES = [(2 * math.pi / length * math.cos(GOLDEN * i), 2 * math.pi / length * math.sin(GOLDEN * i), GOLDEN * (i + 1), share) for i, (length, share) in enumerate(zip((8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3), (.14, .19, .22, .22, .23, .23, .23, .23)))] TOKEN = re.compile(r"//[^\n]*|\S+") -NUMBER = re.compile(r"^[-+]?\d*\.?\d+([eE][-+]?\d+)?$") +NUMBER = re.compile(r"^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$") def limit(speed): @@ -79,38 +79,9 @@ class Scenery: def read(self, path, offset=(0.0, 0.0, 0.0)): path = path.resolve() - if path in self.seen: - return - self.seen.add(path) - text = path.read_bytes().decode("latin-1") - self.texts[path] = text - matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] - words = [m.group(0) for m in matches] - lower = [w.lower() for w in words] - origins = [offset] - i = 0 - while i < len(words): - word = lower[i] - if word == "include" and i + 1 < len(words): - end = i + 1 - while end < len(words) and lower[end] != "end": - end += 1 - self.include(words[i + 1], words[i + 2:end], origins[-1]) - i = end - elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]): - origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4]))) - i += 3 - elif word == "endorigin" and len(origins) > 1: - origins.pop() - elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]): - self.notes["rotate"] += 1 - elif word == "node" and i + 4 < len(words): - kind = lower[i + 4] - if kind == "track": - i = self.track(path, matches, words, lower, i, origins[-1]) - elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]): - self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2]) - i += 1 + if path not in self.seen: + self.seen.add(path) + FileReader(self, path, offset).run() def include(self, name, params, origin): path = self.root / name.replace("\\", "/") @@ -118,21 +89,90 @@ class Scenery: numbers = [float(w) for w in params if NUMBER.match(w)] if GRASS.search(name.lower()) and len(numbers) >= 3: self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) - return - if not path.is_file(): + elif not path.is_file(): self.notes["brak pliku " + name] += 1 - return - if params: + elif not params: + self.read(path, origin) + elif path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): # the same file can come many times with other parameters: its tracks can't be told apart in one copy - if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): - self.notes["pliki z parametrami, tory pominiete"] += 1 - return - self.read(path, origin) + self.notes["pliki z parametrami, tory pominiete"] += 1 def plant(self, x, z): self.plants[(round(x / 10), round(z / 10))].append((x, z)) - def track(self, path, matches, words, lower, i, origin): + +class FileReader: + """Goes through the words of one file. Each handler takes the place of its word and gives the place of the last word it used.""" + + def __init__(self, scenery, path, offset): + self.scenery, self.path = scenery, path + text = path.read_bytes().decode("latin-1") + scenery.texts[path] = text + self.matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] + self.words = [m.group(0) for m in self.matches] + self.lower = [w.lower() for w in self.words] + self.origins = [offset] + self.handlers = {"include": self.include, "origin": self.origin, "endorigin": self.endorigin, "rotate": self.rotate, "node": self.node} + + def run(self): + i = 0 + while i < len(self.words): + handler = self.handlers.get(self.lower[i]) + i = (handler(i) if handler else i) + 1 + + def include(self, i): + if i + 1 >= len(self.words): + return i + end = i + 1 + while end < len(self.words) and self.lower[end] != "end": + end += 1 + self.scenery.include(self.words[i + 1], self.words[i + 2:end], self.origins[-1]) + return end + + def origin(self, i): + values = self.words[i + 1:i + 4] + if len(values) < 3 or not all(NUMBER.match(w) for w in values): + return i + self.origins.append(tuple(o + float(w) for o, w in zip(self.origins[-1], values))) + return i + 3 + + def endorigin(self, i): + if len(self.origins) > 1: + self.origins.pop() + return i + + def rotate(self, i): + if i + 3 < len(self.words) and any(w.strip("0.-") for w in self.words[i + 1:i + 4]): + self.scenery.notes["rotate"] += 1 + return i + + def node(self, i): + if i + 4 >= len(self.words): + return i + kind = self.lower[i + 4] + if kind == "track": + return self.track(i) + if kind == "model" and i + 9 < len(self.words) and GRASS.search(self.lower[i + 9]) and all(NUMBER.match(w) for w in self.words[i + 5:i + 8]): + self.scenery.plant(float(self.words[i + 5]) + self.origins[-1][0], float(self.words[i + 7]) + self.origins[-1][2]) + return i + + def paths_from(self, cursor, count): + paths = [] + origin = self.origins[-1] + for _ in range(count): + values = self.words[cursor:cursor + 15] + if len(values) < 15 or not all(NUMBER.match(v) for v in values): + return None + n = [float(v) for v in values] + part = {"p1": [n[j] + origin[j] for j in range(3)], "c1": n[4:7], "c2": n[7:10], "p2": [n[10 + j] + origin[j] for j in range(3)], "radius": abs(n[14])} + part["line"] = line_of(part) + part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) + paths.append(part) + cursor += 15 + return paths + + def track(self, i): + words, lower = self.words, self.lower at = i + 4 end = at while end < len(words) and lower[end] != "endtrack": @@ -142,33 +182,30 @@ class Scenery: kind = lower[at + 1] if kind not in ("normal", "switch", "road"): return end - cursor = at + 10 - rail = ballast = None - if lower[at + 9] == "vis": - rail, ballast = lower[cursor], lower[cursor + 2] - cursor += 6 - paths = [] - for _ in range(2 if kind == "switch" else 1): - values = words[cursor:cursor + 15] - if len(values) < 15 or not all(NUMBER.match(v) for v in values): - self.notes["tory z parametrami, pominiete"] += 1 - return end - n = [float(v) for v in values] - part = dict(p1=[n[j] + origin[j] for j in range(3)], c1=n[4:7], c2=n[7:10], p2=[n[10 + j] + origin[j] for j in range(3)], radius=abs(n[14])) - part["line"] = line_of(part) - part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) - paths.append(part) - cursor += 15 - if kind == "road": - self.roads.append(paths[0]) - return end - block = lower[at:end] - taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip")) - velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0 - sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0 - self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken)) + visible = lower[at + 9] == "vis" + paths = self.paths_from(at + (16 if visible else 10), 2 if kind == "switch" else 1) + if paths is None: + self.scenery.notes["tory z parametrami, pominiete"] += 1 + elif kind == "road": + self.scenery.roads.append(paths[0]) + else: + self.add_track(i, at, end, kind, visible, paths) return end + def add_track(self, i, at, end, kind, visible, paths): + words = self.words + block = self.lower[at:end] + velocity = 0.0 + if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]): + velocity = float(words[at + block.index("velocity") + 1]) + self.scenery.tracks.append({ + "name": words[i + 3], "kind": kind, + "rail": self.lower[at + 10] if visible else None, "ballast": self.lower[at + 12] if visible else None, + "sound": float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0, + "paths": paths, "speed": velocity if velocity > 0 else None, "source": "wpis" if velocity > 0 else None, + "file": self.path, "offset": self.matches[end].start(), + "taken": any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip"))}) + def bezier(part, t): a = [part["p1"][i] + part["c1"][i] for i in range(3)] @@ -220,50 +257,67 @@ class Joints: return found +def plain_neighbours(t, joints): + return [other for key in ("p1", "p2") for other, _, _ in joints.at(t["paths"][0][key]) if other is not t and other["kind"] == "normal"] + + +def plain_past_switches(t, point, joints): + # the plain tracks reached from a point of a track along the straight paths of switches + points, seen, found = [point], set(), [] + for _ in range(6): + onward = [] + for reached in points: + for other, part, side in joints.at(reached): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] != "switch": + found.append(other) + elif other["paths"].index(part) != other["bent"]: + onward.append(part["p2" if side == "p1" else "p1"]) + points = onward + return found + + +def short_pieces_past_switches(t, joints): + return [o for key in ("p1", "p2") for o in plain_past_switches(t, t["paths"][0][key], joints) if o["paths"][0]["length"] < PIECE] + + +def spread_speed(tracks, joints, reached): + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for other in reached(t, joints): + if other["speed"] is None: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + + def settle_speeds(tracks, joints): # from the nearest track on the stretch between switches, a short piece also along the straight path of a switch - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - for other, _, _ in joints.at(t["paths"][0][key]): - if other is not t and other["kind"] == "normal" and other["speed"] is None: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - points, seen = [t["paths"][0][key]], set() - for _ in range(6): - onward = [] - for point in points: - for other, part, side in joints.at(point): - if other is t or id(part) in seen: - continue - seen.add(id(part)) - if other["kind"] == "switch": - if other["paths"].index(part) != other["bent"]: - onward.append(part["p2" if side == "p1" else "p1"]) - elif other["speed"] is None and other["paths"][0]["length"] < PIECE: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - points = onward + spread_speed(tracks, joints, plain_neighbours) + spread_speed(tracks, joints, short_pieces_past_switches) for t in tracks: if t["speed"] is None: t["speed"], t["source"] = NO_SPEED, "brak" +def plain_ends(tracks): + for t in tracks: + if t["kind"] == "normal": + for part in t["paths"]: + yield part["p1"] + yield part["p2"] + + def distances(tracks, joints, station): best, heap = {}, [] - for t in tracks: - for part in t["paths"]: - for key in ("p1", "p2"): - gap = math.dist((part[key][0], part[key][2]), station) - node = (round(part[key][0], 1), round(part[key][2], 1)) - if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf): - best[node] = gap - heapq.heappush(heap, (gap, node, part[key])) + for point in plain_ends(tracks): + gap = math.dist((point[0], point[2]), station) + node = (round(point[0], 1), round(point[2], 1)) + if gap < 300 and gap < best.get(node, math.inf): + best[node] = gap + heapq.heappush(heap, (gap, node, point)) while heap: distance, node, point = heapq.heappop(heap) if distance > best.get(node, math.inf): @@ -293,30 +347,34 @@ def grass_on(part, plants): return len(near) * 100.0 / max(part["length"], 5.0) +def cells_under(a, b): + for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): + for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): + yield x, z + + def road_cells(roads): cells = collections.defaultdict(list) for road in roads: for a, b in zip(road["line"], road["line"][1:]): - for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): - for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): - cells[(x, z)].append((a, b)) + for cell in cells_under(a, b): + cells[cell].append((a, b)) return cells +def cross_in_plan(a, b, c, d): + rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] + k = rx * sz - rz * sx + if abs(k) < 1e-9: + return False + t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k + u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k + return 0 <= t <= 1 and 0 <= u <= 1 + + def hits_road(part, cells): - for a, b in zip(part["line"], part["line"][1:]): - for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): - for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): - for c, d in cells.get((x, z), ()): - rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] - k = rx * sz - rz * sx - if abs(k) < 1e-9: - continue - t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k - u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k - if 0 <= t <= 1 and 0 <= u <= 1: - return True - return False + line = part["line"] + return any(cross_in_plan(a, b, c, d) for a, b in zip(line, line[1:]) for cell in cells_under(a, b) for c, d in cells.get(cell, ())) def heights_per_mm(t): @@ -423,17 +481,19 @@ def fit_under_pantograph(normal): return low +def joint_curve_changes(t, part, key, joints): + own = end_curve(part, key) + for other, theirs, side in joints.at(part[key]): + if other is not t and other["kind"] == "normal": + their = end_curve(theirs, side) + if abs(own - (-their if side == key else their)) > CURVE_STEP: + return True + return False + + def curve_changes(t, joints): part = t["paths"][0] - if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP: - return True - for key in ("p1", "p2"): - for other, theirs, side in joints.at(part[key]): - if other is not t and other["kind"] == "normal": - their = end_curve(theirs, side) - if abs(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: - return True - return False + return abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP or any(joint_curve_changes(t, part, key, joints) for key in ("p1", "p2")) def move_near_limit(normal, joints, share): @@ -458,24 +518,32 @@ def move_near_limit(normal, joints, share): return chosen +def value_from_plain(t, index, part, joints, product): + found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] + if not found: + return None + speed = max(o["speed"] for o in found) + if index == t["bent"]: + speed = min(speed, DIVERGING) + return min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + + +def value_from_switches(t, part, joints): + found = [] + for key in ("p1", "p2"): + for other, theirs, _ in joints.at(part[key]): + if other["kind"] == "switch" and other is not t: + found += [other["values"][i] for i, q in enumerate(other["paths"]) if q is theirs and other["values"][i] is not None] + return max(found) if found else None + + def grade_switches(switches, joints, product): # each path by the plain tracks it leads to, the worst of them; those only among switches from their neighbours for t in switches: - t["values"] = [None] * len(t["paths"]) - for index, part in enumerate(t["paths"]): - found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] - if found: - speed = max(o["speed"] for o in found) - if index == t["bent"]: - speed = min(speed, DIVERGING) - t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + t["values"] = [value_from_plain(t, index, part, joints, product) for index, part in enumerate(t["paths"])] for _ in range(5): for t in switches: - for index, part in enumerate(t["paths"]): - if t["values"][index] is None: - found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None] - if found: - t["values"][index] = max(found) + t["values"] = [value_from_switches(t, part, joints) if value is None else value for value, part in zip(t["values"], t["paths"])] for t in switches: t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]]