Current handling
When the name of a reverse property coincides with the name of a forward property on the same class, builder/update_openminds.py renames the reverse property using a hard-coded map, and stops the build for any collision not in the map:
# update_openminds.py, in FairgraphClassBuilder.translate()
conflict_resolution = {
"is_part_of": "is_also_part_of",
}
...
reverse_name_python = generate_python_name(reverse_link_name)
if reverse_name_python in forward_property_names:
if reverse_name_python in conflict_resolution:
reverse_name_python = conflict_resolution[reverse_name_python]
else:
raise Exception(
"The following name appears as both a forward and reverse name "
f"for {class_name}: {reverse_name_python}"
)
The one case handled today is File and FileBundle, which have a forward isPartOf property (pointing at FileBundle, or FileBundle/FileRepository) and are also targets of WorkflowRecipeVersion.hasPart, whose reverse is also naturally called isPartOf. The result is two properties, is_part_of and is_also_part_of, and a user who wants to know what a file is part of has to query both.
Symmetric relations
A second way the same collision arises is with symmetric relations, where the relation is its own reverse: if A is a variant of B then B is a variant of A. Examples are isVariantOf (v5.0) and isAlternativeVersionOf (v4.0). Such a relation necessarily links a class to itself, e.g. DatasetVersion.isVariantOf → DatasetVersion, so the reverse property lands on the same class as the forward one, with the same name.
Today no symmetric collision arises, because the reverse names come from the builder's own reverse_name_map, which was written to avoid them. But the problem is independent of where the names come from. If we adopt the reverse names proposed in openMetadataInitiative/openMINDS#97 (as amended by openMetadataInitiative/openMINDS#110), isVariantOf is its own reverse on 10 classes in v5.0 (AnatomicalAtlasVersion, CommonCoordinateFrameworkVersion, DatasetVersion, InterfaceVersion, LivePaperVersion, MetaDataModelVersion, ModelVersion, SoftwareVersion, ValidationTestVersion, WorkflowRecipeVersion), and isAlternativeVersionOf on 10 classes in v4.0. Renaming the reverse to something like is_also_variant_of would give users two properties for a single relation.
Proposal
In both situations the forward and reverse links mean the same thing, and a user asking "what is this a part of?" or "what is this a variant of?" wants a single answer. So rather than renaming, generate a single property with the shared name, of a new kind that performs both the forward and the reverse search and merges the results.
Merging is also needed for correctness in the symmetric case, not just convenience: the data are not necessarily reciprocal. A forward query on isVariantOf finds only the instances that declare the link, not those that are declared as variants by another instance, so a purely forward is_variant_of would under-report unless every link is declared in both directions in the KG.
The conflict_resolution map can then be retired.
Things to check when fixing
- The merged property must be many-valued.
DatasetVersion.isVariantOf and File.isPartOf are arrays, so this holds today, but it is worth asserting in the builder, since a single-valued forward property would silently lose the reverse half.
- The merged property may have mixed target types (for
File.is_part_of: FileBundle from the forward link, WorkflowRecipeVersion from the reverse), so the property's type list is the union.
- On save, only the forward part is written; the reverse links belong to other nodes. This matches how reverse properties are already treated, but it means a value assigned to the merged property is only persisted if its type is valid for the forward link.
- A test that regenerates against a vocabulary containing a self-reverse name, so this cannot regress.
Related
Current handling
When the name of a reverse property coincides with the name of a forward property on the same class,
builder/update_openminds.pyrenames the reverse property using a hard-coded map, and stops the build for any collision not in the map:The one case handled today is
FileandFileBundle, which have a forwardisPartOfproperty (pointing atFileBundle, orFileBundle/FileRepository) and are also targets ofWorkflowRecipeVersion.hasPart, whose reverse is also naturally calledisPartOf. The result is two properties,is_part_ofandis_also_part_of, and a user who wants to know what a file is part of has to query both.Symmetric relations
A second way the same collision arises is with symmetric relations, where the relation is its own reverse: if A is a variant of B then B is a variant of A. Examples are
isVariantOf(v5.0) andisAlternativeVersionOf(v4.0). Such a relation necessarily links a class to itself, e.g.DatasetVersion.isVariantOf → DatasetVersion, so the reverse property lands on the same class as the forward one, with the same name.Today no symmetric collision arises, because the reverse names come from the builder's own
reverse_name_map, which was written to avoid them. But the problem is independent of where the names come from. If we adopt the reverse names proposed in openMetadataInitiative/openMINDS#97 (as amended by openMetadataInitiative/openMINDS#110),isVariantOfis its own reverse on 10 classes in v5.0 (AnatomicalAtlasVersion,CommonCoordinateFrameworkVersion,DatasetVersion,InterfaceVersion,LivePaperVersion,MetaDataModelVersion,ModelVersion,SoftwareVersion,ValidationTestVersion,WorkflowRecipeVersion), andisAlternativeVersionOfon 10 classes in v4.0. Renaming the reverse to something likeis_also_variant_ofwould give users two properties for a single relation.Proposal
In both situations the forward and reverse links mean the same thing, and a user asking "what is this a part of?" or "what is this a variant of?" wants a single answer. So rather than renaming, generate a single property with the shared name, of a new kind that performs both the forward and the reverse search and merges the results.
Merging is also needed for correctness in the symmetric case, not just convenience: the data are not necessarily reciprocal. A forward query on
isVariantOffinds only the instances that declare the link, not those that are declared as variants by another instance, so a purely forwardis_variant_ofwould under-report unless every link is declared in both directions in the KG.The
conflict_resolutionmap can then be retired.Things to check when fixing
DatasetVersion.isVariantOfandFile.isPartOfare arrays, so this holds today, but it is worth asserting in the builder, since a single-valued forward property would silently lose the reverse half.File.is_part_of:FileBundlefrom the forward link,WorkflowRecipeVersionfrom the reverse), so the property's type list is the union.Related