# Research & Design Decisions

---
**Purpose**: Capture discovery findings and design rationale for the 3D EFA normalization feature.
---

## Summary
- **Feature**: `3d-efa-normalization`
- **Discovery Scope**: Extension
- **Key Findings**:
  - Godefroy et al. (2012) provides an explicit 4-step algorithm: rescaling, reorientation, phase shift, direction correction
  - Two pre-existing bugs found in `_transform_single_3d`: arc-length missing dz² and DC component missing `* dt`
  - The paper's Euler angle convention is ZXZ (extrinsic), matching `scipy.spatial.transform.Rotation` with `"ZXZ"` convention

## Research Log

### Godefroy et al. (2012) Algorithm Mapping
- **Context**: Need to map paper equations to code implementation
- **Sources Consulted**: `ref/Godefroy_2012_Comptes Rendus Biologies_1.pdf`, Sections 2 and 3
- **Findings**:
  - Per-harmonic coefficient matrix C_k = [[x_c, x_s], [y_c, y_s], [z_c, z_s]] maps to code's [[an[k], bn[k]], [cn[k], dn[k]], [en[k], fn[k]]]
  - Geometric parameters of each harmonic ellipse (a, b, α, β, γ, φ) are derived from 6 coefficients via Eqs. 4-12
  - Normalization is: (1) scale by 1/√(πa₁b₁), (2) rotate by Ω₁⁻¹, (3) phase-shift by φ₁, (4) fix direction
  - Paper imposes uniqueness: φ ∈ ]−π/4, π/4[, (a,b) ∈ (ℝ⁺)², β ∈ [0, π]
- **Implications**: Direct implementation of paper equations; no need for external algorithms

### Euler Angle Convention and Singularities
- **Context**: Paper uses ZXZ Euler angles; need to handle gimbal lock
- **Sources Consulted**: scipy.spatial.transform.Rotation docs, linear algebra references
- **Findings**:
  - ZXZ convention: Ω = R_γ(k) · R_β(i_α) · R_α(k), where rotations are about axes of the rotated frame
  - Gimbal lock at β = 0 or π: α and γ become degenerate (only α+γ or α−γ is determined)
  - scipy.spatial.transform.Rotation supports `"ZXZ"` Euler angles natively
  - For morphometric data, β ≈ 0 means the first harmonic ellipse is nearly planar in the xy-plane (degenerate 3D curve)
- **Implications**: Implement paper equations directly for the main path; use a numerical guard for β near 0 or π (set γ = 0, compute α from remaining freedom)

### DC Component Bug Analysis
- **Context**: 3D DC terms computed differently from 2D
- **Sources Consulted**: Existing code comparison (2D L329 vs 3D L530-532)
- **Findings**:
  - 2D: `a0 = 2 * np.sum(X_arr[1:, 0] * dt) / T` (weighted by dt)
  - 3D: `a0 = 2 / T * np.sum(X_arr[1:, 0])` (unweighted)
  - The 2D formula is the correct trapezoidal approximation of the mean
  - The 3D formula gives incorrect mean coordinates for non-uniform sampling
- **Implications**: Fix DC computation in 3D to match 2D pattern

### Return Orientation/Scale Format for 3D
- **Context**: 2D returns (ψ, scale) = 2 values; need equivalent for 3D
- **Sources Consulted**: Existing `_normalize_2d` return pattern, `test_orientation_and_scale_2d`
- **Findings**:
  - 2D appends [psi, scale] (2 values) at end of coefficient array
  - For 3D, the transformation parameters are: Euler angles (α₁, β₁, γ₁), phase φ₁, and scale √A₁
  - Returning (α₁, β₁, γ₁, φ₁, scale) = 5 values is most informative
  - Alternative: return only (φ₁, scale) for minimal parity with 2D — but loses rotation info
- **Implications**: Return 5 values (α₁, β₁, γ₁, φ₁, scale) to fully describe the normalization transformation

## Architecture Pattern Evaluation

| Option | Description | Strengths | Risks / Limitations | Notes |
|--------|-------------|-----------|---------------------|-------|
| In-place extension | All code in `_elliptic_fourier_analysis.py` | Follows existing pattern exactly | File grows to ~900 lines | Recommended |
| Separate utilities | New `_rotation.py`, `_geometry.py` | Clean separation | Over-engineering for scope | Not recommended |
| Hybrid | Helpers in same file, structured for extraction | Best balance | Slightly larger file | Selected |

## Design Decisions

### Decision: Implement Paper Equations Directly (Not scipy.spatial.transform)
- **Context**: Need 3D rotation matrices and Euler angle extraction
- **Alternatives Considered**:
  1. Use scipy.spatial.transform.Rotation for all Euler angle operations
  2. Implement paper equations directly using numpy
- **Selected Approach**: Implement paper equations directly, with a numerical guard for gimbal lock
- **Rationale**: The paper provides explicit, well-tested equations. Direct implementation makes the code self-documenting with respect to the reference paper. scipy.spatial.transform would add an indirect layer and its convention mapping needs verification.
- **Trade-offs**: Slightly more code, but clearer correspondence to paper; no risk of convention mismatch
- **Follow-up**: Validate against scipy.spatial.transform in tests as a cross-check

### Decision: Return 5 Orientation/Scale Values for 3D
- **Context**: `return_orientation_scale=True` needs a defined output format for 3D
- **Alternatives Considered**:
  1. Return (α, β, γ, φ, scale) = 5 appended values
  2. Return (φ, scale) = 2 values (minimal parity with 2D)
  3. Return flattened 3×3 rotation matrix + scale = 10 values
- **Selected Approach**: Return (α₁, β₁, γ₁, φ₁, scale) = 5 values appended to coefficient array
- **Rationale**: Provides all parameters needed to reconstruct the original orientation and size. Matches the paper's parameterization. Euler angles are directly interpretable.
- **Trade-offs**: 5 extra columns vs 2 for 2D; users must know the ordering
- **Follow-up**: Document the ordering clearly in the docstring

### Decision: Fix DC Component Bug Alongside Normalization
- **Context**: DC terms in 3D use an incorrect formula (unweighted sum)
- **Selected Approach**: Fix in same PR since it affects 3D coefficient correctness and normalization depends on correct coefficients
- **Rationale**: Normalization divides by quantities derived from correct coefficients; fixing the bug is prerequisite

### Decision: Uniqueness Constraints Per Paper Section 2
- **Context**: 32 possible solutions for geometric parameters; need unique solution
- **Selected Approach**: Implement the paper's constraints: φ ∈ ]−π/4, π/4[, (a,b) > 0, β ∈ [0, π]
- **Rationale**: Paper's constraints are mathematically proven to yield unique solutions
- **Follow-up**: Test with synthetic data covering all quadrants

## Risks & Mitigations
- **Gimbal lock (β ≈ 0 or π)**: Set γ = 0 and compute α from the combined angle; add test case
- **Numerical precision in arctan**: Use `np.arctan2` where possible for correct quadrant handling
- **Direction ambiguity**: Apply direction fix (§3.1.4) strictly after reorientation; test with both signs

## References
- [Godefroy et al. 2012](https://doi.org/10.1016/j.crvi.2011.12.004) — Primary reference for 3D normalization algorithm
- [Kuhl & Giardina 1982](https://doi.org/10.1016/0146-664X(82)90034-X) — Foundation for 2D normalization (existing implementation)
- [scipy.spatial.transform.Rotation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html) — Cross-validation reference for Euler angles
