diff --git a/benchmarks/benchmarks/bench_intervals.py b/benchmarks/benchmarks/bench_intervals.py index 82f126e1..1f4ef451 100644 --- a/benchmarks/benchmarks/bench_intervals.py +++ b/benchmarks/benchmarks/bench_intervals.py @@ -2,11 +2,27 @@ from asv_runner.benchmarks.mark import skip_params_if -from foapy import intervals +from foapy import chain_mode +from foapy.core import intervals_chain, intervals_tuple, tuple_mode from .cases import best_case, dna_case, normal_case, worst_case length = [5, 50, 500, 5000, 50000, 500000, 5000000, 50000000] + +# mode integer values: 1=lossy, 2=normal, 3=cycle, 4=redundant +_CHAIN_MODE = { + 1: chain_mode.boundary, + 2: chain_mode.boundary, + 3: chain_mode.cycle, + 4: chain_mode.boundary, +} +_TUPLE_MODE = { + 1: tuple_mode.lossy, + 2: tuple_mode.normal, + 3: tuple_mode.normal, + 4: tuple_mode.redundant, +} + skip = [ (5000000, "Worst", 1, 1), (5000000, "DNA", 1, 1), @@ -81,10 +97,11 @@ class IntervalsSuite: param_names = ["length", "case", "binding", "mode"] data = None - mode = None - binding = None + chain_mode_val = None + tuple_mode_val = None + binding_val = None - def setup(self, length, case, binding, mode): + def setup(self, length, case, binding_int, mode_int): if case == "Best": self.data = best_case(length) elif case == "DNA": @@ -93,13 +110,22 @@ def setup(self, length, case, binding, mode): self.data = normal_case(length) elif case == "Worst": self.data = worst_case(length) - self.mode = mode - self.binding = binding + self.binding_val = binding_int + self.chain_mode_val = _CHAIN_MODE[mode_int] + self.tuple_mode_val = _TUPLE_MODE[mode_int] @skip_params_if(skip, os.getenv("QUICK_BENCHMARK") == "true") - def time_intervals(self, length, case, binding, mode): - intervals(self.data, self.binding, self.mode) + def time_intervals(self, length, case, binding_int, mode_int): + intervals_tuple( + intervals_chain(self.data, self.binding_val, self.chain_mode_val), + self.binding_val, + self.tuple_mode_val, + ) @skip_params_if(skip, os.getenv("QUICK_BENCHMARK") == "true") - def peakmem_intervals(self, length, case, binding, mode): - return intervals(self.data, self.binding, self.mode) + def peakmem_intervals(self, length, case, binding_int, mode_int): + return intervals_tuple( + intervals_chain(self.data, self.binding_val, self.chain_mode_val), + self.binding_val, + self.tuple_mode_val, + ) diff --git a/benchmarks/benchmarks/bench_ma_intervals.py b/benchmarks/benchmarks/bench_ma_intervals.py index 2c69f1bb..10de5add 100644 --- a/benchmarks/benchmarks/bench_ma_intervals.py +++ b/benchmarks/benchmarks/bench_ma_intervals.py @@ -2,12 +2,27 @@ from asv_runner.benchmarks.mark import skip_params_if -from foapy.ma import intervals, order +from foapy import chain_mode +from foapy.core import tuple_mode +from foapy.ma import intervals_chain, intervals_tuple, order from .ma_cases import best_case, dna_case, normal_case, worst_case length = [5, 50, 500] -# , 5000, 50000, 500000, 5000000, 50000000 + +_CHAIN_MODE = { + 1: chain_mode.boundary, + 2: chain_mode.boundary, + 3: chain_mode.cycle, + 4: chain_mode.boundary, +} +_TUPLE_MODE = { + 1: tuple_mode.lossy, + 2: tuple_mode.normal, + 3: tuple_mode.normal, + 4: tuple_mode.redundant, +} + skip = [ (5000000, "Worst", 1, 1), (5000000, "DNA", 1, 1), @@ -81,10 +96,11 @@ class MaIntervalsSuite: param_names = ["length", "case", "binding", "mode"] data = None - mode = None - binding = None + chain_mode_val = None + tuple_mode_val = None + binding_val = None - def setup(self, length, case, binding, mode): + def setup(self, length, case, binding_int, mode_int): if case == "Best": self.data = order(best_case(length)) elif case == "DNA": @@ -93,13 +109,22 @@ def setup(self, length, case, binding, mode): self.data = order(normal_case(length)) elif case == "Worst": self.data = order(worst_case(length)) - self.mode = mode - self.binding = binding + self.binding_val = binding_int + self.chain_mode_val = _CHAIN_MODE[mode_int] + self.tuple_mode_val = _TUPLE_MODE[mode_int] @skip_params_if(skip, os.getenv("QUICK_BENCHMARK") == "true") - def time_intervals(self, length, case, binding, mode): - intervals(self.data, self.binding, self.mode) + def time_intervals(self, length, case, binding_int, mode_int): + intervals_tuple( + intervals_chain(self.data, self.binding_val, self.chain_mode_val), + self.binding_val, + self.tuple_mode_val, + ) @skip_params_if(skip, os.getenv("QUICK_BENCHMARK") == "true") - def peakmem_intervals(self, length, case, binding, mode): - return intervals(self.data, self.binding, self.mode) + def peakmem_intervals(self, length, case, binding_int, mode_int): + return intervals_tuple( + intervals_chain(self.data, self.binding_val, self.chain_mode_val), + self.binding_val, + self.tuple_mode_val, + ) diff --git a/benchmarks/benchmarks/bench_pipeline_full.py b/benchmarks/benchmarks/bench_pipeline_full.py index d1ea37d5..77fe32d6 100644 --- a/benchmarks/benchmarks/bench_pipeline_full.py +++ b/benchmarks/benchmarks/bench_pipeline_full.py @@ -30,10 +30,10 @@ def setup(self, length, case): def time_pipeline_full(self, length, case): chain = intervals_chain(self.data, binding.start, chain_mode.boundary) - result = intervals_tuple(chain, tuple_mode.normal) + result = intervals_tuple(chain, binding.start, tuple_mode.normal) intervals_distribution(result) def peakmem_pipeline_full(self, length, case): chain = intervals_chain(self.data, binding.start, chain_mode.boundary) - result = intervals_tuple(chain, tuple_mode.normal) + result = intervals_tuple(chain, binding.start, tuple_mode.normal) return intervals_distribution(result) diff --git a/docs/references/mode.md b/docs/references/chain_mode.md similarity index 84% rename from docs/references/mode.md rename to docs/references/chain_mode.md index c6de8314..64b397a0 100644 --- a/docs/references/mode.md +++ b/docs/references/chain_mode.md @@ -1,4 +1,4 @@ -::: foapy.mode +::: foapy.chain_mode options: show_signature_annotations: false members_order: source @@ -7,11 +7,11 @@ show_symbol_type_heading: false show_symbol_type_toc: false -::: foapy.mode +::: foapy.chain_mode options: show_root_heading: false show_docstring_description: false show_docstring_attributes: false show_root_toc_entry: false filters: - - foapy.mode + - foapy.chain_mode diff --git a/docs/references/intervals.md b/docs/references/intervals.md deleted file mode 100644 index 110b9a15..00000000 --- a/docs/references/intervals.md +++ /dev/null @@ -1 +0,0 @@ -::: foapy.intervals diff --git a/docs/references/intervals_chain.md b/docs/references/intervals_chain.md new file mode 100644 index 00000000..611889da --- /dev/null +++ b/docs/references/intervals_chain.md @@ -0,0 +1 @@ +::: foapy.intervals_chain diff --git a/docs/references/intervals_distribution.md b/docs/references/intervals_distribution.md new file mode 100644 index 00000000..9f79bea8 --- /dev/null +++ b/docs/references/intervals_distribution.md @@ -0,0 +1 @@ +::: foapy.intervals_distribution diff --git a/docs/references/intervals_tuple.md b/docs/references/intervals_tuple.md new file mode 100644 index 00000000..e2aa3b1c --- /dev/null +++ b/docs/references/intervals_tuple.md @@ -0,0 +1 @@ +::: foapy.intervals_tuple diff --git a/docs/references/ma/intervals.md b/docs/references/ma/intervals.md deleted file mode 100644 index 4af2c60c..00000000 --- a/docs/references/ma/intervals.md +++ /dev/null @@ -1,2 +0,0 @@ -# foapy.ma.intervals -::: foapy.ma.intervals diff --git a/docs/references/ma/intervals_chain.md b/docs/references/ma/intervals_chain.md new file mode 100644 index 00000000..cd7fc112 --- /dev/null +++ b/docs/references/ma/intervals_chain.md @@ -0,0 +1 @@ +::: foapy.ma.intervals_chain diff --git a/docs/references/ma/intervals_distribution.md b/docs/references/ma/intervals_distribution.md new file mode 100644 index 00000000..ef1c115d --- /dev/null +++ b/docs/references/ma/intervals_distribution.md @@ -0,0 +1 @@ +::: foapy.ma.intervals_distribution diff --git a/docs/references/ma/intervals_tuple.md b/docs/references/ma/intervals_tuple.md new file mode 100644 index 00000000..f1bad2cc --- /dev/null +++ b/docs/references/ma/intervals_tuple.md @@ -0,0 +1 @@ +::: foapy.ma.intervals_tuple diff --git a/docs/references/tuple_mode.md b/docs/references/tuple_mode.md new file mode 100644 index 00000000..da864705 --- /dev/null +++ b/docs/references/tuple_mode.md @@ -0,0 +1,17 @@ +::: foapy.tuple_mode + options: + show_signature_annotations: false + members_order: source + show_docstring_examples: false + show_source: false + show_symbol_type_heading: false + show_symbol_type_toc: false + +::: foapy.tuple_mode + options: + show_root_heading: false + show_docstring_description: false + show_docstring_attributes: false + show_root_toc_entry: false + filters: + - foapy.tuple_mode diff --git a/mkdocs.yml b/mkdocs.yml index bb86b23a..f1d73a78 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,14 +77,19 @@ nav: - "References": - "foapy.alphabet": references/alphabet.md - "foapy.order": references/order.md - - "foapy.intervals": references/intervals.md - "foapy.binding": references/binding.md - - "foapy.mode": references/mode.md + - "foapy.chain_mode": references/chain_mode.md + - "foapy.tuple_mode": references/tuple_mode.md + - "foapy.intervals_chain": references/intervals_chain.md + - "foapy.intervals_tuple": references/intervals_tuple.md + - "foapy.intervals_distribution": references/intervals_distribution.md - "foapy.ma": - references/ma/index.md - "alphabet": references/ma/alphabet.md - "order": references/ma/order.md - - "intervals": references/ma/intervals.md + - "intervals_chain": references/ma/intervals_chain.md + - "intervals_tuple": references/ma/intervals_tuple.md + - "intervals_distribution": references/ma/intervals_distribution.md - "foapy.characteristics": - references/characteristics/index.md - "arithmetic_mean": references/characteristics/arithmetic_mean.md diff --git a/specs/003-cleanup-intervals-pipeline/checklists/requirements.md b/specs/003-cleanup-intervals-pipeline/checklists/requirements.md new file mode 100644 index 00000000..3ae15aa5 --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Cleanup Intervals Pipeline + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-19 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass. Spec is ready for `/speckit.plan`. diff --git a/specs/003-cleanup-intervals-pipeline/contracts/api-changes.md b/specs/003-cleanup-intervals-pipeline/contracts/api-changes.md new file mode 100644 index 00000000..2db1b3f3 --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/contracts/api-changes.md @@ -0,0 +1,71 @@ +# API Contract: Cleanup Intervals Pipeline + +**Branch**: `003-cleanup-intervals-pipeline` | **Date**: 2026-04-19 + +## Public API Diff + +### Removed from `foapy` namespace + +```python +# REMOVED — raises ImportError after this change +from foapy import intervals # was: intervals(X, binding, mode) -> ndarray +from foapy import mode # was: mode.lossy / mode.normal / mode.cycle / mode.redundant +from foapy import is_valid_intervals_chain # was: is_valid_intervals_chain(chain) -> bool +``` + +### Removed from `foapy.ma` namespace + +```python +# REMOVED — raises ImportError after this change +from foapy.ma import intervals # was: intervals(X, binding, mode) -> list[ndarray] +``` + +### Unchanged in `foapy` namespace + +```python +from foapy import intervals_chain # intervals_chain(X, binding, chain_mode) -> ndarray +from foapy import intervals_tuple # intervals_tuple(chain, binding, tuple_mode) -> ndarray +from foapy import intervals_distribution # intervals_distribution(intervals_tuple) -> ndarray +from foapy import chain_mode # chain_mode.boundary | chain_mode.cycle +from foapy import tuple_mode # tuple_mode.lossy | tuple_mode.normal | tuple_mode.redundant +from foapy import binding # binding.start | binding.end +from foapy import order # order(X) -> ndarray +from foapy import alphabet # alphabet(X) -> ndarray +``` + +### Unchanged in `foapy.ma` namespace + +```python +from foapy.ma import intervals_chain +from foapy.ma import intervals_tuple +from foapy.ma import intervals_distribution +from foapy.ma import order +from foapy.ma import alphabet +``` + +--- + +## Mode Mapping Reference + +The following table documents the equivalence between the retired `mode` enum and the decomposed pipeline. This is the contract verified by the equivalence test module. + +| Old call | Decomposed equivalent | +|----------|----------------------| +| `intervals(X, b, mode.lossy)` | `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.lossy)` | +| `intervals(X, b, mode.normal)` | `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.normal)` | +| `intervals(X, b, mode.cycle)` | `intervals_tuple(intervals_chain(X, b, chain_mode.cycle), b, tuple_mode.normal)` | +| `intervals(X, b, mode.redundant)` | `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.redundant)` | + +--- + +## Test-Helper Availability + +The retired `intervals()` functions are available **only** within the test suite: + +```python +# Available in tests ONLY — not a public API +from tests.helpers.intervals import intervals, mode +from tests.helpers.ma_intervals import intervals as ma_intervals +``` + +These helpers MUST NOT be imported from any production source file. diff --git a/specs/003-cleanup-intervals-pipeline/data-model.md b/specs/003-cleanup-intervals-pipeline/data-model.md new file mode 100644 index 00000000..22bd7fc7 --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/data-model.md @@ -0,0 +1,61 @@ +# Data Model: Cleanup Intervals Pipeline + +**Branch**: `003-cleanup-intervals-pipeline` | **Date**: 2026-04-19 + +## Overview + +This feature removes entities from the public API rather than adding them. The only new structural entity introduced is the **test helper module** that houses the retired `intervals()` function. + +--- + +## Retired Entities (removed from public API) + +| Entity | Current location | Disposition | +|--------|-----------------|-------------| +| `intervals(X, binding, mode)` | `src/foapy/core/_intervals.py` | Moved to `tests/helpers/intervals.py` | +| `intervals` (ma variant) | `src/foapy/ma/_intervals.py` | Moved to `tests/helpers/ma_intervals.py` | +| `mode` enum | `src/foapy/core/_mode.py` | Deleted; superseded by `chain_mode` + `tuple_mode` | +| `is_valid_intervals_chain(chain)` | `src/foapy/core/_is_valid_intervals_chain.py` | Deleted; feature deferred | + +--- + +## New Entity: Test Helper Module + +### `tests/helpers/intervals.py` + +Purpose: Provides the retired `intervals(X, binding, mode)` function for use by the equivalence test suite only. Not importable from any `foapy` package path. + +**Contents**: +- The `intervals()` function body moved verbatim from `src/foapy/core/_intervals.py` +- The `mode` enum class moved verbatim from `src/foapy/core/_mode.py` (needed as a local dependency) +- All imports are self-contained within the helpers directory; no `from foapy import mode` references + +**Invariants**: +- Not re-exported from `tests/__init__.py` or any `foapy` init file +- Used only by `tests/test_intervals.py`, `tests/test_ma_intervals.py`, and `tests/test_pipeline_consistency.py` + +### `tests/helpers/ma_intervals.py` + +Purpose: Provides the retired `foapy.ma.intervals(X, binding, mode)` function for equivalence testing of the ma subpackage. + +**Contents**: +- The `intervals()` function body moved verbatim from `src/foapy/ma/_intervals.py` +- Local copy of `mode` enum (or imported from `tests/helpers/intervals.py`) + +--- + +## Remaining Public Entities (unchanged) + +These entities retain their current definitions and are listed here for completeness. + +| Entity | Module | Description | +|--------|--------|-------------| +| `intervals_chain(X, binding, chain_mode)` | `foapy.core` / `foapy` | Computes raw interval chain from any 1-D sequence | +| `intervals_tuple(chain, binding, tuple_mode)` | `foapy.core` / `foapy` | Applies tuple transformation mode to a chain | +| `intervals_distribution(intervals_tuple)` | `foapy.core` / `foapy` | Computes count distribution of interval lengths | +| `chain_mode` enum (`boundary`, `cycle`) | `foapy.core` / `foapy` | Controls chain construction strategy | +| `tuple_mode` enum (`lossy`, `normal`, `redundant`) | `foapy.core` / `foapy` | Controls boundary-interval handling | +| `binding` enum (`start`, `end`) | `foapy.core` / `foapy` | Controls left-to-right vs right-to-left direction | +| `order(X)` | `foapy.core` / `foapy` | Maps sequence to integer order indices | +| `alphabet(X)` | `foapy.core` / `foapy` | Returns first-appearance alphabet of sequence | +| All `foapy.ma` variants | `foapy.ma` | `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `order`, `alphabet` | diff --git a/specs/003-cleanup-intervals-pipeline/plan.md b/specs/003-cleanup-intervals-pipeline/plan.md new file mode 100644 index 00000000..cdb244bb --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/plan.md @@ -0,0 +1,136 @@ +# Implementation Plan: Cleanup Intervals Pipeline + +**Branch**: `003-cleanup-intervals-pipeline` | **Date**: 2026-04-19 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/003-cleanup-intervals-pipeline/spec.md` + +## Summary + +Remove three interim artefacts left over from 002-decompose-intervals-pipeline — the `_is_valid_intervals_chain` stub, the unified `mode` enum, and the `intervals()` core function — so that the public API exposes only the decomposed pipeline (`intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, `tuple_mode`). The retired `intervals()` function is preserved as a test-only helper, existing equivalence tests are kept and expanded, benchmarks are updated to use the decomposed pipeline, and documentation is updated to reflect the new canonical API with links to the fundamentals pages. + +## Technical Context + +**Language/Version**: Python 3.8+ +**Primary Dependencies**: numpy >= 1.20 (sole runtime dependency) +**Storage**: N/A +**Testing**: pytest via `tox -e default` +**Target Platform**: Any platform supporting Python 3.8+ with numpy +**Project Type**: library +**Performance Goals**: No new algorithms; existing performance characteristics are unchanged +**Constraints**: No new runtime dependencies; no breaking changes to the `foapy.ma` subpackage beyond removal of `ma.intervals` +**Scale/Scope**: ~10 source files changed, ~5 test files changed, ~10 documentation files changed + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. Code Quality — single responsibility, no extra deps, passes black/isort/flake8 | PASS | Removal only; no new code with multi-responsibility concerns. No new dependencies. | +| II. Testing Standards — tox canonical runner, AssertBatch for binding×mode, edge cases covered | PASS | Equivalence tests cover all 4 mode mappings × 2 binding directions. Edge cases (empty, single, all-unique, all-same) are required by the spec. Removed test_is_valid_intervals_chain.py is consistent with deferral of that feature. | +| III. API Consistency — uniform contract, `foapy.ma` mirrors core, project-defined exceptions | PASS | Five remaining public names retain their signatures unchanged. `foapy.ma.intervals` is also retired (mirrors core removal). | +| IV. Performance — vectorized numpy, O(n) memory | PASS | No new sequence-processing paths introduced. | +| V. Simplicity — YAGNI, no backwards-compat shims | PASS | `mode` and `is_valid_intervals_chain` are removed rather than aliased. No deprecation shim added. | + +**Constitution violations**: None. Complexity Tracking table is not required. + +## Project Structure + +### Documentation (this feature) + +```text +specs/003-cleanup-intervals-pipeline/ +├── plan.md # This file +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output (migration guide) +└── tasks.md # Phase 2 output (/speckit.tasks - NOT created here) +``` + +### Source Code (repository root) + +Files **removed** from source tree: + +```text +src/foapy/core/_is_valid_intervals_chain.py ← DELETE +src/foapy/core/_mode.py ← DELETE +src/foapy/core/_intervals.py ← MOVE to test helper +src/foapy/ma/_intervals.py ← MOVE to test helper +``` + +Files **modified** in source tree: + +```text +src/foapy/__init__.py ← remove intervals, mode, is_valid_intervals_chain imports +src/foapy/core/__init__.py ← remove intervals, mode, is_valid_intervals_chain imports +src/foapy/ma/__init__.py ← remove intervals import +``` + +Test helper (new): + +```text +tests/helpers/__init__.py ← new (empty) +tests/helpers/intervals.py ← moved core intervals() function +tests/helpers/ma_intervals.py ← moved ma intervals() function +``` + +Tests **removed**: + +```text +tests/test_is_valid_intervals_chain.py ← DELETE (feature deferred; no replacement needed) +``` + +Tests **converted** (old tests transformed into equivalence verifiers using test helper): + +```text +tests/test_intervals.py ← convert: import intervals from helpers, assert vs decomposed pipeline +tests/test_ma_intervals.py ← convert: import ma intervals from helpers, assert vs decomposed pipeline +``` + +Tests **unchanged** (already verifying decomposed pipeline): + +```text +tests/test_pipeline_consistency.py ← already comprehensive; extend with edge cases if missing +tests/test_intervals_chain.py ← unchanged +tests/test_intervals_tuple.py ← unchanged +tests/test_intervals_distribution.py ← unchanged +``` + +Documentation files **removed**: + +```text +docs/references/intervals.md ← DELETE +docs/references/mode.md ← DELETE +docs/references/ma/intervals.md ← DELETE (or convert to stub pointing to new pages) +``` + +Documentation files **added**: + +```text +docs/references/intervals_chain.md +docs/references/intervals_tuple.md +docs/references/intervals_distribution.md +docs/references/chain_mode.md +docs/references/tuple_mode.md +docs/references/ma/intervals_chain.md ← already exists? check +docs/references/ma/intervals_tuple.md ← check/add +docs/references/ma/intervals_distribution.md ← check/add +``` + +`mkdocs.yml` nav updated: +- Remove `foapy.intervals`, `foapy.mode`, `ma/intervals.md` entries +- Add `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, `tuple_mode` entries +- Add ma equivalents + +**Structure Decision**: Single-project layout. All changes are within the existing `src/foapy/`, `tests/`, and `docs/` directories. + +--- + +## Phase 0: Research + +*See [research.md](./research.md).* + +--- + +## Phase 1: Design & Contracts + +*See [data-model.md](./data-model.md), [contracts/](./contracts/), [quickstart.md](./quickstart.md).* diff --git a/specs/003-cleanup-intervals-pipeline/quickstart.md b/specs/003-cleanup-intervals-pipeline/quickstart.md new file mode 100644 index 00000000..90cdc041 --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/quickstart.md @@ -0,0 +1,109 @@ +# Migration Guide: From `intervals()` to the Decomposed Pipeline + +**Branch**: `003-cleanup-intervals-pipeline` | **Date**: 2026-04-19 + +## Overview + +After this cleanup, `foapy.intervals`, `foapy.mode`, and `foapy.is_valid_intervals_chain` are no longer part of the public API. This guide shows how to update existing code to use the decomposed pipeline. + +--- + +## Step 1: Replace the import + +**Before:** +```python +from foapy import intervals, mode, binding +``` + +**After:** +```python +from foapy import intervals_chain, intervals_tuple, chain_mode, tuple_mode, binding +``` + +--- + +## Step 2: Replace the call + +Use the mode mapping table to find the correct `chain_mode` + `tuple_mode` pair for each old `mode` value. + +### `mode.lossy` → `chain_mode.boundary` + `tuple_mode.lossy` + +```python +# Before +result = intervals(X, binding.start, mode.lossy) + +# After +result = intervals_tuple( + intervals_chain(X, binding.start, chain_mode.boundary), + binding.start, + tuple_mode.lossy, +) +``` + +### `mode.normal` → `chain_mode.boundary` + `tuple_mode.normal` + +```python +# Before +result = intervals(X, binding.start, mode.normal) + +# After +result = intervals_tuple( + intervals_chain(X, binding.start, chain_mode.boundary), + binding.start, + tuple_mode.normal, +) +``` + +### `mode.cycle` → `chain_mode.cycle` + `tuple_mode.normal` + +```python +# Before +result = intervals(X, binding.start, mode.cycle) + +# After +result = intervals_tuple( + intervals_chain(X, binding.start, chain_mode.cycle), + binding.start, + tuple_mode.normal, +) +``` + +### `mode.redundant` → `chain_mode.boundary` + `tuple_mode.redundant` + +```python +# Before +result = intervals(X, binding.start, mode.redundant) + +# After +result = intervals_tuple( + intervals_chain(X, binding.start, chain_mode.boundary), + binding.start, + tuple_mode.redundant, +) +``` + +--- + +## Step 3: Remove `is_valid_intervals_chain` calls + +`is_valid_intervals_chain` has been removed (deferred feature). If your code calls it, remove the call. A replacement with a clear specification will be provided in a future release. + +--- + +## New Combinations Available + +The decomposed pipeline also enables combinations that were not possible with the old `mode` enum: + +| New combination | Meaning | +|----------------|---------| +| `chain_mode.cycle` + `tuple_mode.lossy` | Cyclic chain with boundary intervals dropped | +| `chain_mode.cycle` + `tuple_mode.redundant` | Cyclic chain with both boundary components | + +--- + +## Reference: Fundamentals Documentation + +- **`intervals_chain`** — [Intervals Chain](../docs/fundamentals/order/intervals_chain/index.md): bounded and cycled variants +- **`chain_mode`** — [Bounded](../docs/fundamentals/order/intervals_chain/bounded.md) | [Cycled](../docs/fundamentals/order/intervals_chain/cycled.md) +- **`intervals_tuple` / `tuple_mode`** — [Intervals Distribution](../docs/fundamentals/order/intervals_distribution/index.md): lossy, normal, redundant +- **`intervals_distribution`** — [Intervals Distribution index](../docs/fundamentals/order/intervals_distribution/index.md) diff --git a/specs/003-cleanup-intervals-pipeline/research.md b/specs/003-cleanup-intervals-pipeline/research.md new file mode 100644 index 00000000..cf1443d9 --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/research.md @@ -0,0 +1,85 @@ +# Research: Cleanup Intervals Pipeline + +**Branch**: `003-cleanup-intervals-pipeline` | **Date**: 2026-04-19 + +## Summary + +This feature is a cleanup/removal task. No new algorithms, patterns, or technologies are introduced. All design decisions are explicit in the spec and derived from the completed 002-decompose-intervals-pipeline work. The research below documents the decisions already made and the key facts discovered by reading the current codebase. + +--- + +## Decision 1: Test-Helper Location for the Retired `intervals()` Functions + +**Decision**: Create `tests/helpers/intervals.py` (for `foapy.core.intervals`) and `tests/helpers/ma_intervals.py` (for `foapy.ma.intervals`), both unexported from any public package. + +**Rationale**: A dedicated `tests/helpers/` directory is a standard pattern for test utilities that are not part of the library API. Placing them there makes it unambiguous that these functions are test fixtures and prevents accidental re-import from any production code path. + +**Alternatives considered**: +- Inline the old logic in `tests/test_pipeline_consistency.py` directly — rejected because the function body is non-trivial and would be duplicated if used in multiple test files. +- Keep a `_intervals_compat.py` shim in `src/foapy/core/` — rejected: the constitution prohibits backwards-compatibility shims (Principle V). + +--- + +## Decision 2: Fate of `tests/test_intervals.py` and `tests/test_ma_intervals.py` + +**Decision**: Convert both files in-place: replace `from foapy import intervals, mode` with `from tests.helpers.intervals import intervals` (and the corresponding `mode` shim if needed), so that each existing test case becomes an equivalence assertion between the helper and the decomposed pipeline. No test cases are deleted. + +**Rationale**: The spec requires that no previously-present test is silently deleted (FR-004). Converting in-place preserves test coverage intent while making the comparison explicit. The existing `tests/test_pipeline_consistency.py` already covers the same four mappings parametrically, but the original test files also cover detailed edge-case inputs that should not be lost. + +**Alternatives considered**: +- Merge everything into `test_pipeline_consistency.py` and delete the original files — rejected because it risks losing specific edge-case inputs that appear in `test_intervals.py` and `test_ma_intervals.py` but not in the parametric file. + +--- + +## Decision 3: Fate of `tests/test_is_valid_intervals_chain.py` + +**Decision**: Delete the file outright. No replacement test module is created. + +**Rationale**: `is_valid_intervals_chain` is a deferred feature (spec §1). Its stub is being removed; there is nothing for tests to verify. Keeping the test file would reference a non-existent public symbol and cause import errors. + +**Alternatives considered**: +- Move to `tests/helpers/` and skip with `pytest.mark.skip` — rejected: a skipped test for a non-existent function adds maintenance noise with zero benefit. + +--- + +## Decision 4: `foapy.ma.intervals` Retirement + +**Decision**: `src/foapy/ma/_intervals.py` is moved to `tests/helpers/ma_intervals.py` and removed from `foapy.ma.__init__`. The `tests/test_ma_intervals.py` file is converted to import from the test helper and assert equivalence against `foapy.ma.intervals_tuple(foapy.ma.intervals_chain(...), ...)`. + +**Rationale**: The user's spec scopes the removal to `src/foapy/core/_intervals.py` explicitly, but `foapy.ma` mirrors every core change (constitution Principle III: "`foapy.ma` MUST mirror every public function in `foapy.core`"). Since `intervals` is removed from core, it must also be removed from `foapy.ma`. + +**Alternatives considered**: +- Keep `foapy.ma.intervals` while removing `foapy.core.intervals` — rejected: Principle III requires mirroring; an asymmetric API would be a constitution violation. + +--- + +## Decision 5: Documentation Structure + +**Decision**: Add one reference page per new public name: `intervals_chain.md`, `intervals_tuple.md`, `intervals_distribution.md`, `chain_mode.md`, `tuple_mode.md` under `docs/references/`. Mirror for `foapy.ma` (`ma/intervals_chain.md`, `ma/intervals_tuple.md`, `ma/intervals_distribution.md`). Remove `intervals.md` and `mode.md`. Update `mkdocs.yml` nav accordingly. Each new page uses the `:::foapy.` mkdocstrings directive and links to the corresponding fundamentals section. + +**Mapping of public names to fundamentals pages**: + +| Public name | Fundamentals link | +|-------------|------------------| +| `intervals_chain` | `fundamentals/order/intervals_chain/` | +| `chain_mode` | `fundamentals/order/intervals_chain/bounded.md` and `fundamentals/order/intervals_chain/cycled.md` | +| `intervals_tuple` | `fundamentals/order/intervals_distribution/` | +| `tuple_mode` | `fundamentals/order/intervals_distribution/lossy.md` and `fundamentals/order/intervals_distribution/redundant.md` | +| `intervals_distribution` | `fundamentals/order/intervals_distribution/index.md` | + +**Rationale**: Each reference page is a thin mkdocstrings directive that auto-generates content from docstrings. The fundamentals link anchors the function in its mathematical context. + +**Alternatives considered**: +- A single combined page for all five names — rejected: the existing pattern (one page per public symbol) is established in `docs/references/` and must be followed for consistency. + +--- + +## Key Codebase Facts Discovered + +- `src/foapy/__init__.py` imports `intervals`, `mode`, and `is_valid_intervals_chain` from `foapy.core`; all three must be removed from this file. +- `src/foapy/core/__init__.py` imports `_intervals`, `_mode`, and `_is_valid_intervals_chain`; all three must be removed. +- `src/foapy/ma/__init__.py` imports `_intervals`; this must be removed. +- `src/foapy/ma/_intervals.py` imports `from foapy import mode as mode_enum`; this dependency on the retired `mode` is another reason to move it out of the production source tree. +- `tests/test_pipeline_consistency.py` already exists and imports `intervals` from `foapy` for comparison; after this cleanup it must import from the test helper instead. +- `docs/references/ma/` currently only has `alphabet.md`, `index.md`, `intervals.md`, `order.md` — no pages yet for `intervals_chain`, `intervals_tuple`, `intervals_distribution`; all three must be added. +- `mkdocs.yml` nav references `references/intervals.md`, `references/mode.md`, and `references/ma/intervals.md`; all three must be replaced. diff --git a/specs/003-cleanup-intervals-pipeline/spec.md b/specs/003-cleanup-intervals-pipeline/spec.md new file mode 100644 index 00000000..f8b5c5ba --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/spec.md @@ -0,0 +1,129 @@ +# Feature Specification: Cleanup Intervals Pipeline + +**Feature Branch**: `003-cleanup-intervals-pipeline` +**Created**: 2026-04-19 +**Status**: Draft +**Input**: User description: "Update 002-decompose-intervals-pipeline. 1. Remove '_is_valid_intervals_chain.py' - we will implement it in future. This is now poorly defined. 2. Remove '_mode.py' - due it is decomposed into tuple_mode and chain_mode. 3. Remove '_intervals.py' from the core package. Move it to tests interval function. Keep intervals related tests to clarify that intervals_chain -> intervals_tuple = intervals in all possible parameters inputs. Keep intervals benchmarks replacing intervals function with composition interval_chain -> intervals_tuple. 4. Update documentation. Remove interval method. Added intervals_chain intervals_tuple intervals_distribution and related mode. Link it related with corresponding foundation subjects." + +## Context + +This feature finalises the cleanup from **002-decompose-intervals-pipeline**, which added the `intervals_chain`, `intervals_tuple`, and `intervals_distribution` primitives. Now that the decomposed pipeline is implemented, three interim artefacts must be retired and documentation must be updated to reflect the new canonical pipeline. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Public API Contains Only the Decomposed Pipeline (Priority: P1) + +A library user looking at the `foapy` public namespace expects to find only `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` for interval-related work. The old `intervals()` function, the unified `mode` enum, and the `is_valid_intervals_chain` stub are no longer part of the public API. Users who relied on `intervals()` can replicate its behaviour with the decomposed pipeline using a mapping that is documented and verified by the test suite. + +**Why this priority**: A clean public API with no deprecated or incomplete functions reduces confusion for new adopters and aligns the library surface with the fundamentals documentation. Removing ill-defined stubs prevents users from depending on behaviour that will change. + +**Independent Test**: Can be fully tested by importing `foapy` and verifying that `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` are importable; that `intervals`, `mode`, and `is_valid_intervals_chain` are not importable from the top-level namespace; and that any attempt to import the removed names raises `ImportError`. + +**Acceptance Scenarios**: + +1. **Given** a fresh Python environment, **When** a user imports `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` from `foapy`, **Then** all five names resolve without error. +2. **Given** a fresh Python environment, **When** a user attempts to import `intervals` or `mode` or `is_valid_intervals_chain` from `foapy`, **Then** each raises `ImportError`. +3. **Given** existing code that calls `intervals(X, binding, mode.lossy)`, **When** it is replaced with the documented equivalent `intervals_tuple(intervals_chain(X, binding, chain_mode.boundary), binding, tuple_mode.lossy)`, **Then** the output is identical. + +--- + +### User Story 2 - Pipeline Equivalence Is Verified by the Test Suite (Priority: P1) + +A library maintainer and a library user adopting the decomposed pipeline both need assurance that the composition `intervals_chain → intervals_tuple` reproduces the former `intervals()` output for every binding direction and mode combination. This assurance lives in the test suite as a dedicated test module: a test helper provides the `intervals(X, binding, mode)` function for comparison purposes only (it is not part of the public API), and parametrised tests verify equivalence for all four mode mappings across representative inputs. + +**Why this priority**: Without automated verification of pipeline equivalence, regressions could silently break characteristics results for users who migrated to the new API. This test module is the executable specification of the mode-mapping contract. + +**Independent Test**: Can be fully tested by running the dedicated equivalence test module, which imports `intervals_chain`, `intervals_tuple`, and the test-only `intervals` helper, and asserts identical output for each of the four mode mappings on all parametrised inputs. + +**Acceptance Scenarios**: + +1. **Given** any 1-D sequence, any binding direction, and `mode.lossy`, **When** the equivalence test runs, **Then** `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.lossy)` equals the test-helper `intervals(X, b, mode.lossy)` result. +2. **Given** any 1-D sequence, any binding direction, and `mode.normal`, **When** the equivalence test runs, **Then** `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.normal)` equals the test-helper `intervals(X, b, mode.normal)` result. +3. **Given** any 1-D sequence, any binding direction, and `mode.cycle`, **When** the equivalence test runs, **Then** `intervals_tuple(intervals_chain(X, b, chain_mode.cycle), b, tuple_mode.normal)` equals the test-helper `intervals(X, b, mode.cycle)` result. +4. **Given** any 1-D sequence, any binding direction, and `mode.redundant`, **When** the equivalence test runs, **Then** `intervals_tuple(intervals_chain(X, b, chain_mode.boundary), b, tuple_mode.redundant)` equals the test-helper `intervals(X, b, mode.redundant)` result. +5. **Given** edge-case inputs (empty sequence, single element, all-unique elements, all-identical elements), **When** the equivalence tests run, **Then** all four mode mappings pass for each edge case. + +--- + +### User Story 3 - Performance Benchmarks Use the Decomposed Pipeline (Priority: P2) + +A library user evaluating the performance of interval computation needs benchmark numbers that reflect the public API they will actually call. The benchmarks must measure the composition `intervals_chain → intervals_tuple` rather than the retired `intervals()` function, and must cover all four mode mappings at small, medium, and large input sizes. + +**Why this priority**: Benchmarks built on a retired function give misleading performance data. Users comparing throughput across library versions need benchmarks that match the current public API. + +**Independent Test**: Can be fully tested by running the benchmark suite and confirming that: no benchmark calls the retired `intervals()` function; each benchmark exercises `intervals_chain` followed by `intervals_tuple`; results are reported for all four mode mappings at the three input sizes. + +**Acceptance Scenarios**: + +1. **Given** the benchmark suite, **When** it is run, **Then** no benchmark invokes the retired `intervals()` function. +2. **Given** the benchmark suite, **When** it is run for each mode mapping (lossy, normal, cycle, redundant), **Then** execution time is reported at small (≤100 elements), medium (≤10,000 elements), and large (≤1,000,000 elements) input sizes. +3. **Given** the benchmark suite, **When** it is run, **Then** it completes without error and the output is reproducible across runs on the same machine. + +--- + +### User Story 4 - Documentation Describes Only the Decomposed Pipeline (Priority: P2) + +A library user reading the documentation wants to learn how to compute intervals for their sequence. The documentation describes `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode`; does not mention `intervals()` as a callable; and links each function and enum to the relevant section in the fundamentals documentation so users understand the mathematical motivation. + +**Why this priority**: Documentation that still references a retired function misleads users into calling a function that no longer exists. Linking to the fundamentals section anchors each function in the mathematical theory, reducing support questions. + +**Independent Test**: Can be fully tested by reviewing the documentation to confirm: `intervals()` appears only as a historical reference (if at all) rather than a callable API entry; each of the five public names has a documented entry; each entry links to the corresponding foundation subject. + +**Acceptance Scenarios**: + +1. **Given** the API reference documentation, **When** a user browses it, **Then** `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` each have a documented entry with description, parameters, return value, and an example. +2. **Given** the API reference documentation, **When** a user searches for `intervals()` as a standalone function, **Then** it does not appear as a callable API entry. +3. **Given** each function and enum documentation page, **When** a user reads it, **Then** at least one link leads to the corresponding section in the fundamentals documentation. +4. **Given** the documentation for `chain_mode` and `tuple_mode`, **When** a user reads them, **Then** the difference between the two enums is clearly explained with reference to the pipeline stage each controls. + +--- + +### Edge Cases + +- What happens when a user imports `intervals` from `foapy` after this cleanup? The import raises `ImportError`. +- What happens when a user imports `mode` from `foapy` after this cleanup? The import raises `ImportError`. +- What happens when a user imports `is_valid_intervals_chain` from `foapy` after this cleanup? The import raises `ImportError`. +- How does the equivalence test handle sequences with no repeated elements? Each result is an empty array for `lossy` mode and a boundary-only array for other modes; both pipeline paths must agree. +- What if an existing external codebase imports `intervals` directly from `foapy.core`? That import path also breaks; the test-only helper is not re-exported from any public module. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The `intervals` function, the unified `mode` enum, and the `is_valid_intervals_chain` stub MUST NOT be importable from the `foapy` top-level namespace after this cleanup; attempting to do so MUST raise `ImportError`. +- **FR-002**: The `intervals` function MUST be moved to a test-only helper module that is not part of the public package; it MUST NOT be re-exported from any public `foapy` module or subpackage. +- **FR-003**: A dedicated test module MUST exist that imports the test-only `intervals` helper and verifies that the four-mode equivalence `intervals_chain → intervals_tuple = intervals` holds for all four mode mappings, all binding directions, and representative edge-case inputs. +- **FR-004**: Every existing test that validated `intervals()` behaviour MUST be converted to: (a) a test verifying the decomposed pipeline (`intervals_chain → intervals_tuple`) produces the same result, or (b) a test using the test-only helper in the dedicated equivalence test module; no test that was previously present MUST be silently deleted. +- **FR-005**: The benchmark suite MUST replace all calls to `intervals()` with the equivalent `intervals_chain → intervals_tuple` composition for the corresponding mode mapping; coverage MUST include all four mode mappings at small, medium, and large input sizes. +- **FR-006**: The `foapy` public namespace MUST continue to export `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` without change. +- **FR-007**: The documentation MUST be updated so that `intervals()` does not appear as an API callable; each of `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, and `tuple_mode` MUST have a documentation entry with description, parameters, return value, and at least one example. +- **FR-008**: Each documentation entry for the five public names MUST contain at least one link to the corresponding section in the fundamentals documentation. +- **FR-009**: The `mode` enum and `is_valid_intervals_chain` stub MUST be removed from the source tree; no orphan file or import reference to either MUST remain. + +### Key Entities + +- **Test-only `intervals` helper**: The retired `intervals()` function preserved in the test suite solely to drive equivalence comparisons; not importable from any public namespace. +- **Equivalence test module**: A dedicated test file asserting that `intervals_chain → intervals_tuple` matches the test-only `intervals` helper for all four mode mappings across all binding directions and edge-case inputs. +- **Mode mapping table**: The four canonical correspondences — `mode.lossy ↔ chain_mode.boundary + tuple_mode.lossy`, `mode.normal ↔ chain_mode.boundary + tuple_mode.normal`, `mode.cycle ↔ chain_mode.cycle + tuple_mode.normal`, `mode.redundant ↔ chain_mode.boundary + tuple_mode.redundant` — documented and verified by the equivalence test module. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Attempting to import `intervals`, `mode`, or `is_valid_intervals_chain` from `foapy` raises `ImportError` in 100% of test runs after this cleanup. +- **SC-002**: The equivalence test module passes for all four mode mappings, both binding directions, and all edge-case inputs — 0 failures. +- **SC-003**: All characteristics that previously accepted `intervals()` output continue to produce the same results when given `intervals_tuple` output for the same input — zero regressions in the existing characteristics test suite. +- **SC-004**: The benchmark suite runs to completion with no calls to the retired `intervals()` function, covering all four mode mappings at three input sizes. +- **SC-005**: Each of the five public names (`intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, `tuple_mode`) has a documentation entry containing description, parameters, return value, and at least one example with a link to the fundamentals documentation. +- **SC-006**: No orphan import references to `_mode.py`, `_is_valid_intervals_chain.py`, or `_intervals.py` remain anywhere in the source tree or test suite (excluding the test-only helper module itself). + +## Assumptions + +- The decomposed pipeline (`intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, `tuple_mode`) was fully implemented in 002-decompose-intervals-pipeline and is already available; this feature does not add new algorithmic functionality. +- Removing `intervals()` from the public API is a deliberate breaking change for any external caller; there is no deprecation grace period — the function moves directly to a test-only helper. +- The `mode` enum is superseded entirely by `chain_mode` and `tuple_mode`; it is removed from the source tree rather than kept as an alias. +- `is_valid_intervals_chain` is deferred to a future feature with a clearer definition; its stub file is removed rather than left as a placeholder. +- The test-only `intervals` helper retains the original four-mode logic unmodified so that equivalence tests serve as a regression safety net; the helper is not subject to the library's public API stability guarantees. +- Documentation updates cover the `foapy` public API reference pages; the fundamentals documentation pages themselves are already written and only require linking from the function/enum entries. +- Masked-array variants (`foapy.ma`) for the five public names are unchanged by this feature. +- Test conventions continue to follow the existing `CharacteristicsTest`-style base classes and `AssertBatch` helpers where applicable. diff --git a/specs/003-cleanup-intervals-pipeline/tasks.md b/specs/003-cleanup-intervals-pipeline/tasks.md new file mode 100644 index 00000000..eea73bbe --- /dev/null +++ b/specs/003-cleanup-intervals-pipeline/tasks.md @@ -0,0 +1,213 @@ +# Tasks: Cleanup Intervals Pipeline + +**Input**: Design documents from `/specs/003-cleanup-intervals-pipeline/` +**Prerequisites**: plan.md ✅ spec.md ✅ research.md ✅ data-model.md ✅ contracts/ ✅ quickstart.md ✅ + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks) +- **[Story]**: Which user story this task belongs to +- Exact file paths are included in every description + +--- + +## Phase 1: Setup + +**Purpose**: Create the test-helper directory that will house the retired `intervals()` functions. This is a prerequisite for Phases 2+ and must be completed first. + +- [x] T001 Create `tests/helpers/__init__.py` as an empty module marker + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Populate the test helpers with the retired `intervals()` functions before any source files are removed. Both helpers must exist before the source removals in US1. + +**⚠️ CRITICAL**: US1 source-file deletions must not happen until T002 and T003 are complete. + +- [x] T002 Create `tests/helpers/intervals.py`: copy `intervals()` function body verbatim from `src/foapy/core/_intervals.py` and copy `mode` enum definition verbatim from `src/foapy/core/_mode.py`; add module docstring noting this is a test-only helper not part of the public API; ensure all imports are self-contained (no `from foapy import ...` references to the retiring names) +- [x] T003 [P] Create `tests/helpers/ma_intervals.py`: copy `intervals()` function body verbatim from `src/foapy/ma/_intervals.py`; replace `from foapy import mode as mode_enum` with a local import from `tests.helpers.intervals`; add module docstring noting test-only status + +**Checkpoint**: Both test helpers in place and importable — source removal can now proceed. + +--- + +## Phase 3: User Story 1 — Public API Contains Only the Decomposed Pipeline (Priority: P1) 🎯 MVP + +**Goal**: Remove `intervals`, `mode`, and `is_valid_intervals_chain` from every public namespace so that any attempt to import them from `foapy` raises `ImportError`. + +**Independent Test**: `python -c "from foapy import intervals"` raises `ImportError`; `python -c "from foapy import intervals_chain, intervals_tuple, intervals_distribution, chain_mode, tuple_mode"` succeeds. + +### Implementation for User Story 1 + +- [x] T004 [P] [US1] Update `src/foapy/core/__init__.py`: remove the three import lines for `_mode`, `_intervals`, `_is_valid_intervals_chain`; remove `"mode"`, `"intervals"`, `"is_valid_intervals_chain"` from `__all__` +- [x] T005 [P] [US1] Update `src/foapy/__init__.py`: remove `from foapy.core import intervals`, `from foapy.core import mode`, `from foapy.core import is_valid_intervals_chain`; remove all three names from `__all__`; remove `"intervals"`, `"mode"` from the `__dir__` inline set +- [x] T006 [P] [US1] Update `src/foapy/ma/__init__.py`: remove `from ._intervals import intervals`; remove `"intervals"` from `__all__` +- [x] T007 [US1] Delete `src/foapy/core/_is_valid_intervals_chain.py` (after T004 complete) +- [x] T008 [P] [US1] Delete `src/foapy/core/_mode.py` (after T004 complete — parallel with T007) +- [x] T009 [P] [US1] Delete `src/foapy/core/_intervals.py` (after T004 complete — parallel with T007, T008) +- [x] T010 [US1] Delete `src/foapy/ma/_intervals.py` (after T006 complete) +- [x] T011 [P] [US1] Delete `tests/test_is_valid_intervals_chain.py` (no source to import; safe to delete independently) + +**Checkpoint**: `from foapy import intervals` raises `ImportError`; `from foapy import intervals_chain, intervals_tuple, intervals_distribution, chain_mode, tuple_mode` succeeds; `tox -e default -- -k "not test_intervals and not test_ma_intervals and not test_pipeline" -q` passes. + +--- + +## Phase 4: User Story 2 — Pipeline Equivalence Is Verified by the Test Suite (Priority: P1) + +**Goal**: Convert the three affected test files to import `intervals` from the test helper instead of from `foapy`, so each test case becomes a verified equivalence assertion between the helper and the decomposed pipeline. + +**Independent Test**: `tox -e default -- tests/test_intervals.py tests/test_ma_intervals.py tests/test_pipeline_consistency.py -v` passes with zero failures. + +### Implementation for User Story 2 + +- [x] T012 [US2] Update `tests/test_intervals.py`: replace `from foapy import binding, intervals, mode` with `from foapy import binding` and `from tests.helpers.intervals import intervals, mode`; no test case bodies need to change — the helper provides identical behaviour +- [x] T013 [P] [US2] Update `tests/test_ma_intervals.py`: replace `from foapy.ma import intervals` with `from tests.helpers.ma_intervals import intervals`; keep all test bodies unchanged +- [x] T014 [P] [US2] Update `tests/test_pipeline_consistency.py`: replace `from foapy import binding, chain_mode, intervals, mode` with `from foapy import binding, chain_mode` and `from tests.helpers.intervals import intervals, mode`; keep all existing assertions; verify the file already covers all four mode mappings for both binding directions and all edge cases (empty, single-element, all-unique, all-same); add any missing edge-case tests to achieve full coverage + +**Checkpoint**: `tox -e default -- tests/test_intervals.py tests/test_ma_intervals.py tests/test_pipeline_consistency.py -v` passes with zero failures. + +--- + +## Phase 5: User Story 3 — Performance Benchmarks Use the Decomposed Pipeline (Priority: P2) + +**Goal**: Replace the retired `intervals()` calls in the two benchmark files with the equivalent `intervals_chain → intervals_tuple` composition so that benchmarks measure the current public API. + +**Independent Test**: `asv run --quick` (or the equivalent quick-benchmark command) completes with no import errors; no benchmark file contains `from foapy import intervals` or `from foapy.ma import intervals`. + +### Implementation for User Story 3 + +- [x] T015 [US3] Rewrite `benchmarks/benchmarks/bench_intervals.py`: replace `from foapy import intervals` with `from foapy import binding as binding_enum, chain_mode, intervals_chain, intervals_tuple, tuple_mode`; update `IntervalsSuite` so the four `mode` integer params (1–4) map to the correct `(chain_mode, tuple_mode)` pairs in `setup()`; update `time_intervals` and `peakmem_intervals` to call `intervals_tuple(intervals_chain(self.data, self.binding, self.chain_mode), self.binding, self.tuple_mode)`; preserve all `skip_params_if` and `timeout` settings +- [x] T016 [P] [US3] Rewrite `benchmarks/benchmarks/bench_ma_intervals.py`: same pattern as T015 but import from `foapy.ma` for `intervals_chain`, `intervals_tuple`, and update `MaIntervalsSuite` accordingly; preserve all existing skip/timeout settings + +**Checkpoint**: Grep `benchmarks/benchmarks/` for `from foapy import intervals` and `from foapy.ma import intervals` returns no results. + +--- + +## Phase 6: User Story 4 — Documentation Describes Only the Decomposed Pipeline (Priority: P2) + +**Goal**: Remove retired reference pages, add new pages for the five public names plus their `foapy.ma` mirrors, and update `mkdocs.yml` nav so the documentation reflects the current public API with links to the fundamentals pages. + +**Independent Test**: `tox -e docs` builds without errors; no `foapy.intervals` or `foapy.mode` entries appear in the rendered nav. + +### Implementation for User Story 4 + +- [x] T017 [P] [US4] Delete `docs/references/intervals.md` and `docs/references/mode.md` +- [x] T018 [P] [US4] Create `docs/references/intervals_chain.md`: use `:::foapy.intervals_chain` mkdocstrings directive with the same options pattern as `docs/references/binding.md`; add a "See also" link to `fundamentals/order/intervals_chain/index.md` (bounded) and `fundamentals/order/intervals_chain/cycled.md` (cycled) +- [x] T019 [P] [US4] Create `docs/references/intervals_tuple.md`: use `:::foapy.intervals_tuple` directive; add "See also" link to `fundamentals/order/intervals_distribution/index.md` +- [x] T020 [P] [US4] Create `docs/references/intervals_distribution.md`: use `:::foapy.intervals_distribution` directive; add "See also" link to `fundamentals/order/intervals_distribution/index.md` +- [x] T021 [P] [US4] Create `docs/references/chain_mode.md`: use `:::foapy.chain_mode` directive with same options as `docs/references/binding.md`; add "See also" links to `fundamentals/order/intervals_chain/bounded.md` and `fundamentals/order/intervals_chain/cycled.md` +- [x] T022 [P] [US4] Create `docs/references/tuple_mode.md`: use `:::foapy.tuple_mode` directive with same options as `docs/references/binding.md`; add "See also" links to `fundamentals/order/intervals_distribution/lossy.md` and `fundamentals/order/intervals_distribution/redundant.md` +- [x] T023 [P] [US4] Delete `docs/references/ma/intervals.md`; create `docs/references/ma/intervals_chain.md`, `docs/references/ma/intervals_tuple.md`, and `docs/references/ma/intervals_distribution.md` — each using `:::foapy.ma.` directive mirroring the core counterpart format +- [x] T024 [US4] Update `mkdocs.yml` nav References section: remove `"foapy.intervals": references/intervals.md`, `"foapy.mode": references/mode.md`, and `"intervals": references/ma/intervals.md` entries; add entries for `intervals_chain`, `intervals_tuple`, `intervals_distribution`, `chain_mode`, `tuple_mode` under core references and their `ma/` mirrors under the ma section + +**Checkpoint**: `tox -e docs` builds without warnings or errors; all five new core pages and three new ma pages render correctly. + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Purpose**: Verify quality gates pass, confirm no orphan references remain, and ensure the codebase is clean. + +- [x] T025 Run `tox -e default` and confirm zero test failures; fix any remaining failures before marking complete +- [x] T026 [P] Run `pipx run pre-commit run --all-files --show-diff-on-failure` and fix any lint errors (black, isort, flake8) +- [x] T027 [P] Grep the source tree for orphan references: `grep -r "from foapy import intervals\b\|from foapy.core import intervals\b\|from foapy.ma import intervals\b\|is_valid_intervals_chain\|from foapy import mode\b\|_mode\b\|_intervals\b" src/ tests/ benchmarks/ docs/ --include="*.py" --include="*.md"`; confirm zero results outside `tests/helpers/` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — start immediately +- **Foundational (Phase 2)**: Depends on Phase 1 — BLOCKS US1 source deletions +- **US1 (Phase 3)**: Depends on Phase 2 (helpers must exist before source is removed) +- **US2 (Phase 4)**: Depends on Phase 2 (helpers) + Phase 3 (source removed; imports changed) +- **US3 (Phase 5)**: Depends on Phase 3 (source removed; `foapy.intervals` no longer importable) +- **US4 (Phase 6)**: Can begin in parallel with Phase 3 (doc files are independent of source) +- **Polish (Phase 7)**: Depends on all prior phases complete + +### User Story Dependencies + +- **US1 (P1)**: Requires Phase 2 complete; no other story dependency +- **US2 (P1)**: Requires Phase 2 + US1 complete +- **US3 (P2)**: Requires US1 complete; independent of US2 +- **US4 (P2)**: Independent of US1/US2/US3 (doc files only); can proceed in parallel from Phase 3 onwards + +### Within Each Phase + +- T004, T005, T006 in Phase 3 can run in parallel (different files) +- T007, T008, T009 can run in parallel after T004 completes +- T010 runs after T006 completes +- T012, T013, T014 in Phase 4 can run in parallel +- T015, T016 in Phase 5 can run in parallel +- T017–T023 in Phase 6 can all run in parallel; T024 runs after T017–T023 + +--- + +## Parallel Example: Phase 3 (US1) + +``` +# After Phase 2 completes, launch in parallel: +T004: Update src/foapy/core/__init__.py +T005: Update src/foapy/__init__.py +T006: Update src/foapy/ma/__init__.py +T011: Delete tests/test_is_valid_intervals_chain.py + +# After T004 completes, launch in parallel: +T007: Delete src/foapy/core/_is_valid_intervals_chain.py +T008: Delete src/foapy/core/_mode.py +T009: Delete src/foapy/core/_intervals.py + +# After T006 completes: +T010: Delete src/foapy/ma/_intervals.py +``` + +## Parallel Example: Phase 6 (US4) + +``` +# All of these can run in parallel: +T017: Delete docs/references/intervals.md and mode.md +T018: Create docs/references/intervals_chain.md +T019: Create docs/references/intervals_tuple.md +T020: Create docs/references/intervals_distribution.md +T021: Create docs/references/chain_mode.md +T022: Create docs/references/tuple_mode.md +T023: Delete+create docs/references/ma/ files + +# After T017–T023 complete: +T024: Update mkdocs.yml nav +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (T001) +2. Complete Phase 2: Foundational (T002–T003) +3. Complete Phase 3: US1 (T004–T011) +4. **STOP and VALIDATE**: `python -c "from foapy import intervals"` raises `ImportError`; `from foapy import intervals_chain, intervals_tuple, intervals_distribution, chain_mode, tuple_mode` succeeds +5. Then continue with US2, US3, US4 + +### Incremental Delivery + +1. Phase 1 + 2: Helpers in place +2. Phase 3 (US1): API cleaned → test that removed names raise `ImportError` +3. Phase 4 (US2): Equivalence tests converted → `tox -e default` green +4. Phase 5 (US3): Benchmarks updated → no `intervals` import in benchmarks +5. Phase 6 (US4): Docs updated → `tox -e docs` green +6. Phase 7: Polish → all quality gates pass + +--- + +## Notes + +- **[P]** tasks touch different files and have no dependencies on incomplete tasks in the same phase +- `tests/helpers/` is accessible as a Python package only within the test suite; it is never imported from `src/` +- The `mode` enum values map to `chain_mode + tuple_mode` as documented in `contracts/api-changes.md` +- Commit logical groups: helpers, init updates, source deletions, test conversions, benchmarks, docs, polish +- Run `tox -e default -- -k -v` after each phase to catch regressions early diff --git a/src/foapy/__init__.py b/src/foapy/__init__.py index 9c346789..6d714b9a 100644 --- a/src/foapy/__init__.py +++ b/src/foapy/__init__.py @@ -29,12 +29,9 @@ from foapy.core import alphabet # noqa: F401 from foapy.core import binding # noqa: F401 from foapy.core import chain_mode # noqa: F401 - from foapy.core import intervals # noqa: F401 from foapy.core import intervals_chain # noqa: F401 from foapy.core import intervals_distribution # noqa: F401 from foapy.core import intervals_tuple # noqa: F401 - from foapy.core import is_valid_intervals_chain # noqa: F401 - from foapy.core import mode # noqa: F401 from foapy.core import order # noqa: F401 from foapy.core import tuple_mode # noqa: F401 @@ -48,15 +45,12 @@ __foapy_submodules__ | { "order", - "intervals", "intervals_chain", "intervals_distribution", "intervals_tuple", - "is_valid_intervals_chain", "alphabet", "binding", "chain_mode", - "mode", "tuple_mode", } | {"__version__", "__array_namespace_info__"} @@ -89,12 +83,11 @@ def __getattr__(attr): def __dir__(): public_symbols = globals().keys() | __foapy_submodules__ public_symbols += { - "exceptions" "ma", + "exceptions", + "ma", "order", - "intervals", "alphabet", "binding", - "mode", "version", } return list(public_symbols) diff --git a/src/foapy/core/__init__.py b/src/foapy/core/__init__.py index 00a9fcb2..c10aa926 100644 --- a/src/foapy/core/__init__.py +++ b/src/foapy/core/__init__.py @@ -14,13 +14,10 @@ from ._alphabet import alphabet # noqa: F401 from ._binding import binding # noqa: F401 from ._chain_mode import chain_mode # noqa: F401 - from ._mode import mode # noqa: F401 from ._tuple_mode import tuple_mode # noqa: F401 - from ._intervals import intervals # noqa: F401 from ._intervals_chain import intervals_chain # noqa: F401 from ._intervals_distribution import intervals_distribution # noqa: F401 from ._intervals_tuple import intervals_tuple # noqa: F401 - from ._is_valid_intervals_chain import is_valid_intervals_chain # noqa: F401 from ._order import order # noqa: F401 # isort: on @@ -29,13 +26,10 @@ { "binding", "chain_mode", - "mode", "tuple_mode", - "intervals", "intervals_chain", "intervals_distribution", "intervals_tuple", - "is_valid_intervals_chain", "order", "alphabet", } diff --git a/src/foapy/core/_intervals.py b/src/foapy/core/_intervals.py deleted file mode 100644 index eb735ee7..00000000 --- a/src/foapy/core/_intervals.py +++ /dev/null @@ -1,207 +0,0 @@ -from numpy import ndarray - -from foapy.core import mode as constants_mode -from foapy.core._chain_mode import chain_mode -from foapy.core._intervals_chain import intervals_chain -from foapy.core._intervals_tuple import intervals_tuple -from foapy.core._tuple_mode import tuple_mode - - -def intervals(X, binding: int, mode: int) -> ndarray: - """ - Function to extract intervals from a sequence. - - An interval is defined as the distance between consecutive occurrences - of the similar elements in the sequence, with boundary intervals counted as positions - from sequence edges to first/last occurrence. The intervals are extracted - based on the specified binding direction ([start][foapy.binding.start] or [end][foapy.binding.end]) - and mode ([normal][foapy.mode.normal], [lossy][foapy.mode.lossy], [cycle][foapy.mode.cycle], or [redundant][foapy.mode.redundant]). - - Example how intervals are extracted from the sequence with **"binding.start"** and **"mode.normal"**. - - | **X** | b | a | b | c | b | - |:-------------:|:------:|:------:|:------:|:------:|:-----:| - | b | 1 | -> | 2 | -> | 2 | - | a | -> | 2 | | | | - | c | -> | -> | -> | 4 | | - | **intervals** | **1** | **2** | **2** | **4** | **2** | - - - Parameters - ---------- - X: array_like - Array to exctact an intervals from. Must be a 1-dimensional array. - binding: int - [start][foapy.binding.start] = 1 - Intervals are extracted from left to right. - [end][foapy.binding.end] = 2 – Intervals are extracted from right to left. - mode: int - Mode handling the intervals at the sequence boundaries: - - [lossy][foapy.mode.lossy] = 1 - Both interval from the start of the sequence - to the first element occurrence and interval from the - last element occurrence to the end of the sequence - are not taken into account. - - [normal][foapy.mode.normal] = 2 - Interval from the start of the sequence to - the first occurrence of the element - (in case of binding to the beginning) - or interval from the last occurrence of the element to - the end of the sequence - (in case of binding to the end) is taken into account. - - [cycle][foapy.mode.cycle] = 3 - Interval from the start of the sequence to - the first element occurrence - and interval from the last element occurrence to the - end of the sequence are summed - into one interval (as if sequence was cyclic). - Interval is placed either in the beginning of - intervals array (in case of binding to the beginning) - or in the end (in case of binding to the end). - - [redundant][foapy.mode.redundant] = 4 - Both interval from start of the sequence - to the first element occurrence and the interval from - the last element occurrence to the end of the - sequence are taken into account. Their placement in results - array is determined by the binding. - - Returns - ------- - order : ndarray - Intervals extracted from the sequence - - Raises - ------- - Not1DArrayException - When X parameter is not a 1-dimensional array - - ValueError - When binding or mode is not valid - - Examples - -------- - - Get intervals from a sequence binding to [start][foapy.binding.start] and mode [normal][foapy.mode.normal]. - - ``` py linenums="1" - import foapy - - source = ['a', 'b', 'a', 'c', 'a', 'd'] - intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal) - print(intervals) - # [1 2 2 3 2 5] - ``` - - Get intervals from a emprty sequence. - ``` py linenums="1" - import foapy - - source = [] - intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal) - print(intervals) - # [] - ``` - - Getting an intervals of an array with more than 1 dimension is not allowed. - ``` py linenums="1" - import foapy - source = [[1, 2], [3, 4]] - intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal) - # Not1DArrayException: - # {'message': 'Incorrect array form. Expected d1 array, exists 2'} - ``` - """ # noqa: E501 - - # Validate mode - valid_modes = [ - constants_mode.lossy, - constants_mode.normal, - constants_mode.cycle, - constants_mode.redundant, - ] - if mode not in valid_modes: - raise ValueError( - {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."} - ) - - if mode == constants_mode.normal: - return intervals_chain(X, binding, chain_mode.boundary) - - if mode == constants_mode.cycle: - return intervals_chain(X, binding, chain_mode.cycle) - - if mode == constants_mode.lossy: - return intervals_tuple( - intervals_chain(X, binding, chain_mode.boundary), binding, tuple_mode.lossy - ) - - if mode == constants_mode.redundant: - return intervals_tuple( - intervals_chain(X, binding, chain_mode.boundary), - binding, - tuple_mode.redundant, - ) - - # # Validate binding - # if binding not in {constants_binding.start, constants_binding.end}: - # raise ValueError( - # {"message": "Invalid binding value. Use binding.start or binding.end."} - # ) - - # # Validate mode - # valid_modes = [ - # constants_mode.lossy, - # constants_mode.normal, - # constants_mode.cycle, - # constants_mode.redundant, - # ] - # if mode not in valid_modes: - # raise ValueError( - # {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."} - # ) - - # ar = np.asanyarray(X) - - # if ar.shape == (0,): - # return [] - - # if binding == constants_binding.end: - # ar = ar[::-1] - - # perm = ar.argsort(kind="mergesort") - - # mask_shape = ar.shape - # mask = np.empty(mask_shape[0] + 1, dtype=bool) - # mask[:1] = True - # mask[1:-1] = ar[perm[1:]] != ar[perm[:-1]] - # mask[-1:] = True # or mask[-1] = True - - # first_mask = mask[:-1] - # last_mask = mask[1:] - - # intervals = np.empty(ar.shape, dtype=np.intp) - # intervals[1:] = perm[1:] - perm[:-1] - - # delta = len(ar) - perm[last_mask] if mode == constants_mode.cycle else 1 - # intervals[first_mask] = perm[first_mask] + delta - - # inverse_perm = np.empty(ar.shape, dtype=np.intp) - # inverse_perm[perm] = np.arange(ar.shape[0]) - - # if mode == constants_mode.lossy: - # intervals[first_mask] = 0 - # intervals = intervals[inverse_perm] - # result = intervals[intervals != 0] - # elif mode == constants_mode.normal: - # result = intervals[inverse_perm] - # elif mode == constants_mode.cycle: - # result = intervals[inverse_perm] - # elif mode == constants_mode.redundant: - # result = intervals[inverse_perm] - # redundant_intervals = len(ar) - perm[last_mask] - # if binding == constants_binding.end: - # redundant_intervals = redundant_intervals[::-1] - # result = np.concatenate((result, redundant_intervals)) - # if binding == constants_binding.end: - # result = result[::-1] - - # return result diff --git a/src/foapy/core/_is_valid_intervals_chain.py b/src/foapy/core/_is_valid_intervals_chain.py deleted file mode 100644 index 85e9aee1..00000000 --- a/src/foapy/core/_is_valid_intervals_chain.py +++ /dev/null @@ -1,53 +0,0 @@ -import numpy as np - - -def is_valid_intervals_chain(chain) -> bool: - """ - Return ``True`` if *chain* is a structurally valid 1-D intervals chain. - - A valid chain satisfies all of the following conditions: - - 1. It is a 1-D array-like of integers (or empty). - 2. Every value is strictly positive (``>= 1``). - 3. Every value does not exceed the length of the chain (``<= n``). - - The function never raises; any input that cannot be interpreted as a 1-D - integer array returns ``False``. - - Parameters - ---------- - chain : array_like - The candidate intervals chain to validate. - - Returns - ------- - bool - ``True`` if *chain* is a valid 1-D intervals chain, ``False`` - otherwise. - - Examples - -------- - - ``` py linenums="1" - import numpy as np - from foapy.core import is_valid_intervals_chain - - print(is_valid_intervals_chain(np.array([1, 2, 2, 4, 2]))) # True - print(is_valid_intervals_chain(np.array([0, 1, 2]))) # False — zero - print(is_valid_intervals_chain(np.array([[1, 2], [3, 4]]))) # False — 2-D - print(is_valid_intervals_chain([])) # True — empty - ``` - """ - try: - ar = np.asanyarray(chain, dtype=np.intp) - except (TypeError, ValueError): - return False - - if ar.ndim != 1: - return False - - if ar.size == 0: - return True - - n = ar.size - return bool(np.all(ar >= 1) and np.all(ar <= n)) diff --git a/src/foapy/core/_mode.py b/src/foapy/core/_mode.py deleted file mode 100644 index e5692a08..00000000 --- a/src/foapy/core/_mode.py +++ /dev/null @@ -1,69 +0,0 @@ -class mode: - """ - Mode enumeration used to determinate handling the intervals at the sequence boundaries. - - Examples - ---------- - - See [foapy.intervals()][foapy.intervals] function for code examples using modes. - - Example how different modes handle intervals are extracted when binding.start - - === "mode.normal" - - | | b | a | b | c | b | - |:------:|:--:|:--:|:--:|:--:|:--:| - | b | 1 | -> | 2 | -> | 2 | - | a | -> | 2 | | | | - | c | -> | -> | -> | 4 | | - | result | 1 | 2 | 2 | 4 | 2 | - - === "mode.cycle" - - | | transition | a | b | c | b | b | transition | - |:------:|:----------:|:--:|:--:|:--:|:--:|:--:|:----------:| - | b | (1) | 1 | -> | 2 | -> | 2 | (1) | - | a | (2) | -> | 5 | -> | -> | -> | (2) | - | c | (3) | -> | -> | -> | 5 | -> | (3) | - | result | | 1 | 5 | 2 | 5 | 2 | | - - === "mode.lossy" - - | | b | a | b | c | b | - |:------:|:--:|:--:|:--:|:--:|:--:| - | b | x | -> | 2 | -> | 2 | - | a | -> | x | | | | - | c | -> | -> | -> | x | | - | result | | | 2 | | 2 | - - === "mode.redundant" - - | | a | b | c | b | b | end | - |:------:|:--:|:--:|:--:|:--:|:--:|:-----:| - | b | 1 | -> | 2 | -> | 2 | 1 | - | a | -> | 2 | -> | -> | -> | 4 | - | c | -> | -> | -> | 4 | -> | 2 | - | result | 1 | 2 | 2 | 4 | 2 | 1 4 2 | - - - """ # noqa: E501 - - lossy: int = 1 - """ - Ignore boundary intervals - """ - - normal: int = 2 - """ - Include first/last boundary interval based on binding - """ - - cycle: int = 3 - """ - Sumarize boundary intervals as one cyclic interval - """ - - redundant: int = 4 - """ - Include both (first and last) boundary intervals - """ diff --git a/src/foapy/ma/__init__.py b/src/foapy/ma/__init__.py index 567374b3..7c757fcb 100644 --- a/src/foapy/ma/__init__.py +++ b/src/foapy/ma/__init__.py @@ -11,7 +11,6 @@ sys.stderr.write("Running from numpy source directory.\n") else: from ._alphabet import alphabet # noqa: F401 - from ._intervals import intervals # noqa: F401 from ._intervals_chain import intervals_chain # noqa: F401 from ._intervals_distribution import intervals_distribution # noqa: F401 from ._intervals_tuple import intervals_tuple # noqa: F401 @@ -20,7 +19,6 @@ __all__ = list( { "order", - "intervals", "alphabet", "intervals_chain", "intervals_tuple", diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/helpers/intervals.py b/tests/helpers/intervals.py new file mode 100644 index 00000000..842e07d3 --- /dev/null +++ b/tests/helpers/intervals.py @@ -0,0 +1,40 @@ +# Test-only helper. Not part of the public foapy API. +# Contains the retired intervals() function and mode enum preserved +# solely for pipeline equivalence tests. +from foapy.core._chain_mode import chain_mode +from foapy.core._intervals_chain import intervals_chain +from foapy.core._intervals_tuple import intervals_tuple +from foapy.core._tuple_mode import tuple_mode + + +class mode: + lossy: int = 1 + normal: int = 2 + cycle: int = 3 + redundant: int = 4 + + +def intervals(X, binding: int, mode_value: int): + valid_modes = [mode.lossy, mode.normal, mode.cycle, mode.redundant] + if mode_value not in valid_modes: + raise ValueError( + {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."} + ) + + if mode_value == mode.normal: + return intervals_chain(X, binding, chain_mode.boundary) + + if mode_value == mode.cycle: + return intervals_chain(X, binding, chain_mode.cycle) + + if mode_value == mode.lossy: + return intervals_tuple( + intervals_chain(X, binding, chain_mode.boundary), binding, tuple_mode.lossy + ) + + if mode_value == mode.redundant: + return intervals_tuple( + intervals_chain(X, binding, chain_mode.boundary), + binding, + tuple_mode.redundant, + ) diff --git a/src/foapy/ma/_intervals.py b/tests/helpers/ma_intervals.py similarity index 50% rename from src/foapy/ma/_intervals.py rename to tests/helpers/ma_intervals.py index 26b652c8..58f2b02b 100644 --- a/src/foapy/ma/_intervals.py +++ b/tests/helpers/ma_intervals.py @@ -1,169 +1,20 @@ +# Test-only helper. Not part of the public foapy API. +# Contains the retired foapy.ma.intervals() function preserved +# solely for pipeline equivalence tests. import numpy as np +from helpers.intervals import mode as mode_enum from numpy import ma from foapy import binding as binding_enum -from foapy import mode as mode_enum from foapy.exceptions import InconsistentOrderException, Not1DArrayException def intervals(X, binding, mode): - """ - Finding array of array of intervals of the uniform - sequences in the given input sequence - - Parameters - ---------- - X: masked_array - Array to get intervals. - - binding: int - binding.start = 1 - Intervals are extracted from left to right. - binding.end = 2 – Intervals are extracted from right to left. - - mode: int - mode.lossy = 1 - Both interval from the start of the sequence - to the first element occurrence and interval from the - last element occurrence to the end of the sequence are not taken into account. - - mode.normal = 2 - Interval from the start of the sequence to the - first occurrence of the element or interval from the last occurrence - of the element to the end of the sequence is taken into account. - - mode.cycle = 3 - Interval from the start of the sequence to the first - element occurrence - and interval from the last element occurrence to the end of the - sequence are summed - into one interval (as if sequence was cyclic). Interval is - placed either in the - beginning of intervals array (in case of binding to the - beginning) or in the end. - - mode.redundant = 4 - Both interval from start of the sequence - to the first element - occurrence and the interval from the last element occurrence - to the end of the - sequence are taken into account. Their placement in results - array is determined - by the binding. - - Returns - ------- - result: array or Exception. - Exception if not d1 array or wrong mask, array otherwise. - - Examples - -------- - - ----1---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.start, mode.lossy) - >>> b - [ - [5], - [1, 4], - [], - [] - ] - - ----2---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.end, mode.lossy) - >>> b - [ - [5], - [1, 4], - [], - [] - ] - - ----3---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.start, mode.normal) - >>> b - [ - [1, 2, 1], - [2, 3] - ] - - ----4---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.end, mode.normal) - >>> b - [ - [2, 1, 2], - [3, 1] - ] - - ----5---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.start, mode.cycle) - >>> b - [ - [2, 2, 1], - [2, 3] - ] - - ----6---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.end, mode.cycle) - >>> b - [ - [2, 1, 2], - [3, 2] - ] - - ----7---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.start, mode.redunant) - >>> b - [ - [1, 2, 1, 2], - [2, 3, 1] - ] - - ----8---- - >>> import foapy.ma as ma - >>> a = [2, 4, 2, 2, 4] - >>> b = ma.intervals(X, binding.end, mode.redunant) - >>> b - [ - [1, 2, 1, 2], - [2, 3, 1] - ] - - ----9---- - >>> import foapy.ma as ma - >>> a = ['a', 'b', 'c', 'a', 'b', 'c', 'c', 'c', 'b', 'a', 'c', 'b', 'c'] - >>> mask = [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0] - >>> masked_a = ma.masked_array(a, mask) - >>> b = intervals(X, binding.end, mode.redunant) - >>> b - Exception - - ----10---- - >>> import foapy.ma as ma - >>> a = [[2, 2, 2], [2, 2, 2]] - >>> mask = [[0, 0, 0], [0, 0, 0]] - >>> masked_a = ma.masked_array(a, mask) - >>> b = ma.intervals(X, binding.end, mode.redunant) - >>> b - Exception - """ - - # Validate binding if binding not in {binding_enum.start, binding_enum.end}: raise ValueError( {"message": "Invalid binding value. Use binding.start or binding.end."} ) - # Validate mode valid_modes = { mode_enum.lossy, mode_enum.normal, @@ -174,8 +25,6 @@ def intervals(X, binding, mode): raise ValueError( {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."} ) - # ex.: - # ar = ['a', 'c', 'c', 'e', 'd', 'a'] power = X.shape[0] if power == 0: diff --git a/tests/test_characteristics/characterisitcs_test.py b/tests/test_characteristics/characterisitcs_test.py index a30566f2..9414f35a 100644 --- a/tests/test_characteristics/characterisitcs_test.py +++ b/tests/test_characteristics/characterisitcs_test.py @@ -1,9 +1,11 @@ from unittest import TestCase import numpy as np +from helpers.intervals import intervals, mode +from helpers.ma_intervals import intervals as ma_intervals import foapy.ma as ma -from foapy import binding, intervals, mode, order +from foapy import binding, order class CharacteristicsTest(TestCase): @@ -41,7 +43,7 @@ class CharacteristicsInfromationalTest(CharacteristicsTest): def AssertCase(self, X, binding, mode, expected, dtype=None): X = np.array(X) order_seq = ma.order(X) - intervals_seq = ma.intervals(order_seq, binding, mode) + intervals_seq = ma_intervals(order_seq, binding, mode) exists = self.target(intervals_seq, dtype) if expected < exists: @@ -60,7 +62,7 @@ def target(self, X): def AssertCase(self, X, binding, mode, expected, dtype=None): order_seq = ma.order(X) - intervals_seq = ma.intervals(order_seq, binding, mode) + intervals_seq = ma_intervals(order_seq, binding, mode) expected = np.array(expected) exists = self.target(intervals_seq, dtype) @@ -78,5 +80,5 @@ def AssertBatch(self, X, batch, dtype=None): def GetPrecision(self, length, dtype=None): alphabet = np.arange(0, np.fix(length * 0.2), dtype=int) X = np.random.choice(alphabet, length) - intervals_seq = ma.intervals(X, binding.start, mode.normal) + intervals_seq = ma_intervals(X, binding.start, mode.normal) return self.target(intervals_seq, dtype) diff --git a/tests/test_characteristics/test_arithmetic_mean.py b/tests/test_characteristics/test_arithmetic_mean.py index ed6c9176..6148c821 100644 --- a/tests/test_characteristics/test_arithmetic_mean.py +++ b/tests/test_characteristics/test_arithmetic_mean.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import mode from test_characteristics.characterisitcs_test import CharacteristicsTest -from foapy import binding, mode +from foapy import binding from foapy.characteristics import arithmetic_mean diff --git a/tests/test_characteristics/test_average_remoteness.py b/tests/test_characteristics/test_average_remoteness.py index e9b9b381..52fc2f7f 100644 --- a/tests/test_characteristics/test_average_remoteness.py +++ b/tests/test_characteristics/test_average_remoteness.py @@ -1,9 +1,10 @@ import numpy as np +from helpers.intervals import intervals, mode +from helpers.ma_intervals import intervals as intervals_ma from test_characteristics.characterisitcs_test import CharacteristicsTest -from foapy import binding, intervals, mode, order +from foapy import binding, order from foapy.characteristics import average_remoteness, identifying_information -from foapy.ma import intervals as intervals_ma from foapy.ma import order as order_ma diff --git a/tests/test_characteristics/test_depth.py b/tests/test_characteristics/test_depth.py index 6c417024..3a69e00f 100644 --- a/tests/test_characteristics/test_depth.py +++ b/tests/test_characteristics/test_depth.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import mode from test_characteristics.characterisitcs_test import CharacteristicsTest -from foapy import binding, mode +from foapy import binding from foapy.characteristics import depth diff --git a/tests/test_characteristics/test_descriptive_information.py b/tests/test_characteristics/test_descriptive_information.py index 9587a89a..73063c6a 100644 --- a/tests/test_characteristics/test_descriptive_information.py +++ b/tests/test_characteristics/test_descriptive_information.py @@ -1,9 +1,10 @@ import numpy as np +from helpers.intervals import intervals, mode +from helpers.ma_intervals import intervals as intervals_ma from test_characteristics.characterisitcs_test import CharacteristicsInfromationalTest -from foapy import binding, intervals, mode, order +from foapy import binding, order from foapy.characteristics import descriptive_information, geometric_mean -from foapy.ma import intervals as intervals_ma from foapy.ma import order as order_ma diff --git a/tests/test_characteristics/test_geometric_mean.py b/tests/test_characteristics/test_geometric_mean.py index ba318e7c..4ca77d33 100644 --- a/tests/test_characteristics/test_geometric_mean.py +++ b/tests/test_characteristics/test_geometric_mean.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import intervals, mode from test_characteristics.characterisitcs_test import CharacteristicsTest -from foapy import binding, intervals, mode, order +from foapy import binding, order from foapy.characteristics import arithmetic_mean, geometric_mean diff --git a/tests/test_characteristics/test_identifying_information.py b/tests/test_characteristics/test_identifying_information.py index 2ab712a0..4970d6a0 100644 --- a/tests/test_characteristics/test_identifying_information.py +++ b/tests/test_characteristics/test_identifying_information.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import mode from test_characteristics.characterisitcs_test import CharacteristicsInfromationalTest -from foapy import binding, mode +from foapy import binding from foapy.characteristics import identifying_information diff --git a/tests/test_characteristics/test_ma_arithmetic_mean.py b/tests/test_characteristics/test_ma_arithmetic_mean.py index 53fb7e63..39c31696 100644 --- a/tests/test_characteristics/test_ma_arithmetic_mean.py +++ b/tests/test_characteristics/test_ma_arithmetic_mean.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import arithmetic_mean diff --git a/tests/test_characteristics/test_ma_average_remoteness.py b/tests/test_characteristics/test_ma_average_remoteness.py index 98652b32..18d23a2b 100644 --- a/tests/test_characteristics/test_ma_average_remoteness.py +++ b/tests/test_characteristics/test_ma_average_remoteness.py @@ -1,11 +1,12 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant +from helpers.ma_intervals import intervals from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import average_remoteness, identifying_information -from foapy.ma import intervals, order +from foapy.ma import order class TestMaAverageRemoteness(MACharacteristicsTest): diff --git a/tests/test_characteristics/test_ma_depth.py b/tests/test_characteristics/test_ma_depth.py index bb574a7b..bd1ca049 100644 --- a/tests/test_characteristics/test_ma_depth.py +++ b/tests/test_characteristics/test_ma_depth.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import depth diff --git a/tests/test_characteristics/test_ma_geometric_mean.py b/tests/test_characteristics/test_ma_geometric_mean.py index 0b4a911f..3e52d545 100644 --- a/tests/test_characteristics/test_ma_geometric_mean.py +++ b/tests/test_characteristics/test_ma_geometric_mean.py @@ -1,11 +1,12 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant +from helpers.ma_intervals import intervals from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import arithmetic_mean, geometric_mean -from foapy.ma import intervals, order +from foapy.ma import order class TestGeometricMean(MACharacteristicsTest): diff --git a/tests/test_characteristics/test_ma_identifying_information.py b/tests/test_characteristics/test_ma_identifying_information.py index 88b15c44..c94af85a 100644 --- a/tests/test_characteristics/test_ma_identifying_information.py +++ b/tests/test_characteristics/test_ma_identifying_information.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import identifying_information diff --git a/tests/test_characteristics/test_ma_periodicity.py b/tests/test_characteristics/test_ma_periodicity.py index b9722d7b..2867875b 100644 --- a/tests/test_characteristics/test_ma_periodicity.py +++ b/tests/test_characteristics/test_ma_periodicity.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import periodicity diff --git a/tests/test_characteristics/test_ma_uniformity.py b/tests/test_characteristics/test_ma_uniformity.py index 9a45e308..ff489d57 100644 --- a/tests/test_characteristics/test_ma_uniformity.py +++ b/tests/test_characteristics/test_ma_uniformity.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import uniformity diff --git a/tests/test_characteristics/test_ma_volume.py b/tests/test_characteristics/test_ma_volume.py index c873a8fa..a1e1301e 100644 --- a/tests/test_characteristics/test_ma_volume.py +++ b/tests/test_characteristics/test_ma_volume.py @@ -1,9 +1,9 @@ import numpy as np import numpy.ma as ma +from helpers.intervals import mode as mode_constant from test_characteristics.characterisitcs_test import MACharacteristicsTest from foapy import binding as binding_constant -from foapy import mode as mode_constant from foapy.characteristics.ma import volume diff --git a/tests/test_characteristics/test_regularity.py b/tests/test_characteristics/test_regularity.py index 64781710..9f04fb39 100644 --- a/tests/test_characteristics/test_regularity.py +++ b/tests/test_characteristics/test_regularity.py @@ -1,9 +1,11 @@ import numpy as np +from helpers.intervals import mode +from helpers.ma_intervals import intervals from test_characteristics.characterisitcs_test import CharacteristicsInfromationalTest -from foapy import binding, mode +from foapy import binding from foapy.characteristics import regularity -from foapy.ma import intervals, order +from foapy.ma import order class Test_regularity(CharacteristicsInfromationalTest): diff --git a/tests/test_characteristics/test_uniformity.py b/tests/test_characteristics/test_uniformity.py index 6f2c1740..75d00812 100644 --- a/tests/test_characteristics/test_uniformity.py +++ b/tests/test_characteristics/test_uniformity.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import mode from test_characteristics.characterisitcs_test import CharacteristicsInfromationalTest -from foapy import binding, mode +from foapy import binding from foapy.characteristics import uniformity diff --git a/tests/test_characteristics/test_volume.py b/tests/test_characteristics/test_volume.py index a244d65a..5c806dcc 100644 --- a/tests/test_characteristics/test_volume.py +++ b/tests/test_characteristics/test_volume.py @@ -1,7 +1,8 @@ import numpy as np +from helpers.intervals import intervals, mode from test_characteristics.characterisitcs_test import CharacteristicsTest -from foapy import binding, intervals, mode +from foapy import binding from foapy.characteristics import volume diff --git a/tests/test_intervals.py b/tests/test_intervals.py index 2679b19e..f12787dc 100644 --- a/tests/test_intervals.py +++ b/tests/test_intervals.py @@ -2,9 +2,10 @@ import numpy as np import pytest +from helpers.intervals import intervals, mode from numpy.testing import assert_array_equal -from foapy import binding, intervals, mode +from foapy import binding class TestIntervals(TestCase): diff --git a/tests/test_intervals_chain.py b/tests/test_intervals_chain.py index 365c06a8..1c8b3adf 100644 --- a/tests/test_intervals_chain.py +++ b/tests/test_intervals_chain.py @@ -117,7 +117,7 @@ def test_mixed_start_boundary_2(self): def test_mixed_start_boundary_consistent_with_intervals_normal(self): """Chain with boundary mode matches intervals() normal mode result.""" - from foapy import intervals, mode + from helpers.intervals import intervals, mode X = ["a", "b", "a", "c", "a", "d"] chain = intervals_chain(X, binding.start, chain_mode.boundary) diff --git a/tests/test_is_valid_intervals_chain.py b/tests/test_is_valid_intervals_chain.py deleted file mode 100644 index fcef4748..00000000 --- a/tests/test_is_valid_intervals_chain.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest import TestCase - -import numpy as np - -from foapy.core import is_valid_intervals_chain - - -class TestIsValidIntervalsChain(TestCase): - """ - Test is_valid_intervals_chain(chain) -> bool. - - Never raises; returns True only for 1-D integer arrays of strictly - positive values all <= len(chain). - """ - - # ------------------------------------------------------------------------- - # Valid inputs — should return True - # ------------------------------------------------------------------------- - - def test_valid_boundary_start(self): - self.assertTrue( - is_valid_intervals_chain(np.array([1, 2, 2, 4, 2], dtype=np.intp)) - ) - - def test_valid_boundary_end(self): - self.assertTrue( - is_valid_intervals_chain(np.array([2, 4, 2, 2, 1], dtype=np.intp)) - ) - - def test_valid_cycle_start(self): - self.assertTrue( - is_valid_intervals_chain(np.array([1, 5, 2, 5, 2], dtype=np.intp)) - ) - - def test_valid_single_element(self): - self.assertTrue(is_valid_intervals_chain(np.array([1], dtype=np.intp))) - - def test_valid_all_ones(self): - self.assertTrue(is_valid_intervals_chain(np.array([1, 1, 1], dtype=np.intp))) - - def test_valid_max_value_equals_n(self): - # value == n is valid - self.assertTrue( - is_valid_intervals_chain(np.array([5, 1, 2, 3, 4], dtype=np.intp)) - ) - - # ------------------------------------------------------------------------- - # Empty array — should return True - # ------------------------------------------------------------------------- - - def test_empty_array_returns_true(self): - self.assertTrue(is_valid_intervals_chain(np.array([], dtype=np.intp))) - - def test_empty_list_returns_true(self): - self.assertTrue(is_valid_intervals_chain([])) - - # ------------------------------------------------------------------------- - # Invalid values — should return False - # ------------------------------------------------------------------------- - - def test_zero_value_returns_false(self): - self.assertFalse(is_valid_intervals_chain(np.array([0, 1, 2], dtype=np.intp))) - - def test_all_zeros_returns_false(self): - self.assertFalse(is_valid_intervals_chain(np.array([0, 0, 0], dtype=np.intp))) - - def test_negative_value_returns_false(self): - self.assertFalse(is_valid_intervals_chain(np.array([-1, 2, 2], dtype=np.intp))) - - def test_value_exceeds_length_returns_false(self): - # n=3, value 4 > 3 - self.assertFalse(is_valid_intervals_chain(np.array([1, 4, 2], dtype=np.intp))) - - def test_value_equals_n_plus_one_returns_false(self): - # n=3, value 4 = n+1 - self.assertFalse(is_valid_intervals_chain(np.array([4, 1, 2], dtype=np.intp))) - - # ------------------------------------------------------------------------- - # Non-1D arrays — should return False, no exception - # ------------------------------------------------------------------------- - - def test_2d_array_returns_false(self): - self.assertFalse(is_valid_intervals_chain(np.array([[1, 2], [3, 4]]))) - - def test_0d_array_returns_false(self): - self.assertFalse(is_valid_intervals_chain(np.array(42))) - - # ------------------------------------------------------------------------- - # Non-array input — should return False, no exception - # ------------------------------------------------------------------------- - - def test_string_returns_false(self): - self.assertFalse(is_valid_intervals_chain("not_a_chain")) - - def test_none_returns_false(self): - self.assertFalse(is_valid_intervals_chain(None)) - - def test_int_returns_false(self): - self.assertFalse(is_valid_intervals_chain(42)) diff --git a/tests/test_ma_intervals.py b/tests/test_ma_intervals.py index 08f5f966..6691c51f 100644 --- a/tests/test_ma_intervals.py +++ b/tests/test_ma_intervals.py @@ -3,10 +3,10 @@ import numpy as np import numpy.ma as ma import pytest +from helpers.ma_intervals import intervals from numpy.ma.testutils import assert_equal from foapy.exceptions import InconsistentOrderException, Not1DArrayException -from foapy.ma import intervals class TestMaIntervals(TestCase): diff --git a/tests/test_pipeline_consistency.py b/tests/test_pipeline_consistency.py index 88ac99bd..0bd4c32a 100644 --- a/tests/test_pipeline_consistency.py +++ b/tests/test_pipeline_consistency.py @@ -1,9 +1,10 @@ from unittest import TestCase import numpy as np +from helpers.intervals import intervals, mode from numpy.testing import assert_array_equal -from foapy import binding, chain_mode, intervals, mode +from foapy import binding, chain_mode from foapy.core import intervals_chain, intervals_tuple, tuple_mode