Skip to content

Commit 7f90369

Browse files
yuminguwclaude
andcommitted
fix(tacs): correct TACS-2/TACS-3 classification and viewer orientation lines
Two bugs fixed in _launch_tacs_viewer() and _tacs_hierarchy_integration(): 1. Orientation lines used [row-dy, row+dy] instead of [row+dy, row-dy], mirroring all fiber angles relative to the validated draw_curvs() reference. 2. TACS classification applied an incorrect 90 deg complement conversion to nearest_relative_boundary_angle before calling classify_fiber_tacs(). compute_boundary_tangent_angle() returns atan2(dcol, drow) % 180 -- a 90 deg rotated convention vs the fiber angle (0=horizontal). This offset causes nearest_relative_boundary_angle to already equal angle_to_tangent directly (0=parallel, 90=perpendicular). The 90 deg subtraction was inverting TACS-2 and TACS-3 labels. Documentation updated: - boundary_tif_utils._compute_fiber_boundary_relative_angle docstring corrected - geometry_utils.compute_boundary_tangent_angle warning added re: axis convention - REFACTORING_GUIDE.md note added for nearest_relative_boundary_angle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6a201b4 commit 7f90369

4 files changed

Lines changed: 45 additions & 31 deletions

File tree

src/tme_quant/REFACTORING_GUIDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ Note: `angle_to_boundary_edge` in pycurvelets equals `90° − angle_to_boundary
214214
in tme_quant TACS convention (pycurvelets returns the complement angle). Always apply
215215
the conversion; never silently carry over the pycurvelets value with the tme_quant name.
216216

217+
Note: `nearest_relative_boundary_angle` (output of `extract_tif_boundary` /
218+
`_compute_fiber_boundary_relative_angle`) equals `angle_to_boundary_tangent` **directly**
219+
— no conversion needed. `compute_boundary_tangent_angle` uses `atan2(Δcol, Δrow)`,
220+
which is 90° offset from the fiber-angle convention; this offset cancels the expected
221+
complement, so the raw column value is already the TACS-ready angle_to_tangent.
222+
217223
---
218224

219225
## 7. MATLAB Rounding and Numerical Parity

src/tme_quant/examples/example_curvealign_curvelets_mode_pipeline.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,14 @@ def _tacs_hierarchy_integration(
305305
nb_dist = float(raw_dist)
306306
if raw_epict is not None:
307307
in_epictr = bool(raw_epict)
308-
# nearest_relative_boundary_angle is in pycurvelets convention:
309-
# value = arcsin(circ_r([2*fiber_deg, 2*tangent_deg]))
310-
# = 90° − |fiber_angle − boundary_tangent_angle|
311-
# = 90° − angle_to_boundary_tangent
312-
# classify_fiber_tacs() expects angle_to_boundary_tangent, so convert:
308+
# nearest_relative_boundary_angle is used directly as angle_to_tangent.
309+
# compute_boundary_tangent_angle() uses atan(Δcol/Δrow) — a 90°-rotated
310+
# convention vs the fiber angle (0°=horizontal). This offset inverts the
311+
# circ_r result so that nearest_relative_boundary_angle is already the
312+
# angle to the boundary tangent (0°=parallel, 90°=perpendicular), NOT its
313+
# complement. No conversion needed.
313314
if raw_angle is not None and not pd.isna(raw_angle):
314-
angle_to_tangent = 90.0 - float(raw_angle)
315+
angle_to_tangent = float(raw_angle)
315316
if (raw_bpt_r is not None and not pd.isna(raw_bpt_r)
316317
and raw_bpt_c is not None and not pd.isna(raw_bpt_c)):
317318
bdry_pt = np.array([float(raw_bpt_r), float(raw_bpt_c)]) # (row, col)
@@ -386,20 +387,17 @@ def _tacs_hierarchy_integration(
386387
print(" No boundary — TACS classification skipped.")
387388

388389
# ── Angle diagnostic (verify convention) ─────────────────────────────────
389-
# nearest_relative_boundary_angle: high value (→90°) = fiber parallel to boundary tangent
390-
# low value (→ 0°) = fiber perpendicular (invasive)
391-
# angle_to_tangent = 90 - nearest_relative_boundary_angle
392-
# high angle_to_tangent (60-90°) → TACS-3 (perpendicular)
393-
# low angle_to_tangent ( 0-30°) → TACS-2 (parallel)
390+
# nearest_relative_boundary_angle = angle_to_tangent directly (no conversion).
391+
# high value (60-90°) → TACS-3 (perpendicular / invasive)
392+
# low value ( 0-30°) → TACS-2 (parallel)
394393
if has_boundary:
395394
print("\n Angle diagnostic — 5 sample fibers in TACS zone:")
396-
print(f" {'nearest_relative_boundary_angle':>35} {'angle_to_tangent':>17} TACS")
395+
print(f" {'nearest_relative_boundary_angle (=angle_to_tangent)':>52} TACS")
397396
n_shown = 0
398397
for i, (_, frow) in enumerate(fs.iterrows()):
399398
f = fiber_objects[i]
400399
if f.relative_angle_to_boundary_tangent is not None and f.nearest_boundary_distance is not None:
401-
raw = 90.0 - f.relative_angle_to_boundary_tangent # recover raw stored value
402-
print(f" {raw:>35.1f} {f.relative_angle_to_boundary_tangent:>17.1f} {f.tacs_type}")
400+
print(f" {f.relative_angle_to_boundary_tangent:>52.1f} {f.tacs_type}")
403401
n_shown += 1
404402
if n_shown >= 5:
405403
break
@@ -482,8 +480,8 @@ def _launch_tacs_viewer(
482480

483481
# Pre-draw each TACS group; accumulate artists per type key
484482
groups: dict[str | None, list] = {k: [] for k in TACS_STYLE}
485-
# Association lines (fiber center → nearest boundary point), hidden by default
486-
assoc_lines: list = []
483+
# Association lines per TACS key — hidden by default
484+
assoc_by_key: dict[str | None, list] = {k: [] for k in TACS_STYLE}
487485

488486
for fobj in fiber_objects:
489487
key = fobj.tacs_type if fobj.tacs_type in groups else None
@@ -495,7 +493,7 @@ def _launch_tacs_viewer(
495493
dx = line_length * np.cos(angle_rad)
496494
dy = line_length * np.sin(angle_rad)
497495
line, = ax.plot(
498-
[col - dx, col + dx], [row - dy, row + dy],
496+
[col - dx, col + dx], [row + dy, row - dy],
499497
color=color, lw=0.8, alpha=0.85,
500498
)
501499
dot, = ax.plot(col, row, ".", color=color, ms=2.5)
@@ -510,7 +508,7 @@ def _launch_tacs_viewer(
510508
[col, bdry_col], [row, bdry_row],
511509
color=color, lw=0.5, alpha=0.6, ls="--", visible=False,
512510
)
513-
assoc_lines.append(aline)
511+
assoc_by_key[key].append(aline)
514512

515513
# Legend with per-type counts
516514
counts = {k: len(v) // 2 for k, v in groups.items()}
@@ -545,9 +543,7 @@ def _on_select(label: str) -> None:
545543
visible = _current_vis.get(key, False)
546544
for artist in artists:
547545
artist.set_visible(visible)
548-
# Keep association lines in sync with current filter if they are shown
549-
if check.get_status()[0]:
550-
_update_assoc_visibility()
546+
_update_assoc_visibility()
551547
ax.set_title(f"{tag} — TACS viewer [{label}]", fontsize=9)
552548
fig.canvas.draw_idle()
553549

@@ -559,12 +555,10 @@ def _on_select(label: str) -> None:
559555

560556
def _update_assoc_visibility() -> None:
561557
show = check.get_status()[0]
562-
for aline in assoc_lines:
563-
# Only show if the fiber's TACS group is currently visible
564-
aline.set_visible(show and _current_vis.get(aline.get_color(), True))
565-
# Simpler: just toggle all; color already matches the TACS type filter
566-
for aline in assoc_lines:
567-
aline.set_visible(show)
558+
for key, alines in assoc_by_key.items():
559+
visible = show and _current_vis.get(key, False)
560+
for aline in alines:
561+
aline.set_visible(visible)
568562

569563
def _on_check(_: str) -> None:
570564
_update_assoc_visibility()

src/tme_quant/src/tme_quant/fiber_analysis/utils/boundary_tif_utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,16 @@ def _compute_fiber_boundary_relative_angle(
153153
``compute_boundary_tangent_angle`` (port of ``find_outline_slope``) and
154154
``_circ_r`` (port of ``circ_r``).
155155
156-
**Angle convention (pycurvelets):** The returned angle is
157-
``arcsin(circ_r([2·fiber_angle_rad, 2·boundary_angle_rad]))`` in degrees,
158-
which equals ``90° − |fiber_angle − boundary_tangent_angle|``. This is the
159-
*complement* of the TACS ``angle_to_boundary_tangent`` convention.
156+
**Angle convention:** The returned angle is
157+
``arcsin(circ_r([2·fiber_angle_rad, 2·boundary_angle_rad]))`` in degrees.
158+
159+
``compute_boundary_tangent_angle`` (used internally) returns
160+
``atan2(Δcol, Δrow) % 180``, so its 0° is a *vertical* boundary and its 90° is
161+
a *horizontal* boundary — a 90° offset from the fiber-angle convention
162+
(0° = horizontal). This offset inverts the circ_r result so that the returned
163+
value equals the TACS ``angle_to_boundary_tangent`` directly:
164+
0° = parallel, 90° = perpendicular/invasive. **No conversion is needed before
165+
passing to** ``classify_fiber_tacs()``.
160166
161167
Parameters
162168
----------

src/tme_quant/src/tme_quant/fiber_analysis/utils/geometry_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,14 @@ def compute_boundary_tangent_angle(
593593
float
594594
Tangent angle in degrees [0°, 180°), or NaN when fewer than *num*
595595
connected pixels are found around *idx*.
596+
597+
.. warning::
598+
This function returns ``atan2(Δcol, Δrow) % 180`` — a 90°-rotated convention
599+
relative to standard fiber angles (which use 0° = horizontal/col-aligned).
600+
Here 0° = vertical boundary, 90° = horizontal boundary. When used in
601+
``_compute_fiber_boundary_relative_angle``, this offset causes
602+
``nearest_relative_boundary_angle`` to equal ``angle_to_boundary_tangent``
603+
directly (no 90° complement conversion needed).
596604
"""
597605
con_pts = _find_connected_pts(np.asarray(coords, dtype=int), idx, num)
598606
if np.any(np.isnan(con_pts)):

0 commit comments

Comments
 (0)