From 287ea0cec6733ecc5ef7fa02ac24bb21818957d6 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Wed, 13 May 2026 13:06:33 +1200 Subject: [PATCH 1/5] Remove properties.json and vagus.svg from tests resources directory. --- tests/resources/properties.json | 297 -------------------------------- tests/resources/vagus.svg | 270 ----------------------------- 2 files changed, 567 deletions(-) delete mode 100644 tests/resources/properties.json delete mode 100644 tests/resources/vagus.svg diff --git a/tests/resources/properties.json b/tests/resources/properties.json deleted file mode 100644 index 79e3f35..0000000 --- a/tests/resources/properties.json +++ /dev/null @@ -1,297 +0,0 @@ -{ - "features": { - "brain_region": { - "name": "Brain" - }, - "marker_10001": { - "colour": "orange", - "label": "Label 'right level of superior border of jugular foramen on the vagus nerve' for term 'UBERON:9947841' was not found during lookup.", - "models": "UBERON:9947841" - }, - "marker_10002": { - "colour": "orange", - "label": "Label 'right level of inferior border of jugular foramen on the vagus nerve' for term 'UBERON:9936083' was not found during lookup.", - "models": "UBERON:9936083" - }, - "marker_10003": { - "colour": "orange", - "label": "Label 'right level of angle of the mandible on the vagus nerve' for term 'UBERON:9986030' was not found during lookup.", - "models": "UBERON:9986030" - }, - "marker_10004": { - "colour": "orange", - "label": "Label 'right level of carotid bifurcation on the vagus nerve' for term 'UBERON:9936336' was not found during lookup.", - "models": "UBERON:9936336" - }, - "marker_10005": { - "colour": "orange", - "label": "Label 'right level of laryngeal prominence on the vagus nerve' for term 'UBERON:9931228' was not found during lookup.", - "models": "UBERON:9931228" - }, - "marker_10006": { - "colour": "orange", - "label": "Label 'right level of superior border of the clavicle on the vagus nerve' for term 'UBERON:9968398' was not found during lookup.", - "models": "UBERON:9968398" - }, - "marker_10007": { - "colour": "orange", - "label": "Label 'right level of jugular notch on the vagus nerve' for term 'UBERON:9967307' was not found during lookup.", - "models": "UBERON:9967307" - }, - "marker_10008": { - "colour": "orange", - "label": "Label 'right level of sternal angle on the vagus nerve' for term 'UBERON:9987408' was not found during lookup.", - "models": "UBERON:9987408" - }, - "nerve_feature_01": { - "label": "Label 'Right hypoglossal nerve' for term 'ILX:987654' was not found during lookup." - }, - "nerve_feature_02": { - "label": "Label 'Right spinal accessory nerve' for term 'ILX:345678' was not found during lookup." - }, - "nerve_feature_03": { - "label": "Label 'cardiovascular/cardiopulmonary branch of right cervical vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_04": { - "label": "Label 'cardiovascular/cardiopulmonary branch of right thoracic vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_08": { - "label": "Label 'right A branch of cervical vagus nerve to sympathetic chain, cardiovascular, and pulmonary structures' for term '' was not found during lookup." - }, - "nerve_feature_09": { - "label": "Label 'right A branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_10": { - "label": "Label 'right A cardiovascular branch of cervical vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_11": { - "label": "Label 'right A cardiovascular branch of cranial nerve bundle' for term '' was not found during lookup." - }, - "nerve_feature_12": { - "label": "Label 'right A lateral pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_13": { - "label": "Label 'right A medial pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_14": { - "label": "Label 'right A thoracic cardiopulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_15": { - "label": "Label 'right B branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_16": { - "label": "Label 'right B cardiovascular branch of cranial nerve bundle' for term '' was not found during lookup." - }, - "nerve_feature_17": { - "label": "Label 'right B lateral pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_18": { - "label": "Label 'right B medial pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_19": { - "label": "Label 'right B thoracic cardiopulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_20": { - "label": "Label 'right C branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_21": { - "label": "Label 'right C lateral pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_22": { - "label": "Label 'right C medial pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_23": { - "label": "Label 'right C thoracic cardiopulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_24": { - "label": "Label 'right D branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_25": { - "label": "Label 'right D lateral pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_26": { - "label": "Label 'right D medial pulmonary branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_27": { - "label": "Label 'right E branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_28": { - "label": "Label 'right F branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_29": { - "label": "Label 'right G branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_30": { - "label": "Label 'right H branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_31": { - "label": "Label 'right I branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_32": { - "label": "Label 'right J branch of thoracic vagus nerve to esophageal plexus' for term '' was not found during lookup." - }, - "nerve_feature_33": { - "label": "Label 'right branch between cranial nerve bundle and superior cervical ganglion' for term '' was not found during lookup." - }, - "nerve_feature_35": { - "label": "Label 'right branch of cervical vagus nerve to sympathetic chain, cardiovascular, and pulmonary structures' for term '' was not found during lookup." - }, - "nerve_feature_36": { - "label": "Label 'right cardiovascular branch of cervical vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_38": { - "label": "Label 'right cervical vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_39": { - "label": "Label 'right cranial nerve bundle to cervical spinal nerve' for term '' was not found during lookup." - }, - "nerve_feature_42": { - "label": "Label 'right pharyngeal branch of cranial nerve bundle' for term '' was not found during lookup." - }, - "nerve_feature_43": { - "label": "Label 'right recurrent laryngeal nerve' for term '' was not found during lookup." - }, - "nerve_feature_44": { - "label": "Label 'right superior laryngeal nerve' for term '' was not found during lookup." - }, - "nerve_feature_46": { - "label": "Label 'right thoracic vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_47": { - "label": "Label 'right unknown branch of vagus nerve' for term '' was not found during lookup." - }, - "nerve_feature_48": { - "label": "Label 'right vagus nerve' for term '' was not found during lookup." - } - }, - "networks": [ - { - "centrelines": [ - { - "id": "nerve_feature_38" - }, - { - "id": "nerve_feature_48" - }, - { - "id": "nerve_feature_46" - }, - { - "id": "nerve_feature_01", - "models": "ILX:987654" - }, - { - "id": "nerve_feature_02", - "models": "ILX:345678" - }, - { - "id": "nerve_feature_03" - }, - { - "id": "nerve_feature_04" - }, - { - "id": "nerve_feature_08" - }, - { - "id": "nerve_feature_35" - }, - { - "id": "nerve_feature_09" - }, - { - "id": "nerve_feature_10" - }, - { - "id": "nerve_feature_36" - }, - { - "id": "nerve_feature_11" - }, - { - "id": "nerve_feature_12" - }, - { - "id": "nerve_feature_13" - }, - { - "id": "nerve_feature_14" - }, - { - "id": "nerve_feature_15" - }, - { - "id": "nerve_feature_16" - }, - { - "id": "nerve_feature_17" - }, - { - "id": "nerve_feature_18" - }, - { - "id": "nerve_feature_19" - }, - { - "id": "nerve_feature_20" - }, - { - "id": "nerve_feature_21" - }, - { - "id": "nerve_feature_22" - }, - { - "id": "nerve_feature_23" - }, - { - "id": "nerve_feature_24" - }, - { - "id": "nerve_feature_25" - }, - { - "id": "nerve_feature_26" - }, - { - "id": "nerve_feature_27" - }, - { - "id": "nerve_feature_28" - }, - { - "id": "nerve_feature_29" - }, - { - "id": "nerve_feature_30" - }, - { - "id": "nerve_feature_31" - }, - { - "id": "nerve_feature_32" - }, - { - "id": "nerve_feature_33" - }, - { - "id": "nerve_feature_39" - }, - { - "id": "nerve_feature_42" - }, - { - "id": "nerve_feature_43" - }, - { - "id": "nerve_feature_44" - }, - { - "id": "nerve_feature_47" - } - ], - "id": "vagus", - "type": "nerve" - } - ] -} \ No newline at end of file diff --git a/tests/resources/vagus.svg b/tests/resources/vagus.svg deleted file mode 100644 index ac94166..0000000 --- a/tests/resources/vagus.svg +++ /dev/null @@ -1,270 +0,0 @@ - - - - .id(brain_region) - - - .id(nerve_feature_38) - - - .id(nerve_feature_38_outline) - - - .id(nerve_feature_48) - - - .id(nerve_feature_48_outline) - - - .id(nerve_feature_46) - - - .id(nerve_feature_46_outline) - - - .id(nerve_feature_01) - - - .id(nerve_feature_01_outline) - - - .id(nerve_feature_02) - - - .id(nerve_feature_02_outline) - - - .id(nerve_feature_03) - - - .id(nerve_feature_03_outline) - - - .id(nerve_feature_04) - - - .id(nerve_feature_04_outline) - - - .id(nerve_feature_08) - - - .id(nerve_feature_08_outline) - - - .id(nerve_feature_35) - - - .id(nerve_feature_35_outline) - - - .id(nerve_feature_09) - - - .id(nerve_feature_09_outline) - - - .id(nerve_feature_10) - - - .id(nerve_feature_10_outline) - - - .id(nerve_feature_36) - - - .id(nerve_feature_36_outline) - - - .id(nerve_feature_11) - - - .id(nerve_feature_11_outline) - - - .id(nerve_feature_12) - - - .id(nerve_feature_12_outline) - - - .id(nerve_feature_13) - - - .id(nerve_feature_13_outline) - - - .id(nerve_feature_14) - - - .id(nerve_feature_14_outline) - - - .id(nerve_feature_15) - - - .id(nerve_feature_15_outline) - - - .id(nerve_feature_16) - - - .id(nerve_feature_16_outline) - - - .id(nerve_feature_17) - - - .id(nerve_feature_17_outline) - - - .id(nerve_feature_18) - - - .id(nerve_feature_18_outline) - - - .id(nerve_feature_19) - - - .id(nerve_feature_19_outline) - - - .id(nerve_feature_20) - - - .id(nerve_feature_20_outline) - - - .id(nerve_feature_21) - - - .id(nerve_feature_21_outline) - - - .id(nerve_feature_22) - - - .id(nerve_feature_22_outline) - - - .id(nerve_feature_23) - - - .id(nerve_feature_23_outline) - - - .id(nerve_feature_24) - - - .id(nerve_feature_24_outline) - - - .id(nerve_feature_25) - - - .id(nerve_feature_25_outline) - - - .id(nerve_feature_26) - - - .id(nerve_feature_26_outline) - - - .id(nerve_feature_27) - - - .id(nerve_feature_27_outline) - - - .id(nerve_feature_28) - - - .id(nerve_feature_28_outline) - - - .id(nerve_feature_29) - - - .id(nerve_feature_29_outline) - - - .id(nerve_feature_30) - - - .id(nerve_feature_30_outline) - - - .id(nerve_feature_31) - - - .id(nerve_feature_31_outline) - - - .id(nerve_feature_32) - - - .id(nerve_feature_32_outline) - - - .id(nerve_feature_33) - - - .id(nerve_feature_33_outline) - - - .id(nerve_feature_39) - - - .id(nerve_feature_39_outline) - - - .id(nerve_feature_42) - - - .id(nerve_feature_42_outline) - - - .id(nerve_feature_43) - - - .id(nerve_feature_43_outline) - - - .id(nerve_feature_44) - - - .id(nerve_feature_44_outline) - - - .id(nerve_feature_47) - - - .id(nerve_feature_47_outline) - - - .id(marker_10001) - - - .id(marker_10002) - - - .id(marker_10003) - - - .id(marker_10004) - - - .id(marker_10005) - - - .id(marker_10006) - - - .id(marker_10007) - - - .id(marker_10008) - - From ed78e36ef95d4486ab2b7f57a1fc513292c41a35 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Wed, 13 May 2026 13:07:12 +1200 Subject: [PATCH 2/5] Use a 55 ULP window to detect if points are the same in determining connected segments. --- src/cmlibs/exporter/flatmapsvg.py | 104 ++++++++++++++---------------- 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/src/cmlibs/exporter/flatmapsvg.py b/src/cmlibs/exporter/flatmapsvg.py index 3bc67ca..d84d573 100644 --- a/src/cmlibs/exporter/flatmapsvg.py +++ b/src/cmlibs/exporter/flatmapsvg.py @@ -5,11 +5,10 @@ import csv import json import logging - import math import os import random -from decimal import Decimal +import struct from packaging.version import Version from svgpathtools import svg2paths @@ -678,81 +677,78 @@ def union(self, i, j): return root_j return root_i - def __repr__(self): - return f"{self.parent}" +def get_bit_key(val): + """Converts a float to its 64-bit integer representation.""" + return struct.unpack('>Q', struct.pack('>d', val))[0] -def _count_significant_figs(num_str): - return len(Decimal(num_str).normalize().as_tuple().digits) +def get_ulp_neighbours_2d(x, y, window=9): + """Generates all bit-level neighbour keys for a coordinate.""" + x_bits = get_bit_key(x) + y_bits = get_bit_key(y) -def _create_key(pt, tolerance=1e8): - return tuple(int(p * tolerance) for p in pt) + # Generate a grid of keys surrounding the point + return [ + (x_bits + i, y_bits + j) + for i in range(-window, window + 1) + for j in range(-window, window + 1) + ] -def _calculate_tolerance(numbers): - min_sig_figs = math.inf - max_sig_digit = -math.inf - for n in numbers: - min_sig_figs = min([min_sig_figs, _count_significant_figs(f"{n}")]) - # max_sig_digit = max([max_sig_digit, float(f'{float(f"{n:.1g}"):g}')]) - abs_n = math.fabs(n) - max_sig_digit = max([max_sig_digit, math.ceil(math.log10(abs_n if abs_n > 1e-08 else 1.0))]) +def get_ulp_neighbours(x, window=9): + """Generates all bit-level neighbour keys for a coordinate.""" + x_bits = get_bit_key(x) - min_sig_figs = max(14, min_sig_figs) - tolerance_power = min_sig_figs - max_sig_digit - 2 - return 10 ** (-tolerance_power if tolerance_power > 0 else -8) + # Generate a grid of keys surrounding the point + return [ + x_bits + i + for i in range(-window, window + 1) + ] def _connected_segments(curve): - # Determine a tolerance for the curve to use in defining keys - numbers = [] - for c in curve: - for i in [0, 3]: - for j in [0, 1]: - numbers.append(c[i][j]) + curve_size = len(curve) + uf = UnionFind(curve_size) - key_tolerance = 1 / _calculate_tolerance(numbers) + # Create a dictionary to map (x_bits, y_bits) -> segment_index. + start_point_lookup = {} + window = 55 - begin_hash = {} for index, c in enumerate(curve): - key = _create_key(c[0], key_tolerance) - if key in begin_hash: - logger.warning(f"Problem repeated key found while trying to connect segments! {index} - {c}") - begin_hash[key] = index + start_pt = c[0] # [x, y] - curve_size = len(curve) - uf = UnionFind(len(curve)) + neighbour_keys = get_ulp_neighbours_2d(start_pt[0], start_pt[1], window) + for key in neighbour_keys: + # Note: if multiple segments start at the same ULP-neighbour space, + # the last one wins. + start_point_lookup[key] = index + + # Iterate through the ENDS of the segments to find connections. for index, c in enumerate(curve): - y_cur = _create_key(c[3], key_tolerance) - if y_cur in begin_hash: - uf.union(begin_hash[y_cur], index) + end_pt = c[3] # [x, y] + end_key = (get_bit_key(end_pt[0]), get_bit_key(end_pt[1])) + + if end_key in start_point_lookup: + matched_segment_index = start_point_lookup[end_key] + uf.union(matched_segment_index, index) + # Group the indices into connected components sets = {} for i in range(curve_size): root = uf.find(i) if root not in sets: sets[root] = [] - sets[root].append(i) - segments = [] - for s in sets: - seg = [curve[s]] - key = _create_key(curve[s][3], key_tolerance) - while key in begin_hash: - s = begin_hash[key] - seg.append(curve[s]) - old_key = key - key = _create_key(curve[s][3], key_tolerance) - if old_key == key: - logger.warning("Breaking out of loop in determining segments.") - break - - segments.append(seg) - - return segments + # Reconstruct the segments in order. + # Note: This simple reconstruction assumes linear paths. + final_segments = [] + for root, indices in sets.items(): + actual_curves = [curve[i] for i in indices] + final_segments.append(actual_curves) + return final_segments def _collect_curves_into_segments_old(bezier_data): collection_of_paths = {} @@ -989,7 +985,7 @@ def _calculate_cervical_thoracic_boundaries(markers): def _define_background_regions(boundaries, view_box): # Because we calculate eight cervical vertebrae to get to the superior # surface of C1 (Atlas), we need the eighth cervical boundary to find - # the posterior surface of C7. + # the inferior surface of C7. boundary = boundaries[0][7] min_x = view_box[0] min_y = view_box[1] From bcf1ca65dab0bd981e3057660f073598459c851c Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Wed, 13 May 2026 13:52:33 +1200 Subject: [PATCH 3/5] Reconstruct curves in connectecd order. --- src/cmlibs/exporter/flatmapsvg.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/cmlibs/exporter/flatmapsvg.py b/src/cmlibs/exporter/flatmapsvg.py index d84d573..2cbb46d 100644 --- a/src/cmlibs/exporter/flatmapsvg.py +++ b/src/cmlibs/exporter/flatmapsvg.py @@ -733,7 +733,7 @@ def _connected_segments(curve): matched_segment_index = start_point_lookup[end_key] uf.union(matched_segment_index, index) - # Group the indices into connected components + # Group the indices into connected components. sets = {} for i in range(curve_size): root = uf.find(i) @@ -741,12 +741,21 @@ def _connected_segments(curve): sets[root] = [] sets[root].append(i) - # Reconstruct the segments in order. - # Note: This simple reconstruction assumes linear paths. + # Reconstruct the segments in connected order. final_segments = [] for root, indices in sets.items(): - actual_curves = [curve[i] for i in indices] - final_segments.append(actual_curves) + ordered = [] + current = root + visited = set() + + while current is not None and current not in visited: + ordered.append(curve[current]) + visited.add(current) + # Look for the segment that starts where this one ends. + end_pt = curve[current][3] + end_key = (get_bit_key(end_pt[0]), get_bit_key(end_pt[1])) + current = start_point_lookup.get(end_key) + final_segments.append(ordered) return final_segments From 241826233bcf84776c7351d5f79f1ae9474d2db4 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Wed, 13 May 2026 13:59:16 +1200 Subject: [PATCH 4/5] Use checkouts version 6 in run_tests workflow. --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 82fb066..cd2a846 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -16,7 +16,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: From a13c3a7cd2c15d7b00bc98fc54d1a0d4d14f0b3d Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Wed, 13 May 2026 14:01:45 +1200 Subject: [PATCH 5/5] Use actions/setup-python version 6 in run_tests workflow. --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index cd2a846..89c2faf 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }}