Skip to content

Profile Comparison

Metaseed provides tools for comparing profile specifications. This is useful for understanding differences between metadata standards and identifying common elements.

Web Interface

Access the comparison UI at /explore/ when running the web interface:

metaseed ui
# Then visit http://127.0.0.1:8080/explore/

Using the Compare Tool

  1. Select Base Profile: Choose the reference profile and version from the first dropdown
  2. Select Compare Profile: Choose the profile to compare against
  3. Click Compare: View the differences in the ERD visualization

The first profile selected is treated as the base/reference. Differences are shown relative to this base:

  • Unchanged (green): Elements identical in both profiles
  • Added (blue): Elements in the compare profile but not in the base
  • Removed (red): Elements in the base profile but not in compare
  • Modified (amber): Elements in both but with different attributes
  • Conflict (purple): Elements in both with incompatible attributes

Green means the two profiles agree, which is the first thing a comparison is asked. Added and removed are a hue apart rather than green against red, and every state also carries a glyph (= + - ~ !) so the distinction does not depend on colour vision. The canvas, the legend and the entity panel use the same five colours.

The server decides every colour. Each node in the graph data carries its complete vis.js colour (background, border, highlight, hover) and font colour, and each edge carries its colour and a diff_type (unchanged, added or removed; nested or reference when a single profile is explored). The page draws what it is sent and filters edges by diff_type; it keeps no colour table of its own, so a host that renders the graph cannot drift from the legend. When one profile is explored, nodes take the builder's white-and-moss style.

ERD Visualization

The comparison displays an interactive entity-relationship diagram:

Entity Colors:

Color Meaning
Green Entity exists in both profiles (unchanged)
Blue Entity only in compare profile (added)
Red Entity only in base profile (removed)
Amber Entity in both but fields differ (modified)
Purple Entity in both with incompatible field attributes (conflict)

Edge Colors:

Color Meaning
Green Relationship exists in both profiles
Blue Relationship only in the compare profile (added)
Red Relationship only in the base profile (removed)

Field Indicators:

Symbol Meaning
+ Field added (in compare, not in base)
- Field removed (in base, not in compare)
~ Field modified (different attributes)
! Field conflict (incompatible differences)
* Required field

Both sides of a change:

A field whose type, required marker or nested target differs is drawn on one line, carrying the old value and the new one separated by an arrow:

~  protocol: string, optional → required
~  parameter_accession_number: ontology_term → string
~→ observations: list, Observation → ObservationUnit

The marker is ~, which the legend already explains as modified, so - and + keep meaning only removed and added. Drawing a change the way git does — the old version on a - line above the new one on a + line — misreads here: those two glyphs are the legend's, not a diff's, so a field that merely became required looked like one field removed and another added, and every changed field cost two lines of node height.

A field can also be modified for a reason the line does not carry — a reworded description, another ontology term, a tightened constraint. Those keep the bare ~ line; the entity panel names those attributes under the field.

A removed field keeps its - line and an added field its + line, each showing the version held by the profile that has it. The removed field's line is what the field was before it was dropped, and the panel spells out that the compare profile does not have it.

Each node carries every profile's version of each field (sides in the graph payload, ordered as the profiles were selected), not the first one found; the entity panel lists both sides under the field, with the changed attributes named.

Entity panel:

Clicking an entity opens a panel with everything the profile says about it: the entity's description and ontology term, its SEEK mapping when the profile declares one (role, template, extended metadata), and for every field its nested target (the entity a list or entity field holds), its type, whether it is required, its description, ontology term, constraints (pattern, length, range, item counts), controlled vocabulary, unit, example, identifier/label markers, tier, and any SEEK or ISA markers (isa_tag, seek_attribute_type, seek_controlled_vocab). A field shows only the attributes it has set, so a plain string field is one line and a vocabulary-bound identifier is several.

Validation rules:

The panel lists the validation rules that apply to the entity — those whose applies_to names it or is all — with the rule's name, type, description and its parameters (field, condition, pattern, bounds, reference, message). A rule that references another entity (reference: Study.identifier) is also drawn on the graph, as a dashed amber edge from the entity it applies to toward the entity it references, labelled with the rule's name; hovering it shows the field and the target. The sidebar's Validation rules section lists every rule of the base profile, so cross-entity rules are visible without opening each entity, and its Profile section shows what the builder's profile form holds: display name, description, ontology, and root entity.

The panel is one script, static/js/explore-panel.js, that metaseed serves; metaseed-hub renders the same panel from it rather than keeping a copy.

Export Reports

After comparing, export the results:

  • MD: Markdown report with tables
  • CSV: Spreadsheet-compatible format
  • HTML: Styled HTML report

Python API

Comparing Profiles

from metaseed.specs.merge import compare

# Compare ISA with JERM (ISA is the base)
result = compare([
    ("isa", "1.0"),
    ("seek", "1.0"),
])

# Access statistics
print(f"Total entities: {result.statistics.total_entities}")
print(f"Common entities: {result.statistics.common_entities}")
print(f"Conflicting fields: {result.statistics.conflicting_fields}")

# Iterate through entity differences
for entity_diff in result.entity_diffs:
    print(f"{entity_diff.entity_name}: {entity_diff.diff_type.value}")

    for field_diff in entity_diff.field_diffs:
        if field_diff.diff_type.value != "unchanged":
            print(f"  {field_diff.field_name}: {field_diff.diff_type.value}")

Generating Reports

from metaseed.specs.merge import compare, MarkdownReportGenerator

result = compare([("isa", "1.0"), ("seek", "1.0")])

# Generate Markdown report
report = MarkdownReportGenerator(result).generate()
print(report)

# Or CSV/HTML
from metaseed.specs.merge import CSVReportGenerator, HTMLReportGenerator

csv_report = CSVReportGenerator(result).generate()
html_report = HTMLReportGenerator(result).generate()

Visualization Data

Generate vis.js compatible graph data:

from metaseed.specs.merge import compare, DiffVisualizer

result = compare([("isa", "1.0"), ("seek", "1.0")])

visualizer = DiffVisualizer()
graph_data = visualizer.build_diff_graph(result)

# graph_data contains:
# - nodes: Entity nodes with their vis.js colours, font colour and field data
# - edges: Relationships with their colour and diff_type
# - legend: Color legend for diff types
# - statistics: Summary statistics

Available Profiles

Profile Description
isa/1.0 Investigation-Study-Assay framework
miappe/1.1, miappe/1.2 Plant phenotyping metadata
miappe-htp/1.0 High-throughput plant phenotyping
seek/1.0 The model FAIRDOM-SEEK stores research in, built on JERM
darwin-core/1.0 Biodiversity data standard
dissco/0.4 Digital Specimen standard
ena/1.0 European Nucleotide Archive submissions
metabolights/1.0 Metabolomics studies
pride/1.0 Proteomics (ProteomeXchange) datasets

Diff Types

Type Description
unchanged Identical in all compared profiles
added Present in compare profile, absent in base
removed Present in base profile, absent in compare
modified Present in both but with different attributes
conflict Incompatible differences (e.g., different field types)

Data Models

ComparisonResult

Contains the full comparison between profiles:

  • profiles: List of profile identifiers compared
  • entity_diffs: List of EntityDiff objects
  • statistics: ComparisonStatistics with counts
  • metadata_diffs: Differences in profile metadata
  • validation_rule_diffs: Differences in validation rules

EntityDiff

Represents differences for a single entity:

  • entity_name: Name of the entity
  • diff_type: DiffType enum value
  • profiles: Dict mapping profile ID to presence (bool)
  • field_diffs: List of FieldDiff objects
  • has_conflicts: Whether any fields have conflicts

FieldDiff

Represents differences for a single field:

  • field_name: Name of the field
  • diff_type: DiffType enum value
  • profiles: Dict mapping profile ID to FieldSpec or None
  • attributes_changed: List of attribute names that differ
  • is_conflict: Whether this is a conflict

ComparisonStatistics

Summary counts:

  • total_entities: Total unique entities across all profiles
  • common_entities: Entities present in all profiles
  • unique_entities: Entities in only one profile
  • modified_entities: Entities with differences
  • total_fields: Total unique fields
  • common_fields: Fields identical across profiles
  • conflicting_fields: Fields with conflicts

Profile Merging

Metaseed also supports merging multiple profiles into a single combined profile.

CLI Usage

# Basic merge with default strategy (first_wins)
metaseed merge miappe/1.1 isa/1.0 -o combined.yaml

# Merge with most restrictive strategy
metaseed merge miappe/1.1 seek/1.0 -s most_restrictive -o strict.yaml

# Merge with custom name and version
metaseed merge miappe/1.1 isa/1.0 -n miappe-extended -v 2.0 -o extended.yaml

# Prefer a specific profile for conflicts
metaseed merge miappe/1.1 isa/1.0 -s prefer_miappe/1.1 -o miappe-based.yaml

Python API

from metaseed.specs.merge import merge

# Basic merge
result = merge(
    profiles=[("miappe", "1.1"), ("isa", "1.0")],
    strategy="first_wins",
    output_name="combined",
    output_version="1.0",
)

# Export to YAML
yaml_output = result.to_yaml()

# Export to dict
dict_output = result.to_dict()

# Check for warnings
for warning in result.warnings:
    print(f"{warning.entity_name}.{warning.field_name}: {warning.message}")

Merge Strategies

Strategy Behavior
first_wins Use first profile's value for conflicts
last_wins Use last profile's value for conflicts
most_restrictive required=True wins, tighter constraints
least_restrictive required=False wins, looser constraints
prefer_<profile> Always prefer specific profile (e.g., prefer_miappe/1.1)

MergeResult

Contains the merged profile and metadata:

  • merged_profile: The resulting ProfileSpec
  • source_profiles: List of profile identifiers merged
  • strategy_used: Name of the merge strategy applied
  • resolutions_applied: List of conflict resolutions
  • warnings: List of warnings generated
  • has_unresolved_conflicts: Whether conflicts remain

ConflictResolution

Manual resolution for a specific conflict:

  • entity_name: Entity containing the conflict
  • field_name: Field with the conflict
  • attribute: Attribute in conflict (e.g., "required", "type")
  • resolved_value: Value to use for resolution
  • source_profile: Profile the value was taken from (or None if custom)

MergeWarning

Warning generated during merge:

  • entity_name: Entity where warning occurred
  • field_name: Field where warning occurred
  • message: Warning message
  • resolution_applied: Description of automatic resolution

Strategy Functions

from metaseed.specs.merge import list_strategies, get_strategy

# List all available strategies
strategies = list_strategies()
# Returns: ['first_wins', 'last_wins', 'most_restrictive', 'least_restrictive', 'prefer_<profile>']

# Get a specific strategy instance
strategy = get_strategy("most_restrictive")

Manual Conflict Resolution

Override automatic conflict resolution with manual resolutions:

from metaseed.specs.merge import merge, ConflictResolution

# Define manual resolution
resolution = ConflictResolution(
    entity_name="Study",
    field_name="title",
    attribute="required",
    resolved_value=True,
    source_profile="miappe/1.1",
)

# Apply during merge
result = merge(
    profiles=[("miappe", "1.1"), ("isa", "1.0")],
    strategy="first_wins",
    manual_resolutions=[resolution],
)

# Check applied resolutions
for res in result.resolutions_applied:
    print(f"Resolved {res.entity_name}.{res.field_name}: {res.resolved_value}")