Skip to content

Croissant

MLCommons Croissant 1.1 JSON-LD generation and validation, including a SHA-256 per exported CSV. mlcroissant is an optional dependency and is imported lazily.

croissant

Croissant (MLCommons) JSON-LD metadata generation and validation.

Generates Croissant 1.1 JSON-LD using the mlcroissant library (optional dep). Includes SHA-256 hashes for all CSV files in the splits directory.

generate_croissant

generate_croissant(config: DatasetConfig, splits_dir: Path, version: str = 'clean') -> dict

Generate Croissant 1.1 JSON-LD for a dataset version.

Uses the mlcroissant library to build a valid Metadata object.

Parameters:

Name Type Description Default
config DatasetConfig

DatasetConfig

required
splits_dir Path

Path to the version dir (e.g., .../ptbxl/clean/)

required
version str

"original" or "clean"

'clean'

Returns:

Type Description
dict

dict — the Croissant JSON-LD

Source code in ecgbench/croissant.py
def generate_croissant(
    config: DatasetConfig,
    splits_dir: Path,
    version: str = "clean",
) -> dict:
    """Generate Croissant 1.1 JSON-LD for a dataset version.

    Uses the mlcroissant library to build a valid Metadata object.

    Args:
        config: DatasetConfig
        splits_dir: Path to the version dir (e.g., .../ptbxl/clean/)
        version: "original" or "clean"

    Returns:
        dict — the Croissant JSON-LD
    """
    mlc = _require_mlcroissant()
    import pandas as pd

    splits_dir = Path(splits_dir)

    # Discover all CSV files
    csv_files = _discover_csv_files(splits_dir)
    if not csv_files:
        raise FileNotFoundError(f"No CSV files found in {splits_dir}")

    # Build FileObjects for each CSV
    file_objects = []
    for csv_path in csv_files:
        rel_path = csv_path.relative_to(splits_dir)
        name = str(rel_path).replace("/", "-").replace("\\", "-").replace(".csv", "-csv")
        file_objects.append(
            mlc.FileObject(
                id=name,
                name=name,
                content_url=str(rel_path),
                encoding_formats=["text/csv"],
                sha256=_sha256(csv_path),
            )
        )

    # Build RecordSets for train/val/test from the split directories
    record_sets = []
    for split_name in ("train", "val", "test"):
        split_dir = splits_dir / split_name
        if not split_dir.exists():
            continue

        fold_csvs = sorted(split_dir.glob("fold_*.csv"))
        if not fold_csvs:
            continue

        sample_df = pd.read_csv(fold_csvs[0], nrows=5)

        # Find a file object ID to reference as source
        source_ref = None
        for fo in file_objects:
            if split_name in fo.id and "fold" in fo.id:
                source_ref = fo.id
                break

        fields = []
        for col in sample_df.columns:
            dtype = _infer_field_type(col, str(sample_df[col].dtype))
            field = mlc.Field(
                id=f"{split_name}-{col}",
                name=col,
                data_types=dtype,
                source=mlc.Source(
                    file_object=source_ref,
                    extract=mlc.Extract(column=col),
                ),
            )
            fields.append(field)

        record_sets.append(
            mlc.RecordSet(
                id=f"{split_name}-records",
                name=f"{split_name}-records",
                fields=fields,
            )
        )

    # Creator info
    creators = []
    for c in config.creators:
        cls = mlc.Organization if c.type == "Organization" else mlc.Person
        creators.append(cls(name=c.name, url=c.url))

    # Build the Croissant Metadata
    keywords = config.croissant.keywords if config.croissant else ["ECG"]
    try:
        metadata = mlc.Metadata(
            name=f"{config.slug}-{version}",
            url=config.url,
            description=config.description or f"{config.name} ECG dataset ({version} version)",
            sd_licence=config.license or "",
            version=config.version,
            cite_as=config.citation or "",
            date_published=date.today().isoformat(),
            conforms_to="http://mlcommons.org/croissant/1.1",
            creators=creators or None,
            keywords=keywords,
            distribution=file_objects,
            record_sets=record_sets,
        )
    except Exception as e:
        logger.warning(
            "Failed to build Croissant Metadata object: %s. "
            "Falling back to manual JSON-LD construction.",
            e,
        )
        return _build_manual_jsonld(config, splits_dir, version, csv_files)

    try:
        return metadata.to_json()
    except Exception:
        return json.loads(json.dumps(metadata.__dict__, default=str))

save_croissant

save_croissant(config: DatasetConfig, splits_dir: Path, output_path: Path | None = None, version: str = 'clean') -> Path

Generate and save croissant.json.

Parameters:

Name Type Description Default
config DatasetConfig

DatasetConfig

required
splits_dir Path

Path to the version dir

required
output_path Path | None

Where to write. Defaults to splits_dir/croissant.json

None
version str

"original" or "clean"

'clean'

Returns:

Type Description
Path

Path to the saved file

Source code in ecgbench/croissant.py
def save_croissant(
    config: DatasetConfig,
    splits_dir: Path,
    output_path: Path | None = None,
    version: str = "clean",
) -> Path:
    """Generate and save croissant.json.

    Args:
        config: DatasetConfig
        splits_dir: Path to the version dir
        output_path: Where to write. Defaults to splits_dir/croissant.json
        version: "original" or "clean"

    Returns:
        Path to the saved file
    """
    splits_dir = Path(splits_dir)
    if output_path is None:
        output_path = splits_dir / "croissant.json"
    output_path = Path(output_path)

    croissant_data = generate_croissant(config, splits_dir, version)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(croissant_data, f, indent=2, ensure_ascii=False, default=str)

    logger.info("Saved Croissant metadata to %s", output_path)
    return output_path

validate_croissant

validate_croissant(croissant_path: Path) -> tuple[bool, list[str]]

Validate a Croissant JSON-LD file.

Parameters:

Name Type Description Default
croissant_path Path

Path to the croissant.json file

required

Returns:

Type Description
tuple[bool, list[str]]

(is_valid, list_of_errors)

Source code in ecgbench/croissant.py
def validate_croissant(croissant_path: Path) -> tuple[bool, list[str]]:
    """Validate a Croissant JSON-LD file.

    Args:
        croissant_path: Path to the croissant.json file

    Returns:
        (is_valid, list_of_errors)
    """
    mlc = _require_mlcroissant()

    errors = []
    try:
        metadata = mlc.Metadata(jsonld=croissant_path)
        # Attempt to access properties to trigger validation
        _ = metadata.name
        _ = metadata.distribution
    except Exception as e:
        errors.append(str(e))

    return (len(errors) == 0, errors)