# Implementation Plan

- [x] 1. Add parallel computation import and fix size-and-shape mode bug
  - Add the `Parallel` and `delayed` imports from sklearn's parallel utilities to the Procrustes analysis module
  - Fix the broken size-and-shape alignment method: replace undefined instance attributes with local variables for specimen count and landmark count, and fix the iteration loop to use the centered data instead of the original input
  - Verify the fix by running existing tests to confirm no regressions
  - _Requirements: 4.1, 2.1_

- [x] 2. Parallelize shape alignment without semilandmarks
  - Add a static helper that performs single-specimen Procrustes alignment (delegates to scipy's procrustes function) and returns the aligned specimen and disparity
  - Modify the non-semilandmark branch of the shape alignment loop to dispatch per-specimen work through `Parallel(n_jobs=self.n_jobs)` using the new helper
  - Unpack collected results into the aligned specimen array and total disparity, preserving the existing convergence logic
  - _Requirements: 1.1, 1.2, 1.3, 1.4, 4.2, 4.3_

- [x] 3. Parallelize size-and-shape alignment
  - Add a static helper that performs single-specimen orthogonal rotation (delegates to scipy's orthogonal_procrustes) and returns the rotated specimen and per-specimen disparity
  - Modify the size-and-shape alignment loop to dispatch per-specimen rotation through `Parallel(n_jobs=self.n_jobs)` using the new helper
  - Collect and unpack results, preserving convergence behavior
  - _Requirements: 2.1, 2.2, 4.2, 4.3_

- [x] 4. Parallelize semilandmark iteration
  - Add an instance method that consolidates the per-specimen semilandmark iteration: rotate specimen and curve geometry to match the mean shape, compute disparity, slide semilandmarks using the appropriate criterion, re-project onto the original curve, then center and scale both the specimen and curve geometry
  - Accept the sliding function as a callable parameter to avoid branching within the consolidated method
  - Modify the semilandmark branch of the shape alignment loop to dispatch per-specimen work through a single `Parallel(n_jobs=self.n_jobs)` call using the consolidated method, replacing the three separate per-specimen loops
  - Unpack collected results into the specimen array, curve geometry array, and total disparity
  - _Requirements: 3.1, 3.2, 3.3, 4.2, 4.3_

- [x] 5. Add parametrized n_jobs tests
- [x] 5.1 Test parallel shape alignment without semilandmarks
  - Add a parametrized test over `n_jobs` values `[None, 1, 3]` for standard GPA shape alignment using the mosquito wings dataset
  - Compute a baseline result with `n_jobs=None` and verify each parallel configuration produces numerically equivalent output and mean shape
  - Follow the existing EFA test pattern with `@pytest.mark.parametrize`
  - _Requirements: 5.1, 5.3, 5.4_

- [x] 5.2 Test parallel semilandmark sliding
  - Add parametrized tests over `n_jobs` values `[None, 1, 3]` for GPA with curve semilandmarks, covering both bending energy and Procrustes distance criteria
  - Use the existing semilandmark test fixtures (2D and optionally 3D)
  - Verify aligned results and mean shape are numerically equivalent across all `n_jobs` values
  - _Requirements: 5.2, 5.3, 5.4_

- [x] 5.3 Test parallel size-and-shape alignment
  - Add a parametrized test over `n_jobs` values `[None, 1, 3]` for GPA with `scaling=False`
  - Verify aligned results and mean shape are numerically equivalent across all `n_jobs` values
  - _Requirements: 2.2, 5.3, 5.4_
