Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 56 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# EIDA Consistency Checker
[![Run Tests](https://github.com/EIDA/eida-consistency/actions/workflows/test.yml/badge.svg)](https://github.com/EIDA/eida-consistency/actions/workflows/test.yml)
![Coverage](badges/coverage.svg)
[![Docs](https://img.shields.io/badge/docs-visited-blue)](https://EIDA.github.io/eida-consistency/)
[![Docs](https://img.shields.io/badge/docs-online-blue)](https://EIDA.github.io/eida-consistency/)

---

A tool to evaluate the consistency between EIDA nodes' **availability** and **dataselect** web services.
Designed for use in quality control and monitoring tasks across the European Integrated Data Archive (EIDA).

📖 **Full documentation:** <https://EIDA.github.io/eida-consistency/>

---

## 🚀 Installation & Quick Start
Expand Down Expand Up @@ -57,35 +59,32 @@ Options:
- `--node`: Node code (e.g. `RESIF`, `NOA`, `ETH`)
- `--epochs`: Number of random test epochs (default: 10) OR percentage (e.g., `"5%"`, `0.05`)
- `--duration`: Epoch length in seconds (≥600)
- `--seed`: Reproducible seed
- `--seed`: Random seed for sampling. Does **not** reproduce an older run — to re-verify a finding see [Re-run & Re-verify](#-re-run--re-verify)
- `--delete-old`: Keep only the most recent report
- `--stdout`: Print JSON report to stdout
- `--report-dir`: Save reports to a custom folder (default: `reports/`)
- `--report-dir`: Save reports to a custom folder (default: `reports/`); works before or after the subcommand
- `--log-level`: Control verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`)

### Compare Reports

Compare results across two runs with the same seed:
Compare two report files (e.g. a before/after pair for the same node):

```bash
uvx eida-consistency compare reports/resif_run1.json reports/resif_run2.json
```

### Explore Inconsistencies

Drill down into inconsistencies with day-by-day exploration:
Re-check **only the inconsistencies** of a report, drilling down day-by-day to
find their exact boundaries. With no report argument it uses the newest report;
`--index` (repeatable) targets specific findings.

```bash
uvx eida-consistency explore reports/nodes/noa/*.json --index 7 --days 15 --verbose
uvx eida-consistency explore # newest report, all inconsistencies
uvx eida-consistency explore reports/noa_20260621_140111_113496.json --index 7
```

You can also use:

```bash
uvx eida-consistency explore --latest
```

to automatically use the newest report.
See [Re-run & Re-verify](#-re-run--re-verify) for when to use this vs `check` vs a fresh run.

### Manage Node List

Expand All @@ -107,8 +106,8 @@ uvx eida-consistency list-nodes

Reports are stored in `./reports/` by default, or in a custom folder using `--report-dir`.

- JSON reports: `reports/resif_<seed>.json`
- Markdown reports: `reports/resif_<seed>.md`
- JSON reports: `reports/<node>_<YYYYMMDD_HHMMSS>_<seed>.json`
- Markdown reports: `reports/<node>_<YYYYMMDD_HHMMSS>_<seed>.md`
- Global summary: [`summary.md`](https://github.com/EIDA/eida-consistency/blob/main/reports/summary.md)

---
Expand Down Expand Up @@ -165,29 +164,53 @@ uv run mkdocs serve

---

## 🧪 Example Workflow
## 🔁 Re-run & Re-verify

### 1. Run a check for NOA:
There are three distinct ways to "run it again", depending on what you want:

```bash
uvx eida-consistency consistency --seed 1234 --node NOA --epochs 20 --duration 600 --report-dir reports/test_noa
```
### 2. Explore incosistencies:
If inconsistencies are found:
```bash
uvx eida-consistency explore reports/nodes/resif/*.json --verbose
```
### 3. Apply fixes
Investigate the reported service inconsistencies and fix them at node level. Maybe use dmtri command suggested.
1. **Re-verify the findings of an existing report** — re-check only the
inconsistencies that a report recorded, replaying each one's exact window.
This is the right way to confirm a node-side fix:

### 4. Re-run Consistency
```bash
uvx eida-consistency consistency --seed 1234 --node NOA --epochs 20 --duration 600 --report-dir reports/test_noa
```
### 5.Compare Before/After
```bash
uvx eida-consistency explore reports/noa_20260621_140111_113496.json
```

Add `--index N` (repeatable) to re-check specific inconsistencies only.

2. **Re-check a single stream/window** — for a one-off, targeted check:

```bash
uvx eida-consistency check --node NOA --net HP --sta SERG --loc "" --cha HHZ \
--start 2016-09-20 --end 2016-10-19
```

3. **Run a fresh sampled check** — draw a new random set of streams for a node:

```bash
uvx eida-consistency consistency --node NOA --epochs 20 --duration 600
```

> ⚠️ Passing the same `--seed` does **not** reproduce an older run once the
> node's live inventory changes. To reproduce a *specific* finding, use option
> 1 or 2 above, which replay the exact window rather than re-sampling.

---

## 🧪 Example Workflow

```bash
uv run eida-consistency compare reports/test_noa/old.json reports/test_noa/new.json
# 1. Run a check (reports land in reports/test_noa/)
uvx eida-consistency consistency --node NOA --epochs 20 --report-dir reports/test_noa

# 2. Re-check the inconsistencies it found
uvx eida-consistency explore --report-dir reports/test_noa

# 3. Fix them at node level, then re-verify by replaying the same report
uvx eida-consistency explore reports/test_noa/<report>.json

# 4. Compare before/after
uvx eida-consistency compare reports/test_noa/old.json reports/test_noa/new.json
```

---
Expand Down
38 changes: 35 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ eida-consistency consistency --node NOA --epochs 10 --duration 600
* `--node TEXT`: EIDA node code (e.g., NOA, RESIF). [Required]
* `--epochs INTEGER`: Number of random time to check. [Default: 10]
* `--duration INTEGER`: Duration of each check in seconds (>= 600). [Default: 600]
* `--seed INTEGER`: Random seed for reproducibility.
* `--seed INTEGER`: Random seed for the candidate sampling. It does **not**
reliably reproduce an older run (a seed only selects the same streams while
the node's inventory is unchanged); to re-verify a past finding use
`explore`/`check`. See [Re-run & Re-verify](#re-run-re-verify).
* `--report-dir PATH`: Directory to store reports. Accepted before the
subcommand (`eida-consistency --report-dir DIR consistency …`) or after it
(`eida-consistency consistency … --report-dir DIR`). [Default: `reports/`]
* `--upload`: Upload the report to the configured S3 bucket.

### compare
Expand All @@ -36,12 +42,38 @@ eida-consistency compare report_A.json report_B.json

### explore

Explore the boundaries of inconsistent data found in a report.
Re-check **only the inconsistencies** of a report, drilling down day-by-day to
find their exact boundaries. Pass a report file, or omit it to use the newest
report in the report directory.

```bash
eida-consistency explore --index 0
# re-check every inconsistency in the latest report
eida-consistency explore

# re-check only specific inconsistencies in a given report
eida-consistency explore reports/noa_20260621_140111_113496.json --index 0 --index 3
```

**Options:**

* `--index INTEGER`: Index of an inconsistency to explore (repeatable; default: all).
* `--days INTEGER`: Maximum days to explore backward/forward. [Default: 30]
* `--verbose`: Print query URLs while exploring.
* `--json`: Emit discovered fixes as JSON on stdout (logs stay on stderr).
* `--report-dir PATH`: Directory to load reports from (same placement rules as above).

## Re-run & Re-verify

There are three ways to "run it again", depending on intent:

* **Re-verify a report's findings** — replay each recorded inconsistency's exact
window: `eida-consistency explore <report.json>` (only inconsistencies; use
`--index` to target specific ones).
* **Re-check a single stream/window** — `eida-consistency check --node … --net …
--sta … --cha … --start … --end …`.
* **Fresh sampled run** — `eida-consistency consistency --node …`. Note that
`--seed` does not reproduce an older run once the node's inventory changes.

### list-nodes

List all configured EIDA nodes.
Expand Down
11 changes: 10 additions & 1 deletion src/eida_consistency/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
"""EIDA node consistency checker package."""
"""EIDA node consistency checker package."""

from importlib.metadata import PackageNotFoundError, version as _pkg_version

try:
__version__ = _pkg_version("eida-consistency")
except PackageNotFoundError: # pragma: no cover - package not installed
__version__ = "0.0.0+unknown"

__all__ = ["__version__"]
22 changes: 18 additions & 4 deletions src/eida_consistency/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,17 @@ def cli(ctx, log_level, report_dir):
is_flag=True,
help="Upload report to configured S3 bucket after saving locally.",
)
@click.option(
"--report-dir",
"report_dir_opt",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
default=None,
help="Directory to store reports (overrides the global --report-dir).",
)
@click.pass_context
def consistency(ctx, node, epochs, duration, seed, delete_old, print_stdout, upload):
def consistency(ctx, node, epochs, duration, seed, delete_old, print_stdout, upload, report_dir_opt):
"""Run availability + dataselect consistency check, or housekeeping with --delete-old."""
report_dir: Path = ctx.obj["report_dir"]
report_dir: Path = report_dir_opt or ctx.obj["report_dir"]

if delete_old:
delete_old_reports(report_dir, keep=1)
Expand Down Expand Up @@ -230,10 +237,17 @@ def check(ctx, node, net, sta, cha, loc, start, end):
help="Emit the discovered fixes as JSON to stdout (machine-readable). "
"Human logs/progress stay on stderr, so stdout is pure JSON.",
)
@click.option(
"--report-dir",
"report_dir_opt",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
default=None,
help="Directory to load reports from (overrides the global --report-dir).",
)
@click.pass_context
def explore(ctx, report, index, days, verbose, as_json):
def explore(ctx, report, index, days, verbose, as_json, report_dir_opt):
"""Explore day-by-day boundaries of inconsistencies from a report."""
report_dir: Path = ctx.obj["report_dir"]
report_dir: Path = report_dir_opt or ctx.obj["report_dir"]

if not report:
try:
Expand Down
3 changes: 3 additions & 0 deletions src/eida_consistency/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

from eida_consistency import __version__
from eida_consistency.core.coverage import parse_iso

REPORT_DIR = Path("reports")
Expand Down Expand Up @@ -182,6 +183,7 @@ def create_report_object(

return {
"summary": {
"version": __version__,
"node": node,
"seed": seed,
"epochs_requested": epochs,
Expand Down Expand Up @@ -285,6 +287,7 @@ def save_report_markdown(report: Dict[str, Any], report_dir: Path = REPORT_DIR)
"",
"## Run Summary",
"",
f"- Tool version: `{summary.get('version', '?')}`",
f"- Seed: `{summary['seed']}`",
f"- Time: `{summary['timestamp']}`",
f"- Epochs requested: `{summary['epochs_requested']}`",
Expand Down
11 changes: 11 additions & 0 deletions tests/report/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ def test_create_report_object_basic():
assert isinstance(summary["timestamp"], str)


def test_create_report_object_includes_tool_version():
import eida_consistency

rep = report.create_report_object("NODE", 1, 1, 600, [])
assert rep["summary"]["version"] == eida_consistency.__version__
assert isinstance(rep["summary"]["version"], str)
assert rep["summary"]["version"]


def test_create_report_object_empty_records():
rep = report.create_report_object("NODE", 1, 1, 600, [])
assert rep["summary"]["score"] == 0.0
Expand Down Expand Up @@ -98,6 +107,8 @@ def test_save_report_markdown_with_skipped(tmp_path):
assert "Quality Breakdown" in text
assert "Service/Network Errors: `1`" in text
assert "Scored checks" in text
import eida_consistency
assert f"Tool version: `{eida_consistency.__version__}`" in text
assert "Skipped checks" in text
assert "TransientDataselectFailure" in text
assert "| Channel | Window (UTC) | Mismatch (UTC) | Gap | Disagreement |" in text
Expand Down
32 changes: 32 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ def fake_run(**kwargs): called.update(kwargs)
assert called["node"] == "NOA"


def test_consistency_report_dir_after_subcommand(monkeypatch, tmp_path):
"""--report-dir placed AFTER the subcommand overrides the group default."""
target = tmp_path / "custom"
called = {}
monkeypatch.setattr(cli, "run_consistency_check", lambda **kw: called.update(kw))
runner = CliRunner()
result = runner.invoke(
cli.consistency,
["--node", "NOA", "--report-dir", str(target)],
obj={"report_dir": tmp_path},
)
assert result.exit_code == 0, result.output
assert called["report_dir"] == target


# -----------------
# compare command
# -----------------
Expand Down Expand Up @@ -162,6 +177,23 @@ def test_explore_with_latest(monkeypatch, tmp_path):
assert result.exit_code == 0
assert called["report"].name == "rep.json"

def test_explore_report_dir_after_subcommand(monkeypatch, tmp_path):
"""explore honors --report-dir placed after the subcommand when finding latest."""
custom = tmp_path / "custom"
custom.mkdir()
(custom / "rep.json").write_text("{}")
called = {}
monkeypatch.setattr(cli, "explore_boundaries",
lambda report, indices, max_days, verbose: called.update({"report": report}))
runner = CliRunner()
# group default (tmp_path) has no reports; the override (custom) does.
result = runner.invoke(cli.explore, ["--report-dir", str(custom)],
obj={"report_dir": tmp_path})
assert result.exit_code == 0, result.output
assert called["report"].name == "rep.json"
assert called["report"].parent == custom


def test_explore_no_reports(tmp_path):
runner = CliRunner()
result = runner.invoke(cli.explore, [], obj={"report_dir": tmp_path})
Expand Down
Loading