# Gap Analysis: 3D EFA Normalization

## 1. Current State Investigation

### Key Files and Modules

| File | Role | Lines |
|------|------|-------|
| `ktch/harmonic/_elliptic_fourier_analysis.py` | Main EFA implementation (2D + 3D) | 713 |
| `ktch/harmonic/__init__.py` | Public API re-export | 34 |
| `ktch/harmonic/tests/test_elliptic_fourier_analysis.py` | Test suite | ~300 |

### Existing 3D Capabilities

- **`_transform_single_3d`** (L448-546): Computes raw 3D Fourier coefficients. Calls `_normalize_3d` when `norm=True`.
- **`_normalize_3d`** (L548-549): Stub — raises `NotImplementedError`.
- **`_inverse_transform_single_3d`** (L551-599): Reconstructs 3D coordinates from coefficients. Handles `norm=True` by zeroing DC components.
- **`_cse` / `_sse`** (L655-711): Shared cosine/sine series expansion helpers. Work for any dimension.
- **`rotation_matrix_2d`** (L648-652): 2D rotation utility. Serves as template for 3D.

### Existing 2D Normalization (Template)

`_normalize_2d` (L347-398) implements Kuhl-Giardina (1982):
1. Extract 1st harmonic coefficients (a1, b1, c1, d1)
2. Compute rotation angle θ from 1st ellipse orientation
3. Compute scale from semi-major axis length
4. Compute phase angle ψ
5. Apply R(−ψ) · coef · R(nθ) / scale for each harmonic n ≥ 1
6. Return normalized coefficients + (ψ, scale)

This workflow maps structurally to the Godefroy 3D algorithm.

### Coefficient Layout

| Code | Paper (Godefroy) | Meaning |
|------|------------------|---------|
| `an[k]` | x_{c_k} | Cosine coefficient for x, harmonic k |
| `bn[k]` | x_{s_k} | Sine coefficient for x, harmonic k |
| `cn[k]` | y_{c_k} | Cosine coefficient for y, harmonic k |
| `dn[k]` | y_{s_k} | Sine coefficient for y, harmonic k |
| `en[k]` | z_{c_k} | Cosine coefficient for z, harmonic k |
| `fn[k]` | z_{s_k} | Sine coefficient for z, harmonic k |

Per-harmonic coefficient matrix (3×2):
```
C_k = [[an[k], bn[k]],    # x_c, x_s
       [cn[k], dn[k]],    # y_c, y_s
       [en[k], fn[k]]]    # z_c, z_s
```

### Conventions and Patterns

- **Naming**: Private `_normalize_Xd` methods, utility functions at module level
- **Return pattern**: `_normalize_2d` returns `(An, Bn, Cn, Dn, psi, scale)` — augmented tuple
- **Parallelization**: Transform dispatched per-sample via `Parallel`/`delayed`
- **Testing**: Co-located in `ktch/harmonic/tests/`, pytest-based

---

## 2. Requirements Feasibility Analysis

### Requirement-to-Asset Map

| Requirement | Existing Asset | Gap | Tag |
|-------------|---------------|-----|-----|
| **Req 1**: 3D normalization | `_normalize_3d` stub (L548-549) | Full implementation needed | **Missing** |
| **Req 1a**: Plane alignment | None | 3D rotation matrix (Euler ZXZ), geometric param extraction | **Missing** |
| **Req 1b**: Axis alignment within plane | None | Phase shift correction | **Missing** |
| **Req 1c**: Scale normalization | None | Ellipse area computation (A1 = πa1b1) | **Missing** |
| **Req 2**: Return orientation/scale (3D) | `_transform_single_3d` accepts param but ignores it | Implementation needed | **Missing** |
| **Req 2**: Euler angle parameterization | None | Geometric parameter extraction from coefficients | **Missing** |
| **Req 3**: Inverse transform (3D, norm) | `_inverse_transform_single_3d` exists | Already zeroes DC; sufficient for basic usage | **Partial** |
| **Req 4**: Arc-length fix | L493: `sqrt(dx²+dy²)` | Add `dz²` | **Bug** |
| **Req 4 (secondary)**: DC component | L530-532: missing `* dt` | Inconsistent with 2D DC calculation | **Bug** |
| **Req 5**: Backward compatibility | All existing methods | No breaking changes needed | **OK** |
| **Req 6**: Tests | `test_elliptic_fourier_analysis.py` | 3D normalization tests entirely missing | **Missing** |

### Bugs Discovered

#### Bug 1: Arc-Length Missing z-Component
- **Location**: `_elliptic_fourier_analysis.py:493`
- **Current**: `dt = np.sqrt(dx**2 + dy**2)`
- **Correct**: `dt = np.sqrt(dx**2 + dy**2 + dz**2)`
- **Impact**: Critical — incorrect parameterization distorts all 3D coefficients when `t=None`

#### Bug 2: DC Component Calculation (3D)
- **Location**: `_elliptic_fourier_analysis.py:530-532`
- **Current**: `a0 = 2 / T * np.sum(X_arr[1:, 0])`
- **2D equivalent** (L329): `a0 = 2 * np.sum(X_arr[1:, 0] * dt) / T`
- **Impact**: Incorrect mean position for non-uniform sampling
- **Note**: This is a pre-existing bug independent of normalization

### Technical Needs from Godefroy et al. (2012) Algorithm

The paper describes a 4-step normalization (Section 3):

**Step 1 — Rescaling (§3.1.1)**: Divide all coefficients by √(A₁) where A₁ = π·a₁·b₁

**Step 2 — Reorientation (§3.1.2)**: Apply Ω₁⁻¹ to all harmonic matrices, where Ω₁ = R_γ₁ · R_β₁ · R_α₁ (ZXZ Euler)

**Step 3 — Phase shift (§3.1.3)**: Rotate each harmonic's (cos, sin) pair by φ₁

**Step 4 — Direction (§3.1.4)**: If Y_{s₁} < 0 after reorientation, negate all sine coefficients

To execute these steps, need to first compute geometric parameters (§2):
- Phase φ from arctan formula (Eq. 4)
- Semi-axes a, b from Eqs. 5-6
- Euler angles α, β, γ from the rotation matrix Ω

### Research Needed

1. **Euler angle singularity handling**: β = 0 or π causes gimbal lock. Godefroy et al. do not address this. Need to decide on numerical strategy (e.g., small perturbation, quaternion fallback).
2. **Uniqueness conditions**: Paper imposes constraints (φ ∈ ]−π/4, π/4[, a,b > 0, β ∈ [0,π]) to get unique solution among 32 possible. Implementation must enforce these.
3. **`return_orientation_scale` format**: 2D returns (ψ, scale) as 2 values. 3D needs (α, β, γ, scale) = 4 values, or possibly flatten the 3×3 rotation matrix. Design decision needed.

### Complexity Signal

- **Algorithmic logic**: Complex — multi-step normalization with geometric computations, Euler angle extraction, edge cases
- **External integration**: None — pure numerical computation
- **Performance**: Single-sample normalization is O(n_harmonics), negligible cost

---

## 3. Implementation Approach Options

### Option A: Extend `_elliptic_fourier_analysis.py` In-Place

**Rationale**: The 2D normalization already lives in this file. Adding 3D follows the same pattern.

**Changes**:
- Replace `_normalize_3d` stub with full implementation (~80-120 lines)
- Add helper functions at module level: `rotation_matrix_3d_euler_zxz()`, `_compute_harmonic_geometry_3d()`
- Fix bugs at L493 and L530-532
- Add `return_orientation_scale` handling in `_transform_single_3d`
- Add tests in `test_elliptic_fourier_analysis.py`

**Trade-offs**:
- ✅ Follows existing pattern (2D normalization is in the same file)
- ✅ Single-file change, minimal disruption
- ✅ `rotation_matrix_2d` already lives at module level in this file
- ❌ File grows by ~150-200 lines (currently 713 → ~900)
- ❌ Geometric parameter extraction is reusable but buried in this file

### Option B: Extract Rotation/Geometry Utilities to Separate Module

**Rationale**: 3D rotation matrices and geometric parameter extraction are general-purpose utilities.

**Changes**:
- New file: `ktch/harmonic/_rotation.py` with rotation matrix utilities
- New file: `ktch/harmonic/_geometry.py` with geometric parameter extraction
- Implement `_normalize_3d` in `_elliptic_fourier_analysis.py` using imports
- Move existing `rotation_matrix_2d` to `_rotation.py` (deprecation shim in old location)

**Trade-offs**:
- ✅ Clean separation, reusable utilities
- ✅ Easier to test rotation/geometry independently
- ❌ More files, more complex import graph
- ❌ Moving `rotation_matrix_2d` may be over-engineering for this scope
- ❌ Departs from current single-file pattern for each analysis method

### Option C: Hybrid — Helpers in Same File, Extract Later if Needed

**Rationale**: Add all new code within the existing file (matching current pattern) but structure it cleanly for potential future extraction.

**Changes**:
- Implement `_normalize_3d` in `_elliptic_fourier_analysis.py`
- Add `rotation_matrix_3d_euler_zxz()` and `_compute_harmonic_geometry_3d()` as module-level functions alongside existing `rotation_matrix_2d` and `_cse`/`_sse`
- Fix bugs
- Add tests

**Trade-offs**:
- ✅ Follows existing convention exactly
- ✅ Minimal file proliferation
- ✅ Easy to extract later if utilities are needed elsewhere
- ✅ Module-level utility functions are already the established pattern
- ❌ File grows, but remains coherent (single analysis method)

---

## 4. Implementation Complexity & Risk

**Effort**: **M (3–7 days)**
- Core algorithm is well-documented in the paper
- Mathematical formulation is explicit (equations provided)
- Main complexity is in Euler angle extraction and edge cases
- Testing requires crafting synthetic 3D test data with known properties

**Risk**: **Medium**
- **Known**: Algorithm from peer-reviewed paper with explicit equations
- **Manageable**: Euler angle singularities have established solutions
- **Moderate**: Numerical stability of geometric parameter extraction needs care
- **Low**: No external dependencies needed (numpy/scipy sufficient)

---

## 5. Recommendations for Design Phase

### Preferred Approach

**Option C (Hybrid)** — Add all code to existing file, matching current patterns. Module-level utility functions for rotation matrices and geometry extraction, private `_normalize_3d` method for the normalization workflow.

### Key Design Decisions to Resolve

1. **`return_orientation_scale` format for 3D**: Return (α, β, γ, scale) as 4 appended values, or use a different parameterization? The 2D case returns (ψ, scale) = 2 values.

2. **Euler angle singularity strategy**: Use `scipy.spatial.transform.Rotation` for robustness, or implement paper equations directly?

3. **DC component bug (L530-532)**: Fix alongside normalization, or treat as separate bug fix? (Recommendation: fix together since it affects 3D coefficient correctness.)

4. **Phase ambiguity resolution**: Paper specifies φ ∈ ]−π/4, π/4[ and imposes uniqueness conditions. Need to implement the full constraint set.

### Research Items for Design Phase

- Verify the paper's uniqueness conditions (Section 2) are sufficient for all practical cases
- Determine test strategy: synthetic ellipses with known Euler angles vs. reference dataset comparison
- Evaluate whether `scipy.spatial.transform.Rotation` should be used for Euler angle computations (adds a scipy dependency that already exists)
