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
13 changes: 11 additions & 2 deletions packages/imspy-predictors/tests/test_mixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ def sample_data(self):
return np.vstack([cluster1, cluster2, cluster3]).astype(np.float32)

@pytest.fixture
def gmm(self):
"""Create a GMM for testing."""
def gmm(self, sample_data):
"""Create a GMM for testing.

Means start at data points under a fixed seed. Unseeded N(0, 1) starts
let all three components fall into one cluster in about 4% of runs,
which failed test_fit intermittently in CI.
"""
import torch
from imspy_predictors.mixture import GaussianMixtureModel, TORCH_AVAILABLE
torch.manual_seed(0)
np.random.seed(0)
return GaussianMixtureModel(
num_components=3,
data_dim=2,
data=sample_data,
backend='torch',
)

Expand Down
8 changes: 5 additions & 3 deletions packages/imspy-simulation/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,16 @@ Used only when `acquisition_type = DDA`.
## `[provenance]` — mzPROV self-disclosure

TimSim emits an Ed25519-signed provenance record declaring the output is
simulated and binding it to the config + signing key. Requires the optional
`mzprov` package; import-guarded (a missing package logs a warning, never fails).
simulated and binding it to the config + signing key, using
[mzprov](https://github.com/mzprov/mzprov) (installed with imspy-simulation).
A signing error logs a warning and never fails the run. Check a file with
`mzprov verify <output>`.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `emit_provenance` | bool | `true` | Emit the signed provenance record. |
| `provenance_embed` | bool | `true` | Embed the envelope into the output (`.d` provenance table / mzML `fileContent`). `false` writes a sibling `<name>.provenance.json` sidecar. (Vendor `.raw` always falls back to a sidecar.) |
| `provenance_key_path` | str | _none_ | Signing key path. `none` = default `~/.config/timsim/keys/` (auto-generated). |
| `provenance_key_path` | str | _none_ | Signing key path. `none` = mzprov's default, `~/.config/mzprov/keys/` (auto-generated); an existing key at the older `~/.config/timsim/keys/` is used in place. |

## `[logging]`

Expand Down
1 change: 1 addition & 0 deletions packages/imspy-simulation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies = [
"zstd>=1.5",
"matplotlib>=3.5",
"imspy-search>=0.4.1",
"mzprov>=0.1.2", # signs every output (emit_provenance, on by default)
]

[project.optional-dependencies]
Expand Down
47 changes: 13 additions & 34 deletions packages/imspy-simulation/src/imspy_simulation/timsim/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,16 +515,16 @@ def get_default_settings() -> dict:
# data", binds the output to the config + a signing key). Emitted for ALL four
# vendors: Bruker .d (structural canonicalization), SCIEX/Waters mzML (mzML content
# canonicalization), and Thermo .raw (opaque whole-file content hash, sidecar-only —
# a vendor binary can't be embedded). Requires the optional `mzprov` package
# (with .raw support); import-guarded.
# a vendor binary can't be embedded). Signed with `mzprov`, a dependency; a signing
# error is logged and never fails the run.
'emit_provenance': True,
# True (default): EMBED the signed envelope INTO the output itself — the .d's
# analysis.tdf (a provenance table) or the mzML's fileContent — so provenance
# travels with the file and no extra file is produced. False: write a sibling
# `<name>.provenance.json` sidecar instead. (A vendor .raw can't be embedded; that
# path would fall back to a sidecar.)
'provenance_embed': True,
'provenance_key_path': None, # None = default ~/.config/timsim/keys/ (auto-gen)
'provenance_key_path': None, # None = mzprov's default key (auto-gen; an existing ~/.config/timsim/keys/ key is kept)

# SCIEX ZenoTOF SWATH build-from-.wiff (instrument=sciex_zenotof; template_path = .wiff).
# The .wiff method has no per-scan timing, so the SWATH schedule is synthesized from
Expand Down Expand Up @@ -1238,22 +1238,15 @@ def emit_provenance_sidecar(d_path, db_path, config_path, experiment_name,
embed, key_path, logger) -> None:
"""Write an mzPROV Ed25519-signed provenance sidecar for a Bruker .d output —
tamper-evident self-disclosure that this is TimSim-simulated data, bound to the
config and a signing key. Import-guarded: a missing `mzprov` package or any signing
error is logged as a warning and never fails the run. mzprov v0 canonicalizes
.d/mzML only (not vendor .raw), so callers gate this to the Bruker path."""
try:
from mzprov.sign import sign_simulation_output
except ImportError:
logger.warning(
" provenance: `mzprov` not installed — skipping sidecar "
"(pip install the mzprov python implementation to enable)"
)
return
config and a signing key. Any signing error is logged as a warning and never fails
the run."""
try:
from imspy_simulation import __version__ as _sim_version
except Exception:
_sim_version = "unknown"
try:
from mzprov.sign import sign_simulation_output

gt = db_path if (db_path and os.path.exists(db_path)) else None
out = sign_simulation_output(
d_path=d_path,
Expand All @@ -1277,21 +1270,14 @@ def emit_provenance_sidecar_mzml(mzml_path, config_path, experiment_name,
simulated data, bound to the config + a signing key. Uses mzprov's first-class mzML
signer (``sign_mzml_output``), which canonicalizes the mzML content (config + mzML
content hash; note v0 does not bind the ground-truth DB the way the .d path does).
Import-guarded: a missing ``mzprov`` package or any signing error is logged as a
warning and never fails the run."""
try:
from mzprov.sign import sign_mzml_output
except ImportError:
logger.warning(
" provenance: `mzprov` not installed — skipping mzML sidecar "
"(pip install the mzprov python implementation to enable)"
)
return
Any signing error is logged as a warning and never fails the run."""
try:
from imspy_simulation import __version__ as _sim_version
except Exception:
_sim_version = "unknown"
try:
from mzprov.sign import sign_mzml_output

out = sign_mzml_output(
mzml_path=mzml_path,
config_path=config_path,
Expand All @@ -1312,21 +1298,14 @@ def emit_provenance_sidecar_raw(raw_path, config_path, experiment_name, key_path
.raw is an opaque proprietary binary with no safe injection point, so this is ALWAYS a
JSON sidecar (regardless of the run's embed preference) and the attestation is an opaque
whole-file content hash (sensitive to any byte change). Uses mzprov's ``sign_raw_output``.
Import-guarded (covers both a missing ``mzprov`` and an older ``mzprov`` without raw
support): any failure is logged as a warning and never fails the run."""
try:
from mzprov.sign import sign_raw_output
except ImportError:
logger.warning(
" provenance: `mzprov` (with .raw support) not available — skipping .raw "
"sidecar (pip install/upgrade the mzprov python implementation to enable)"
)
return
Any signing error is logged as a warning and never fails the run."""
try:
from imspy_simulation import __version__ as _sim_version
except Exception:
_sim_version = "unknown"
try:
from mzprov.sign import sign_raw_output

out = sign_raw_output(
raw_path=raw_path,
config_path=config_path,
Expand Down
74 changes: 74 additions & 0 deletions packages/imspy-simulation/tests/test_mzprov_signing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""TimSim signs its outputs with mzprov, which is now a declared dependency.

Each vendor path's signer is run against mzprov's own minimal fixtures and
the result is checked with mzprov's verifier, so a change on either side that
breaks the attestation shows up here rather than in a user's run.
"""
import logging

import pytest

from mzprov import verify_sidecar
from mzprov._fixtures import make_minimal_d, make_minimal_mzml
from mzprov.verify import find_provenance_for, verify_embedded_d, verify_embedded_mzml

from imspy_simulation.timsim.simulator import (
emit_provenance_sidecar,
emit_provenance_sidecar_mzml,
emit_provenance_sidecar_raw,
)

LOG = logging.getLogger("test_mzprov_signing")


@pytest.fixture
def signing_env(tmp_path, monkeypatch):
"""Isolate the auto-generated signing key from the real home directory."""
monkeypatch.setenv("HOME", str(tmp_path / "home"))
config = tmp_path / "run.toml"
config.write_text('[experiment]\nname = "run"\n')
return tmp_path, config


@pytest.mark.parametrize("embed", [False, True])
def test_bruker_d_is_signed_and_verifies(signing_env, embed):
tmp_path, config = signing_env
d = make_minimal_d(tmp_path, name="run")
emit_provenance_sidecar(d, None, config, "run", embed, None, LOG)

if embed:
result = verify_embedded_d(d)
else:
result = verify_sidecar(tmp_path / "run.provenance.json")
assert result.overall_ok
assert result.payload.simulator_name == "TimSim"


@pytest.mark.parametrize("embed", [False, True])
def test_mzml_is_signed_and_verifies(signing_env, embed):
tmp_path, config = signing_env
mzml = make_minimal_mzml(tmp_path, name="run")
emit_provenance_sidecar_mzml(mzml, config, "run", embed, None, LOG)

if embed:
result = verify_embedded_mzml(mzml)
else:
result = verify_sidecar(mzml.with_name(mzml.stem + ".provenance.json"))
assert result.overall_ok


def test_thermo_raw_is_signed_and_verifies(signing_env):
tmp_path, config = signing_env
raw = tmp_path / "run.raw"
raw.write_bytes(b"opaque vendor bytes")
emit_provenance_sidecar_raw(raw, config, "run", None, LOG)

assert find_provenance_for(raw) is not None
assert verify_sidecar(tmp_path / "run.provenance.json").overall_ok


def test_signing_failure_never_fails_the_run(signing_env, caplog):
tmp_path, config = signing_env
with caplog.at_level(logging.WARNING, logger=LOG.name):
emit_provenance_sidecar_mzml(tmp_path / "missing.mzML", config, "run", False, None, LOG)
assert "signing failed (non-fatal)" in caplog.text
Loading