# deteqt

Python client for DeteQT metrology and quantum-chip diagnostics on
InfluxDB v2.

## Documentation

- [Main website](https://deteqt-d9f77a.gitlab.io/)
- [Usage](https://deteqt-d9f77a.gitlab.io/usage/)
- [Examples](https://deteqt-d9f77a.gitlab.io/examples/)
- [API reference](https://deteqt-d9f77a.gitlab.io/api/)

## Install

```zsh
# Create and activate a virtual environment
uv venv deteqt-env --python 3.13  # any supported version >=3.12
source deteqt-env/bin/activate  # Linux/macOS
# .\deteqt-env\Scripts\activate  # Windows

# Install from PyPI
uv pip install deteqt

# Verify
uv run --active python -c "import deteqt; print(deteqt.__version__)"
```

If you cloned this repository (contributor / local dev), use `uv sync`
instead. This installs the exact versions locked in `uv.lock` for fully
reproducible results:

```zsh
uv sync --all-groups
```

## Configure settings

```python
from deteqt import write_settings_file

settings_path = write_settings_file(
    url="https://deteqt.duckdns.org",
    org="YOUR_ORG",
    bucket="YOUR_BUCKET",
    token="YOUR_WRITE_TOKEN",
    timeout_ms=30000,  # optional
)
print(settings_path)  # settings.toml
```

This creates `settings.toml` in your current working directory.

You can also edit `settings.toml` directly (a ready-to-fill template
is committed at the repo root):

```toml
url = "https://deteqt.duckdns.org"
org = "YOUR_ORG"
bucket = "YOUR_BUCKET"
token = "YOUR_WRITE_TOKEN"
timeout_ms = 30000
```

If writes intermittently fail with read-timeout errors, increase `timeout_ms`
(for example `60000`).

## CI/CD variables

The doc build job auto-executes example notebooks when a real InfluxDB
token is available. Set these four **masked** variables in
GitLab → Settings → CI/CD → Variables:

| Variable | Example value |
|---|---|
| `DETEQT_URL` | `https://deteqt.duckdns.org` |
| `DETEQT_ORG` | `your-org` |
| `DETEQT_BUCKET` | `your-bucket` |
| `DETEQT_TOKEN` | `your-write-token` (masked) |

The CI job writes `settings.toml` from these variables before
running `nbconvert`, so every notebook finds it automatically via
`_find_settings()`. Notebooks that need a local data folder
(`03`, `04`, `05`, `09`) are always skipped.

For local notebook runs, either create `settings.toml` with
`write_settings_file(...)` or export the token once:

```zsh
export DETEQT_TOKEN="your-write-token"
```

## Logging

Add this once at the top of your script or notebook to see progress
in your terminal:

```python
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
```

---

## Write one point

```python
from deteqt import TUID, write_point

run_tuid: TUID = TUID("20260213-143319-249-01d39d")

summary = write_point(
    {
        "qoi": "frequency",
        "nominal_value": 1.01,
        "uncertainty": 0.01,
        "tuid": run_tuid,
        "element": "q1",
        "label": "f01",
        "unit": "GHz",
        "element_label": "qubit-01",
        "device_ID": "chip-a",
        "run_ID": "run-001",
        "cycle_ID": "cycle-01",
        "condition": "4K",
        "extra_tags": {
            "area_of_interest": "sweetspot",
            "long_label": "Q1 frequency",
        },
        "extra_fields": {
            "temperature_mK": 12.3,
            "passed_qc": True,
        },
    }
)
print(summary)
# {'records_total': 1, 'records_written': 1, 'records_failed': 0}
```

## Write batch (with optional extras)

```python
from deteqt import write_batch

summary = write_batch(
    [
        {
            "qoi": "frequency",
            "nominal_value": 1.01,
            "uncertainty": 0.01,
            "run_ID": "run-001",
            "cycle_ID": "cycle-01",
            "extra_tags": {
                "area_of_interest": "sweetspot",
                "long_label": "Q1 frequency",
            },
            "extra_fields": {"temperature_mK": 12.3},
        },
        {
            "qoi": "phase",
            "nominal_value": 0.12,
            "run_ID": "run-001",
            "extra_tags": {
                "area_of_interest": "sweetspot",
                "long_label": "Q1 frequency",
            },
            "extra_fields": {"passed_qc": True},
        },
    ]
)
```

For larger loads, tune `chunk_size` (default `500`). Use
`continue_on_error=True, return_errors=True` to keep writing and receive
per-record failure details.
On uncertain chunk API failures, records are marked failed without per-record
retry to avoid duplicate writes.

## Common schema policy

Use these common keys across all integrations:

- measurement: `qoi`
- tags:
    `element`,
    `label`,
    `unit`,
    `element_label`,
    `device_ID`,
    `run_ID`,
    `cycle_ID`,
    `condition`
- fields:
    `nominal_value`,
    `uncertainty`,
    `tuid`
- optional timestamp key:
    `time` (RFC3339 string, `datetime`, or unix epoch number)

Partner-specific keys `area_of_interest` and `long_label` are treated as
optional extra tags.
If your settings file is elsewhere, pass `settings_path=".../settings.toml"`.

## Recursive ingest/write (single mode)

```python
from deteqt import ingest_and_write

summary = ingest_and_write(folder="quantify-data")
print(summary)
```

To inspect recursive pairing before writing:

```python
from deteqt import debug_ingest_matches

debug_ingest_matches("quantify-data")
# prints lines like:
# run-1/analysis/quantities_of_interest.json -> run-1/metadata/snapshot.json
```

The recursive path is JSON-only and always hybrid:

- values come from `quantities_of_interest*.json` (plain `.json`)
- metadata/tags come from `snapshot*.json` (commonly compressed in exports;
  supports `.json.xz`, `.json.gz`, `.json.bz2`, and plain `.json`)
- only standard SCQT transmon QoIs are kept
- folders named `analysis_BasicAnalysis` and `.ipynb_checkpoints` are ignored
- if hybrid merge returns no records for a run, that run falls back to
  snapshot-only parsing

Supported analysis aliases are normalized to standard names (for example:
`T1 -> t1`, `Qi -> resonator_qi`, `Qc -> resonator_qc`,
`fr -> resonator_freq_low` and `resonator_freq_high`).
