From e6be7ed8963026ea5670b923180b4fe75f1c6140 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 27 Apr 2026 21:42:00 +0200 Subject: [PATCH 01/15] test: update one-off fixtures for openMINDS v5 --- .../+oneoffs/+v4/organizationWithOneId.m | 9 ++ .../+oneoffs/+v4/organizationWithTwoIds.m | 11 ++ .../+oneoffs/+v4/personWithOneAffiliation.m | 24 ++++ .../+oneoffs/+v4/personWithTwoAffiliations.m | 30 +++++ .../+oneoffs/+v5/organizationWithOneId.m | 9 ++ .../+oneoffs/+v5/organizationWithTwoIds.m | 11 ++ .../+oneoffs/+v5/personWithOneAffiliation.m | 26 ++++ .../+oneoffs/+v5/personWithTwoAffiliations.m | 34 ++++++ .../+oneoffs/currentSchemaMajorVersion.m | 12 ++ .../+ommtest/+oneoffs/organizationName.m | 9 ++ tools/tests/oneOffs/organizationWithOneId.m | 9 +- tools/tests/oneOffs/organizationWithTwoIds.m | 11 +- tools/tests/oneOffs/personArray.m | 17 ++- .../tests/oneOffs/personWithOneAffiliation.m | 22 +--- .../tests/oneOffs/personWithTwoAffiliations.m | 28 +---- tools/tests/unitTests/CollectionTest.m | 115 +++++++++--------- tools/tests/unitTests/ResolverTest.m | 77 +++++++++--- tools/tests/unitTests/testLinkedCategory.m | 93 +++++++++++--- 18 files changed, 399 insertions(+), 148 deletions(-) create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithOneId.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithTwoIds.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithOneAffiliation.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithTwoAffiliations.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithOneId.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithTwoIds.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithOneAffiliation.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithTwoAffiliations.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/currentSchemaMajorVersion.m create mode 100644 tools/tests/oneOffs/+ommtest/+oneoffs/organizationName.m diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithOneId.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithOneId.m new file mode 100644 index 000000000..5a472bcca --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithOneId.m @@ -0,0 +1,9 @@ +function org = organizationWithOneId() +% organizationWithOneId - Create a v4 Organization with one digital ID. + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + + org = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "fullName", "University of Oslo"); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithTwoIds.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithTwoIds.m new file mode 100644 index 000000000..c588d5916 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/organizationWithTwoIds.m @@ -0,0 +1,11 @@ +function org = organizationWithTwoIds() +% organizationWithTwoIds - Create a v4 Organization with two digital IDs. + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + grid = openminds.core.GRIDID( ... + "identifier", "https://grid.ac/institutes/grid.5510.1"); + + org = openminds.core.Organization( ... + "digitalIdentifier", {ror, grid}, ... + "fullName", "University of Oslo"); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithOneAffiliation.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithOneAffiliation.m new file mode 100644 index 000000000..a8db494ed --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithOneAffiliation.m @@ -0,0 +1,24 @@ +function [person, affiliation] = personWithOneAffiliation() +% personWithOneAffiliation - Create a v4 Person with one Affiliation. + + ror = openminds.core.RORID("identifier", "https://ror.org/02jx3x895"); + org = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "fullName", "University College London"); + + affiliation = openminds.core.Affiliation("memberOf", org); + + orcid = openminds.core.ORCID( ... + "identifier", "https://orcid.org/0000-0000-0000-0000"); + + contact = openminds.core.ContactInformation( ... + "email", "johndsmith@somewhere.org"); + + person = openminds.core.Person( ... + "familyName", "Smith", ... + "givenName", "John D.", ... + "alternateName", "js", ... + "affiliation", affiliation, ... + "digitalIdentifier", orcid, ... + "contactInformation", contact); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithTwoAffiliations.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithTwoAffiliations.m new file mode 100644 index 000000000..ea6fed707 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v4/personWithTwoAffiliations.m @@ -0,0 +1,30 @@ +function [person, affiliations] = personWithTwoAffiliations() +% personWithTwoAffiliations - Create a v4 Person with two Affiliations. + + ror = openminds.core.RORID("identifier", "https://ror.org/02jx3x895"); + orgA = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "fullName", "University College London"); + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + orgB = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "fullName", "University of Oslo", ... + "shortName", "UiO"); + + affiliations = openminds.core.Affiliation("memberOf", orgA); + affiliations(2) = openminds.core.Affiliation("memberOf", orgB); + + orcid = openminds.core.ORCID( ... + "identifier", "https://orcid.org/0000-0000-0000-0000"); + + contact = openminds.core.ContactInformation( ... + "email", "johndsmith@somewhere.org"); + + person = openminds.core.Person( ... + "familyName", "Smith", ... + "givenName", "John D.", ... + "affiliation", affiliations, ... + "digitalIdentifier", orcid, ... + "contactInformation", contact); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithOneId.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithOneId.m new file mode 100644 index 000000000..2c5a6a044 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithOneId.m @@ -0,0 +1,9 @@ +function org = organizationWithOneId() +% organizationWithOneId - Create a v5 Organization with one digital ID. + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + + org = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "name", "University of Oslo"); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithTwoIds.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithTwoIds.m new file mode 100644 index 000000000..f9be7044a --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/organizationWithTwoIds.m @@ -0,0 +1,11 @@ +function org = organizationWithTwoIds() +% organizationWithTwoIds - Create a v5 Organization with two digital IDs. + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + rrid = openminds.core.RRID( ... + "identifier", "https://scicrunch.org/resolver/RRID:SCR_012345"); + + org = openminds.core.Organization( ... + "digitalIdentifier", {ror, rrid}, ... + "name", "University of Oslo"); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithOneAffiliation.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithOneAffiliation.m new file mode 100644 index 000000000..be553de8f --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithOneAffiliation.m @@ -0,0 +1,26 @@ +function [person, affiliation] = personWithOneAffiliation() +% personWithOneAffiliation - Create a v5 Person and matching Affiliation. + + ror = openminds.core.RORID("identifier", "https://ror.org/02jx3x895"); + org = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "name", "University College London"); + + orcid = openminds.core.ORCID( ... + "identifier", "https://orcid.org/0000-0000-0000-0000"); + + contact = openminds.core.ContactInformation( ... + "email", "johndsmith@somewhere.org"); + + person = openminds.core.Person( ... + "familyName", "Smith", ... + "givenName", "John D.", ... + "preferredName", "John D. Smith", ... + "alternateName", "js", ... + "digitalIdentifier", orcid, ... + "contactInformation", contact); + + affiliation = openminds.core.Affiliation( ... + "person", person, ... + "organization", org); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithTwoAffiliations.m b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithTwoAffiliations.m new file mode 100644 index 000000000..eb2f28809 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/+v5/personWithTwoAffiliations.m @@ -0,0 +1,34 @@ +function [person, affiliations] = personWithTwoAffiliations() +% personWithTwoAffiliations - Create a v5 Person and two Affiliation records. + + ror = openminds.core.RORID("identifier", "https://ror.org/02jx3x895"); + orgA = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "name", "University College London"); + + ror = openminds.core.RORID("identifier", "https://ror.org/01xtthb56"); + orgB = openminds.core.Organization( ... + "digitalIdentifier", ror, ... + "name", "University of Oslo", ... + "acronym", "UiO"); + + orcid = openminds.core.ORCID( ... + "identifier", "https://orcid.org/0000-0000-0000-0000"); + + contact = openminds.core.ContactInformation( ... + "email", "johndsmith@somewhere.org"); + + person = openminds.core.Person( ... + "familyName", "Smith", ... + "givenName", "John D.", ... + "preferredName", "John D. Smith", ... + "digitalIdentifier", orcid, ... + "contactInformation", contact); + + affiliations = openminds.core.Affiliation( ... + "person", person, ... + "organization", orgA); + affiliations(2) = openminds.core.Affiliation( ... + "person", person, ... + "organization", orgB); +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/currentSchemaMajorVersion.m b/tools/tests/oneOffs/+ommtest/+oneoffs/currentSchemaMajorVersion.m new file mode 100644 index 000000000..355d7c5a5 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/currentSchemaMajorVersion.m @@ -0,0 +1,12 @@ +function majorVersion = currentSchemaMajorVersion() +% currentSchemaMajorVersion - Identify the active openMINDS schema generation. + + organizationMeta = meta.class.fromName("openminds.core.actors.Organization"); + propertyNames = string({organizationMeta.PropertyList.Name}); + + if any(propertyNames == "name") && any(propertyNames == "countryOfFormation") + majorVersion = 5; + else + majorVersion = 4; + end +end diff --git a/tools/tests/oneOffs/+ommtest/+oneoffs/organizationName.m b/tools/tests/oneOffs/+ommtest/+oneoffs/organizationName.m new file mode 100644 index 000000000..e1ca2d548 --- /dev/null +++ b/tools/tests/oneOffs/+ommtest/+oneoffs/organizationName.m @@ -0,0 +1,9 @@ +function name = organizationName(org) +% organizationName - Return the active schema's primary organization name. + + if isprop(org, "name") + name = org.name; + else + name = org.fullName; + end +end diff --git a/tools/tests/oneOffs/organizationWithOneId.m b/tools/tests/oneOffs/organizationWithOneId.m index a3aa06d0c..581a351ba 100644 --- a/tools/tests/oneOffs/organizationWithOneId.m +++ b/tools/tests/oneOffs/organizationWithOneId.m @@ -7,8 +7,9 @@ % generated. % - ror = openminds.core.RORID('identifier','https://ror.org/01xtthb56'); - - org = openminds.core.Organization('digitalIdentifier', ror,... - 'fullName','University of Oslo'); + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + org = ommtest.oneoffs.v5.organizationWithOneId(); + else + org = ommtest.oneoffs.v4.organizationWithOneId(); + end end diff --git a/tools/tests/oneOffs/organizationWithTwoIds.m b/tools/tests/oneOffs/organizationWithTwoIds.m index 72a42dba9..73ee5b6af 100644 --- a/tools/tests/oneOffs/organizationWithTwoIds.m +++ b/tools/tests/oneOffs/organizationWithTwoIds.m @@ -7,10 +7,9 @@ % generated. % - ror = openminds.core.RORID('identifier','https://ror.org/01xtthb56'); - - grid = openminds.core.GRIDID('identifier', 'https://grid.ac/institutes/grid.5510.1'); - - org = openminds.core.Organization('digitalIdentifier', {ror, grid},... - 'fullName','University of Oslo'); + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + org = ommtest.oneoffs.v5.organizationWithTwoIds(); + else + org = ommtest.oneoffs.v4.organizationWithTwoIds(); + end end diff --git a/tools/tests/oneOffs/personArray.m b/tools/tests/oneOffs/personArray.m index 171ca8f45..8035d4e86 100644 --- a/tools/tests/oneOffs/personArray.m +++ b/tools/tests/oneOffs/personArray.m @@ -16,10 +16,17 @@ person = crewMembers(iRow,:); - persons(end+1) = openminds.core.Person( ... - 'givenName', person.givenName, ... - 'familyName', person.familyName, ... - 'alternateName', person.alternateName, ... - 'contactInformation', contacts(person.email) ); %#ok + personArguments = { ... + "givenName", person.givenName, ... + "familyName", person.familyName, ... + "alternateName", person.alternateName, ... + "contactInformation", contacts(person.email) }; + + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + personArguments = [personArguments, { ... + "preferredName", join([person.givenName, person.familyName], " ")}]; + end + + persons(end+1) = openminds.core.Person(personArguments{:}); %#ok end end diff --git a/tools/tests/oneOffs/personWithOneAffiliation.m b/tools/tests/oneOffs/personWithOneAffiliation.m index 2ecc309e8..4b7d7cc74 100644 --- a/tools/tests/oneOffs/personWithOneAffiliation.m +++ b/tools/tests/oneOffs/personWithOneAffiliation.m @@ -1,4 +1,4 @@ -function p = personWithOneAffiliation() +function varargout = personWithOneAffiliation() % personWithOneAffiliation - test creation of a person object in openMINDS % % p = personWithOneAffiliation() @@ -7,18 +7,8 @@ % generated. % -ror = openminds.core.RORID('identifier','https://ror.org/02jx3x895'); -org = openminds.core.Organization('digitalIdentifier',ror,... - 'fullName','University College London'); - -af = openminds.core.Affiliation('memberOf', org); - -orcid = openminds.core.ORCID('identifier',... - 'https://orcid.org/0000-0000-0000-0000'); - -contact = openminds.core.ContactInformation('email',... - 'johndsmith@somewhere.org'); - -p = openminds.core.Person('familyName','Smith','givenName','John D.',... - 'alternateName', "js", 'affiliation',af,'digitalIdentifier',orcid,... - 'contactInformation',contact); +if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + [varargout{1:nargout}] = ommtest.oneoffs.v5.personWithOneAffiliation(); +else + [varargout{1:nargout}] = ommtest.oneoffs.v4.personWithOneAffiliation(); +end diff --git a/tools/tests/oneOffs/personWithTwoAffiliations.m b/tools/tests/oneOffs/personWithTwoAffiliations.m index 2a47f15d7..3cc2a9681 100644 --- a/tools/tests/oneOffs/personWithTwoAffiliations.m +++ b/tools/tests/oneOffs/personWithTwoAffiliations.m @@ -1,4 +1,4 @@ -function p = personWithTwoAffiliations() +function varargout = personWithTwoAffiliations() % personWithTwoAffiliations - test creation of a person object in openMINDS % % p = personWithTwoAffiliations() @@ -7,24 +7,8 @@ % generated. % -ror = openminds.core.RORID('identifier','https://ror.org/02jx3x895'); -orgA = openminds.core.Organization('digitalIdentifier',ror,... - 'fullName','University College London'); - -ror = openminds.core.RORID('identifier','https://ror.org/01xtthb56'); - -orgB = openminds.core.Organization('digitalIdentifier',ror,... - 'fullName', 'University of Oslo', 'shortName', 'UiO'); - -af = openminds.core.Affiliation('memberOf', orgA); -af(2) = openminds.core.Affiliation('memberOf', orgB); - -orcid = openminds.core.ORCID('identifier',... - 'https://orcid.org/0000-0000-0000-0000'); - -contact = openminds.core.ContactInformation('email',... - 'johndsmith@somewhere.org'); - -p = openminds.core.Person('familyName','Smith','givenName','John D.',... - 'affiliation',af,'digitalIdentifier',orcid,... - 'contactInformation',contact); +if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + [varargout{1:nargout}] = ommtest.oneoffs.v5.personWithTwoAffiliations(); +else + [varargout{1:nargout}] = ommtest.oneoffs.v4.personWithTwoAffiliations(); +end diff --git a/tools/tests/unitTests/CollectionTest.m b/tools/tests/unitTests/CollectionTest.m index 9f77a100f..e4c82691a 100644 --- a/tools/tests/unitTests/CollectionTest.m +++ b/tools/tests/unitTests/CollectionTest.m @@ -27,13 +27,19 @@ function testCreateCollectionWithNameAndDescription(testCase) function testCreateCollectionWithInstances(testCase) % Test creating a collection with instances - person = personWithOneAffiliation(); - %org = organizationWithOneId(); - - collection = openminds.Collection(person); - - testCase.verifyEqual(length(collection), 5); - % Person, ContactInformation, Organization, ORCID, RORID + [person, affiliation] = personWithOneAffiliation(); + + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + collection = openminds.Collection(person, affiliation); + expectedNumNodes = 6; + % Person, ContactInformation, ORCID, Affiliation, Organization, RORID + else + collection = openminds.Collection(person); + expectedNumNodes = 5; + % Person, ContactInformation, Organization, ORCID, RORID + end + + testCase.verifyEqual(length(collection), expectedNumNodes); testCase.verifyTrue(collection.isKey(person.id)); end @@ -54,34 +60,38 @@ function testAddNodeWithoutLinks(testCase) function testAddNodeWithLinkedType(testCase) % Test adding a node with linked types collection = openminds.Collection(); - person = personWithOneAffiliation(); - - collection.add(person); + [person, affiliation] = personWithOneAffiliation(); + + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + collection.add(affiliation); + org = affiliation.organization; + else + collection.add(person); + org = person.affiliation.memberOf; + end % Verify that the linked nodes are also added to the collection testCase.verifyGreaterThan(length(collection), 1); testCase.verifyTrue(collection.isKey(person.id)); - - % Get the affiliation from the person - affiliation = person.affiliation; - % Get the organization from the affiliation - org = affiliation.memberOf; testCase.verifyTrue(collection.isKey(org.id)); end function testAddNodeWithEmbeddedType(testCase) % Test adding a node with embedded types collection = openminds.Collection(); - person = personWithOneAffiliation(); - - collection.add(person); - - % Get the affiliation from the person - affiliation = person.affiliation; - % Affiliation is embedded, verify that it's key is not in the - % collection + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + [dataset, affiliation] = CollectionTest.datasetWithOneContributorAffiliation(); + collection.add(dataset); + else + person = personWithOneAffiliation(); + collection.add(person); + affiliation = person.affiliation; + end + + % Affiliation is embedded in the containing schema, so the + % affiliation node itself should not be stored in the collection. testCase.verifyFalse(collection.isKey(affiliation.id)); end @@ -170,7 +180,9 @@ function testGet(testCase) % Verify that the retrieved organization is the same as the original testCase.verifyEqual(retrievedOrg.id, org.id); - testCase.verifyEqual(retrievedOrg.fullName, org.fullName); + testCase.verifyEqual( ... + ommtest.oneoffs.organizationName(retrievedOrg), ... + ommtest.oneoffs.organizationName(org)); end function testHasType(testCase) @@ -220,16 +232,16 @@ function testUpdateLinks(testCase) collection.add(person); initialLength = length(collection); - newAffiliation = openminds.core.Affiliation(... - 'memberOf', openminds.core.Organization('fullName', 'University of Somewhere'), ... - 'startDate', datetime("yesterday")); - person.affiliation(end+1) = newAffiliation; + newContact = openminds.core.ContactInformation( ... + "email", "john.smith@somewhere-else.org"); + person.contactInformation = newContact; % Update links collection.updateLinks(); % Verify that linked types are added testCase.verifyGreaterThan(length(collection), initialLength); + testCase.verifyTrue(collection.isKey(newContact.id)); end function testSaveAndLoad(testCase) @@ -272,7 +284,7 @@ function testSaveToMultipleFiles(testCase) % Verify that files are created files = dir(fullfile(folderPath, '**', '*.jsonld')); - testCase.verifyEqual(length(files), 7); + testCase.verifyEqual(length(files), length(collection)); % Create a new collection and load the files newCollection = openminds.Collection(); @@ -289,10 +301,9 @@ function testLoadInstances(testCase) collection = openminds.Collection(); person = personWithOneAffiliation(); org = organizationWithOneId(); - - expectedNumDocuments = 7; collection.add(person, org); + expectedNumDocuments = length(collection); % Save the collection to a file filePath = 'collection.jsonld'; @@ -311,8 +322,8 @@ function testSaveInstances(testCase) % Tests saving instances with MetadataStore person = personWithOneAffiliation(); org = organizationWithOneId(); - - expectedNumDocuments = 7; + collection = openminds.Collection(person, org); + expectedNumDocuments = length(collection); % Save instances to a file filePath = 'instances.jsonld'; @@ -341,31 +352,19 @@ function testSaveInstances(testCase) end methods (Static, Access = private) - function person = personWithOneAffiliation() - % Create a person with one affiliation - ror = openminds.core.RORID('identifier','https://ror.org/02jx3x895'); - org = openminds.core.Organization('digitalIdentifier',ror,... - 'fullName','University College London'); - - af = openminds.core.Affiliation('memberOf', org); - - orcid = openminds.core.ORCID('identifier',... - 'https://orcid.org/0000-0000-0000-0000'); - - contact = openminds.core.ContactInformation('email',... - 'johndsmith@somewhere.org'); - - person = openminds.core.Person('familyName','Smith','givenName','John D.',... - 'alternateName', "js", 'affiliation',af,'digitalIdentifier',orcid,... - 'contactInformation',contact); - end - - function org = organizationWithOneId() - % Create an organization with one digital ID - ror = openminds.core.RORID('identifier','https://ror.org/01xtthb56'); - - org = openminds.core.Organization('digitalIdentifier', ror,... - 'fullName','University of Oslo'); + function [dataset, affiliation] = datasetWithOneContributorAffiliation() + [person, affiliation] = personWithOneAffiliation(); + contribution = openminds.core.Contribution( ... + "contributor", person, ... + "type", openminds.controlledterms.ContributionType( ... + [], "name", "authoring")); + + dataset = openminds.core.Dataset( ... + "contribution", contribution, ... + "contributorAffiliation", affiliation, ... + "description", "Test dataset", ... + "fullName", "Test dataset", ... + "shortName", "test-dataset"); end end end diff --git a/tools/tests/unitTests/ResolverTest.m b/tools/tests/unitTests/ResolverTest.m index 7563071fb..c78f65677 100644 --- a/tools/tests/unitTests/ResolverTest.m +++ b/tools/tests/unitTests/ResolverTest.m @@ -93,19 +93,21 @@ function testResolveDatasetWithAuthor(testCase) authorRef = openminds.core.Person('id', 'https://mock.io/author_456'); % Create dataset with author reference - dataset = openminds.core.Dataset(... - 'fullName', "Test Dataset", ... - 'author', authorRef); + dataset = ResolverTest.createDatasetWithAuthors( ... + authorRef, "Test Dataset"); % Verify author starts empty - testCase.verifyEqual(dataset.author.givenName, ""); + authors = ResolverTest.getDatasetAuthors(dataset); + testCase.verifyEqual(authors.givenName, ""); % Resolve the dataset (should resolve linked authors) - dataset.resolve('NumLinksToResolve', 1); + dataset.resolve( ... + 'NumLinksToResolve', ResolverTest.datasetAuthorResolveDepth()); % Verify author is now resolved - testCase.verifyEqual(dataset.author.givenName, "Mock"); - testCase.verifyEqual(dataset.author.familyName, "Person"); + authors = ResolverTest.getDatasetAuthors(dataset); + testCase.verifyEqual(authors.givenName, "Mock"); + testCase.verifyEqual(authors.familyName, "Person"); end function testInstanceResolverCanResolve(testCase) @@ -189,18 +191,19 @@ function testResolveWithNumLinksToResolve(testCase) % Create a dataset with an author reference authorRef = openminds.core.Person('id', 'https://mock.io/author_789'); - dataset = openminds.core.Dataset(... - 'fullName', "Test Dataset", ... - 'author', authorRef); + dataset = ResolverTest.createDatasetWithAuthors( ... + authorRef, "Test Dataset"); % Test that resolve method exists and can be called testCase.verifyTrue(ismethod(dataset, 'resolve')); % Resolve with link depth of 1 - dataset.resolve('NumLinksToResolve', 1); + dataset.resolve( ... + 'NumLinksToResolve', ResolverTest.datasetAuthorResolveDepth()); % Verify the author was resolved - testCase.verifyEqual(dataset.author.givenName, "Mock"); + authors = ResolverTest.getDatasetAuthors(dataset); + testCase.verifyEqual(authors.givenName, "Mock"); end function testResolveMultipleLinkedInstances(testCase) @@ -213,16 +216,54 @@ function testResolveMultipleLinkedInstances(testCase) author2 = openminds.core.Person('id', 'https://mock.io/author2'); % Create dataset with multiple authors - dataset = openminds.core.Dataset(... - 'fullName', "Multi-Author Dataset", ... - 'author', {author1, author2}); + dataset = ResolverTest.createDatasetWithAuthors( ... + [author1, author2], "Multi-Author Dataset"); % Resolve with link depth - dataset.resolve('NumLinksToResolve', 1); + dataset.resolve( ... + 'NumLinksToResolve', ResolverTest.datasetAuthorResolveDepth()); % Verify both authors were resolved - testCase.verifyEqual(dataset.author(1).givenName, "Mock"); - testCase.verifyEqual(dataset.author(2).givenName, "Mock"); + authors = ResolverTest.getDatasetAuthors(dataset); + testCase.verifyEqual(authors(1).givenName, "Mock"); + testCase.verifyEqual(authors(2).givenName, "Mock"); + end + end + + methods (Static, Access = private) + function dataset = createDatasetWithAuthors(authors, fullName) + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + contribution = openminds.core.Contribution( ... + "contributor", authors, ... + "type", openminds.controlledterms.ContributionType( ... + [], "name", "authoring")); + + dataset = openminds.core.Dataset( ... + "contribution", contribution, ... + "description", fullName, ... + "fullName", fullName, ... + "shortName", fullName); + else + dataset = openminds.core.Dataset( ... + "fullName", fullName, ... + "author", authors); + end + end + + function authors = getDatasetAuthors(dataset) + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + authors = [dataset.contribution.contributor]; + else + authors = dataset.author; + end + end + + function depth = datasetAuthorResolveDepth() + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + depth = 2; + else + depth = 1; + end end end end diff --git a/tools/tests/unitTests/testLinkedCategory.m b/tools/tests/unitTests/testLinkedCategory.m index 7394c853d..6569349f2 100644 --- a/tools/tests/unitTests/testLinkedCategory.m +++ b/tools/tests/unitTests/testLinkedCategory.m @@ -25,16 +25,28 @@ function initializeInstances(testCase) % Create a dataset with one author of type Person - ds = openminds.core.Dataset(); - ds.author = personWithOneAffiliation; + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + [person, affiliation] = personWithOneAffiliation(); + ds = testLinkedCategory.createDatasetWithContributors(person, affiliation); + else + ds = openminds.core.Dataset(); + ds.author = personWithOneAffiliation; + end testCase.DatasetWithOnePersonAuthor = ds; % Create a dataset and add two persons as authors. - ds = openminds.core.Dataset(); - person1 = personWithOneAffiliation; - person2 = personWithTwoAffiliations; - ds.author = [person1, person2]; + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + [person1, affiliation1] = personWithOneAffiliation(); + [person2, affiliations2] = personWithTwoAffiliations(); + ds = testLinkedCategory.createDatasetWithContributors( ... + [person1, person2], [affiliation1, affiliations2]); + else + ds = openminds.core.Dataset(); + person1 = personWithOneAffiliation; + person2 = personWithTwoAffiliations; + ds.author = [person1, person2]; + end testCase.DatasetWithTwoPersonAuthor = ds; end @@ -52,7 +64,7 @@ function testRetrieveScalarHomogeneousType(testCase) %#ok<*MANU> % Get dataset for testing ds = testCase.DatasetWithOnePersonAuthor; - author = ds.author; + author = testLinkedCategory.getAuthors(ds); expectedAuthorType = 'openminds.core.actors.Person'; testCase.assertClass(author, expectedAuthorType) @@ -61,7 +73,7 @@ function testRetrieveScalarHomogeneousType(testCase) %#ok<*MANU> function testRetrieveNestedScalarHomogeneousType(testCase) % Get dataset for testing ds = testCase.DatasetWithOnePersonAuthor; - organization = ds.author.affiliation.memberOf; + organization = testLinkedCategory.getOrganizations(ds); if ~isempty(organization) testCase.assertClass(organization, 'openminds.core.actors.Organization') end @@ -71,7 +83,8 @@ function testRetrievePropertyOfNestedScalarHomogeneousType(testCase) % Get dataset for testing ds = testCase.DatasetWithOnePersonAuthor; - organizationName = ds.author.affiliation.memberOf.fullName; + organization = testLinkedCategory.getOrganizations(ds); + organizationName = ommtest.oneoffs.organizationName(organization); testCase.assertClass(organizationName, "string") % Alternative: This does not work. @@ -83,14 +96,14 @@ function testRetrievePropertyOfNestedNonScalarHomogeneousType(testCase) % Get dataset for testing ds = testCase.DatasetWithTwoPersonAuthor; - affiliationList = [ds.author.affiliation]; - organizationList = [affiliationList.memberOf]; - organizationName = [organizationList.fullName]; + affiliationList = testLinkedCategory.getAffiliations(ds); + organizationList = testLinkedCategory.getOrganizations(ds); + organizationName = arrayfun( ... + @ommtest.oneoffs.organizationName, organizationList); testCase.assertClass(organizationName, 'string') - S = ds.author.affiliation; % Assert length of this is 3 - testCase.assertLength(S, 3) + testCase.assertLength(affiliationList, 3) end function testRetrieveNonScalarHomogeneousType(testCase) @@ -100,7 +113,7 @@ function testRetrieveNonScalarHomogeneousType(testCase) ds = testCase.DatasetWithTwoPersonAuthor; % Check type of author property - author = ds.author; + author = testLinkedCategory.getAuthors(ds); expectedAuthorType = 'openminds.core.actors.Person'; testCase.assertClass(author, expectedAuthorType) end @@ -111,7 +124,7 @@ function testRetrieveNonScalarHomogeneousTypeParenOne(testCase) % Todo: What should be expected here ? ds = testCase.DatasetWithTwoPersonAuthor; - author = ds.author; + author = testLinkedCategory.getAuthors(ds); expectedParenIndexedType = 'openminds.core.actors.Person'; testCase.assertClass(author(1), expectedParenIndexedType) @@ -122,7 +135,7 @@ function testRetrieveNonScalarHomogeneousTypeParenAll(testCase) % all elements of array ds = testCase.DatasetWithTwoPersonAuthor; - author = ds.author; + author = testLinkedCategory.getAuthors(ds); expectedParenIndexedType = 'openminds.core.actors.Person'; testCase.assertClass(author(:), expectedParenIndexedType) @@ -130,7 +143,7 @@ function testRetrieveNonScalarHomogeneousTypeParenAll(testCase) function testNonScalarHomogeneousTypeLength(testCase) ds = testCase.DatasetWithTwoPersonAuthor; - author = ds.author; + author = testLinkedCategory.getAuthors(ds); % Test length of array testCase.assertLength(author, 2) @@ -143,7 +156,7 @@ function testRetreivePropertyOfScalarLinkedCategoryType(testCase) % Get dataset for testing ds = testCase.DatasetWithOnePersonAuthor; - author = ds.author; + author = testLinkedCategory.getAuthors(ds); try authorName = author.givenName; @@ -154,4 +167,46 @@ function testRetreivePropertyOfScalarLinkedCategoryType(testCase) end end end + + methods (Static, Access = private) + function ds = createDatasetWithContributors(persons, affiliations) + contribution = openminds.core.Contribution( ... + "contributor", persons, ... + "type", openminds.controlledterms.ContributionType( ... + [], "name", "authoring")); + + ds = openminds.core.Dataset( ... + "contribution", contribution, ... + "contributorAffiliation", affiliations, ... + "description", "Test dataset", ... + "fullName", "Test dataset", ... + "shortName", "test-dataset"); + end + + function authors = getAuthors(ds) + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + authors = [ds.contribution.contributor]; + else + authors = ds.author; + end + end + + function affiliations = getAffiliations(ds) + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + affiliations = ds.contributorAffiliation; + else + affiliations = [ds.author.affiliation]; + end + end + + function organizations = getOrganizations(ds) + affiliations = testLinkedCategory.getAffiliations(ds); + + if ommtest.oneoffs.currentSchemaMajorVersion() >= 5 + organizations = [affiliations.organization]; + else + organizations = [affiliations.memberOf]; + end + end + end end From 67d51e7f11d81a423026b9991bd2e4590b521887 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 27 Apr 2026 22:04:01 +0200 Subject: [PATCH 02/15] docs: archive legacy v4 tutorials --- .../{ => archive/v4.0}/basicNeuroscienceDataset.mlx | Bin .../{ => archive/v4.0}/crewMemberCollection.mlx | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename code/livescripts/{ => archive/v4.0}/basicNeuroscienceDataset.mlx (100%) rename code/livescripts/{ => archive/v4.0}/crewMemberCollection.mlx (100%) diff --git a/code/livescripts/basicNeuroscienceDataset.mlx b/code/livescripts/archive/v4.0/basicNeuroscienceDataset.mlx similarity index 100% rename from code/livescripts/basicNeuroscienceDataset.mlx rename to code/livescripts/archive/v4.0/basicNeuroscienceDataset.mlx diff --git a/code/livescripts/crewMemberCollection.mlx b/code/livescripts/archive/v4.0/crewMemberCollection.mlx similarity index 100% rename from code/livescripts/crewMemberCollection.mlx rename to code/livescripts/archive/v4.0/crewMemberCollection.mlx From fb43f5abaa7aeed505db33734f0370083c591166 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 27 Apr 2026 23:07:33 +0200 Subject: [PATCH 03/15] Update crew member tutorial to v5 --- code/livescripts/crewMemberCollection.mlx | Bin 0 -> 8396 bytes docs/tutorials/crewMemberCollection.md | 320 ++++++++++++---------- 2 files changed, 182 insertions(+), 138 deletions(-) create mode 100644 code/livescripts/crewMemberCollection.mlx diff --git a/code/livescripts/crewMemberCollection.mlx b/code/livescripts/crewMemberCollection.mlx new file mode 100644 index 0000000000000000000000000000000000000000..a22e1db844802494ef75b722467b947d9a677c0a GIT binary patch literal 8396 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 A=>Px# literal 0 HcmV?d00001 diff --git a/docs/tutorials/crewMemberCollection.md b/docs/tutorials/crewMemberCollection.md index 202d2baa2..60cfaef86 100644 --- a/docs/tutorials/crewMemberCollection.md +++ b/docs/tutorials/crewMemberCollection.md @@ -1,5 +1,5 @@ -# Metadata instances and collections +# Metadata instances and collections In this example we will create a metadata collection for the crew members of the "Heart of Gold" Spacecraft as described in the openMINDS [getting started](https://openminds-documentation.readthedocs.io/en/latest/shared/getting_started.html) guide. @@ -19,18 +19,16 @@ crewMembers = readtable(filePath, "TextType", "String") # Create instances -Let us create a set of metadata instances from this table that represents the crew members. We assume that memberOf provides the full name of a consortium each person is affiliated to. Since members might be affiliated to the same consortium we assume further that the same full name means the same consortium. We can also assume that the email is unique for each person. +Let us create a set of metadata instances from this table that represents the crew members. We assume that `memberOf` provides the full name of the consortium each person belongs to. A **`Consortium`** describes the group, while its `memberships` property lists the actors that belong to it. Since multiple people can belong to the same consortium, identical `memberOf` values are treated as references to the same **`Consortium`** instance. We also assume that `email` is unique for each person. With these assumptions we will create: -- a metadata Collection for storing metadata instances -- a unique set of Consortium instances based on the name given in the memberOf column -- a ContactInformation instance based on the email column -- a Person instance for each table row with: - - the givenName, familyName, and alternateName (if available) - - a link to the respective ContactInformation instance - - a person-specific embedded Affiliation instance that links to the respective Consortium instance +- a metadata `Collection` for storing metadata instances +- a unique set of **`Consortium`** instances based on the name given in the `memberOf` column +- a **`ContactInformation`** instance for each unique email address +- a **`Person`** instance for each table row, using `givenName`, `familyName`, `preferredName`, `alternateName` if available, and the corresponding **`ContactInformation`** +- a **`Membership`** instance for each person, assigned to the corresponding **`Consortium`** We start by creating an empty metadata collection for storing metadata instances. @@ -41,15 +39,19 @@ collection = openminds.Collection(... "Description", "Crew members of the 'Heart of Gold' spacecraft") ``` -```TextOutput +```matlabTextOutput collection = Collection with properties: - Name: "Crew Members" - Description: "Crew members of the 'Heart of Gold' spacecraft" - Nodes: dictionary with unset key and value types + + Name: "Crew Members" + Description: "Crew members of the 'Heart of Gold' spacecraft" + Nodes: dictionary with unset key and value types + LinkResolver: [] + MetadataStore: [0x0 openminds.internal.FileMetadataStore] + ``` -The collection will hold instances in a dictionary object of the Nodes property. Note: the Name and Description are optional and are currently not stored with the metadata instances. +The collection will hold instances in a dictionary object of the `Nodes` property. Note: the `Name` and `Description` are optional and are currently not stored with the metadata instances. We move on and start creating instances for the consortia: @@ -62,7 +64,8 @@ createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-'))); % with unique "Consortium" instances uniqueConsortiumNames = unique(crewMembers.memberOf); -consortia = dictionary; +% Create dictionary. Fall back to containers.Map for MATLAB < R2022b +try consortia = dictionary; catch; consortia = containers.Map; end for consortiumName = uniqueConsortiumNames' consortia(consortiumName) = openminds.core.Consortium(... 'id', createId(consortiumName), ... @@ -72,54 +75,62 @@ end disp(consortia) ``` -```TextOutput +```matlabTextOutput dictionary (string ⟼ openminds.core.actors.Consortium) with 1 entry: - "Heart of Gold Spacecraft Crew" ⟼ [Heart of Gold Spacecraft Crew] (Consortium) + + "Heart of Gold Spacecraft Crew" ⟼ [Heart of Gold Spacecraft Crew] (Consortium) ``` -We have now created a dictionary that holds the Consortium instances. Since all the persons in this example belongs to the same consortium, this dictionary only holds one instance. +We have now created a dictionary that holds the `Consortium` instances. Since all the persons in this example belong to the same consortium, this dictionary only holds one instance. -We can also look at the Consortium instance in more detail: +We can also look at the `Consortium` instance in more detail: ```matlab disp(consortia("Heart of Gold Spacecraft Crew")) ``` -```TextOutput - Consortium (https://openminds.ebrains.eu/core/Consortium) with properties: - contactInformation: [None] (ContactInformation) +```matlabTextOutput + Consortium (_:heart-of-gold-spacecraft-crew) with properties: + + contactInformation: [None] (ContactInformation) fullName: "Heart of Gold Spacecraft Crew" homepage: "" + memberships: [None] (Membership) shortName: "" - Required Properties: fullName + + Required Properties: fullName, memberships ``` -The Consortium instance has four properties, and we have filled out fullName. Whenever you create an openMINDS instance in MATLAB, you can either supply one or more name-value pairs when you create the instance (as we did above), or you can first create the instance and then assign values using dot-indexing on the instance object. +The `Consortium` instance has five properties, and we have filled out `fullName`. Whenever you create an openMINDS instance in MATLAB, you can either supply one or more name\-value pairs when you create the instance (as we did above), or you can first create the instance and then assign values using dot\-indexing on the instance object. ```matlab consortium = openminds.core.Consortium(); consortium.fullName = "Heart of Gold Spacecraft Crew" ``` -```TextOutput +```matlabTextOutput consortium = - Consortium (https://openminds.ebrains.eu/core/Consortium) with properties: - contactInformation: [None] (ContactInformation) + Consortium (_:18b0099e-01fd-482f-81dc-6721c18ab2d8) with properties: + + contactInformation: [None] (ContactInformation) fullName: "Heart of Gold Spacecraft Crew" homepage: "" + memberships: [None] (Membership) shortName: "" - Required Properties: fullName + + Required Properties: fullName, memberships + ``` -When the instance is displayed, you will see all the properties that are part of the instance type, and which of those are required (Note: at the moment of writing this guide, required properties are not enforced). The display should also give information about what types are expected for each of the property values. For example, the contactInformation property requires a ContactInformation instance (as indicated by the annotation in the brackets). If you want to learn more about the types as you explore the instances, you can always press the links in the instance display and they will take you to the openMINDS documentation page for that instance. +When the instance is displayed, you will see all the properties that are part of the instance type, and which of those are required (Note: at the moment of writing this guide, required properties are not enforced). The display should also give information about what types are expected for each of the property values. For example, the `contactInformation` property requires a `ContactInformation` instance (as indicated by the annotation in the brackets). If you want to learn more about the types as you explore the instances, you can always press the links in the instance display and they will take you to the openMINDS documentation page for that instance. -The consortium in this example does not have contact information, but we will move on and create ContactInformation types for each of the persons: +The consortium in this example does not have contact information, but we will move on and create `ContactInformation` types for each of the persons: ```matlab % Create a dictionary to hold "ContactInformation" instances -contacts = dictionary; +try contacts = dictionary; catch; contacts = containers.Map; end for email = crewMembers.email' contacts(email) = openminds.core.ContactInformation(... @@ -129,93 +140,114 @@ end disp(contacts) ``` -```TextOutput +```matlabTextOutput dictionary (string ⟼ openminds.core.actors.ContactInformation) with 4 entries: - "arthur-dent@hitchhikers-guide.galaxy" ⟼ [arthur-dent@hitchhikers-guide.galaxy] (ContactInformation) - "ford-prefect@hitchhikers-guide.galaxy" ⟼ [ford-prefect@hitchhikers-guide.galaxy] (ContactInformation) - "trillian-astra@hitchhikers-guide.galaxy" ⟼ [trillian-astra@hitchhikers-guide.galaxy] (ContactInformation) - "zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ [zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation) + + "arthur-dent@hitchhikers-guide.galaxy" ⟼ [arthur-dent@hitchhikers-guide.galaxy] (ContactInformation) + "ford-prefect@hitchhikers-guide.galaxy" ⟼ [ford-prefect@hitchhikers-guide.galaxy] (ContactInformation) + "trillian-astra@hitchhikers-guide.galaxy" ⟼ [trillian-astra@hitchhikers-guide.galaxy] (ContactInformation) + "zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ [zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation) ``` -This gave us four ContactInformation instances. Finally we will create Person instances and attach the ContactInformation and Consortium instances: +This gave us four **`ContactInformation`** instances. Next we create **`Person`** instances and each one to its **`ContactInformation`**. Finally we create one **`Membership`** instance per person and assign those memberships to the consortium. ```matlab % Extract data to create a list of "Person" instances where each "Person" -% instance will link to their respective "ContactInformation" instance and -% embed an "Affiliation" instance that links to the respective "Consortium" instance +% instance will link to their respective "ContactInformation" instance + persons = openminds.core.Person.empty; for iRow = 1:height(crewMembers) + personRow = crewMembers(iRow,:); + fullName = personRow.givenName + " " + personRow.familyName; - person = crewMembers(iRow,:); - fullName = person.givenName + " " + person.familyName; - persons(end+1) = openminds.core.Person( ... - 'id', createId(fullName), ... - 'givenName', person.givenName, ... - 'familyName', person.familyName, ... - 'alternateName', person.alternateName, ... - 'contactInformation', contacts(person.email), ... - 'affiliation', openminds.core.Affiliation('memberOf', consortia(person.memberOf) )); %#ok + "id", createId(fullName), ... + "givenName", personRow.givenName, ... + "familyName", personRow.familyName, ... + "preferredName", fullName, ... + "alternateName", personRow.alternateName, ... + "contactInformation", contacts(personRow.email)); %#ok +end + +% Create memberships for each person / crew member +memberships = openminds.core.miscellaneous.Membership.empty; +for i = 1:numel(persons) + memberships(end+1) = openminds.core.miscellaneous.Membership( ... + "member", persons(i)); %#ok end + +% Add crew members to the crew +crew = consortia("Heart of Gold Spacecraft Crew"); +crew.memberships = memberships; ``` # Add instances to collection and export collection -Now that we have all the instances, we can add them to the collection. It is sufficient to add the Person instances because the collection will automatically detect linked and embedded instances and add them automatically to the Nodes property. +Now that we have all the instances, we can add them to the `collection`. It is sufficient to add the `crew` consortium. The collection will follow embedded and linked instances from there and add the detected metadata instances to the `Nodes` property. ```matlab -collection.add(persons) +collection.add(crew) disp(collection) ``` -```TextOutput +```matlabTextOutput Collection with properties: - Name: "Crew Members" - Description: "Crew members of the 'Heart of Gold' spacecraft" - Nodes: dictionary (string ⟼ cell) with 9 entries + + Name: "Crew Members" + Description: "Crew members of the 'Heart of Gold' spacecraft" + Nodes: dictionary (string ⟼ cell) with 9 entries + LinkResolver: [] + MetadataStore: [0x0 openminds.internal.FileMetadataStore] ``` -As described above, we see that the Nodes hold 9 instances. We can look at the Nodes and we should expect to see 4 Person instances, 4 ContactInformation instances and 1 Consortium instance. +The `Nodes` dictionary contains 9 top\-level instances: 4 **`Person`** instances, 4 **`ContactInformation`** instances, and 1 **`Consortium`** instance. The **`Membership`** instances are embedded in the **`Consortium`**, so they are serialized as part of the consortium rather than as separate top\-level nodes. + ```matlab disp(collection.Nodes) ``` -```TextOutput +```matlabTextOutput dictionary (string ⟼ cell) with 9 entries: - "_:arthur-dent" ⟼ {[Dent, Arthur] (Person)} - "_:arthur-dent@hitchhikers-guide.galaxy" ⟼ {[arthur-dent@hitchhikers-guide.galaxy] (ContactInformation)} - "_:heart-of-gold-spacecraft-crew" ⟼ {[Heart of Gold Spacecraft Crew] (Consortium)} - "_:ford-prefect" ⟼ {[Prefect, Ford] (Person)} - "_:ford-prefect@hitchhikers-guide.galaxy" ⟼ {[ford-prefect@hitchhikers-guide.galaxy] (ContactInformation)} - "_:tricia-marie-mcmillan" ⟼ {[McMillan, Tricia Marie] (Person)} - "_:trillian-astra@hitchhikers-guide.galaxy" ⟼ {[trillian-astra@hitchhikers-guide.galaxy] (ContactInformation)} - "_:zaphod-beeblebrox" ⟼ {[Beeblebrox, Zaphod] (Person)} - "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ {[zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)} + + "_:arthur-dent@hitchhikers-guide.galaxy" ⟼ {[arthur-dent@hitchhikers-guide.galaxy] (ContactInformation)} + "_:arthur-dent" ⟼ {[Dent, Arthur] (Person)} + "_:ford-prefect@hitchhikers-guide.galaxy" ⟼ {[ford-prefect@hitchhikers-guide.galaxy] (ContactInformation)} + "_:ford-prefect" ⟼ {[Prefect, Ford] (Person)} + "_:trillian-astra@hitchhikers-guide.galaxy" ⟼ {[trillian-astra@hitchhikers-guide.galaxy] (ContactInformation)} + "_:tricia-marie-mcmillan" ⟼ {[McMillan, Tricia Marie] (Person)} + "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ {[zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)} + "_:zaphod-beeblebrox" ⟼ {[Beeblebrox, Zaphod] (Person)} + "_:heart-of-gold-spacecraft-crew" ⟼ {[Heart of Gold Spacecraft Crew] (Consortium)} ``` -A note here: Since the collection holds a mix of different types, each type is inside a cell (as indicated by the curly brackets). In order to get an instance from the Nodes, we need to index into a cell object: +A note here: Since the collection holds a mix of different types, each type is inside a cell (as indicated by the curly brackets). In order to get an instance from the `Nodes`, we need to index into a cell object: ```matlab if isMATLABReleaseOlderThan("R2023a") cellValue = collection.Nodes("_:arthur-dent"); - person = cellValue{1} + personRow = cellValue{1} else - person = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a + personRow = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a end ``` -```TextOutput -person = - Person (https://openminds.ebrains.eu/core/Person) with properties: - affiliation: Heart of Gold Spacecraft Crew (Affiliation) +```matlabTextOutput +personRow = + Person (_:arthur-dent) with properties: + alternateName: - associatedAccount: [None] (AccountInformation) - contactInformation: arthur-dent@hitchhikers-guide.galaxy (ContactInformation) - digitalIdentifier: [None] (ORCID) + associatedAccount: [None] (AccountInformation) + contactInformation: arthur-dent@hitchhikers-guide.galaxy (ContactInformation) + digitalIdentifier: [None] (Any of: GenericIdentifier, ORCID) familyName: "Dent" givenName: "Arthur" - Required Properties: givenName + preferredName: "Arthur Dent" + + Required Properties: preferredName + + Show all accessible properties of Person + ``` Finally, we can save the collection @@ -230,107 +262,119 @@ str = fileread(savePath); disp(str) ``` -```TextOutput +```matlabTextOutput { "@context": { - "@vocab": "https://openminds.ebrains.eu/vocab/" + "@vocab": "https://openminds.om-i.org/props/" }, "@graph": [ + { + "@id": "_:arthur-dent@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "arthur-dent@hitchhikers-guide.galaxy" + }, { "@id": "_:arthur-dent", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:arthur-dent@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:arthur-dent@hitchhikers-guide.galaxy" - }, "familyName": "Dent", - "givenName": "Arthur" + "givenName": "Arthur", + "preferredName": "Arthur Dent" }, { - "@id": "_:arthur-dent@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "arthur-dent@hitchhikers-guide.galaxy" - }, - { - "@id": "_:heart-of-gold-spacecraft-crew", - "@type": "https://openminds.ebrains.eu/core/Consortium", - "fullName": "Heart of Gold Spacecraft Crew" + "@id": "_:ford-prefect@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "ford-prefect@hitchhikers-guide.galaxy" }, { "@id": "_:ford-prefect", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:ford-prefect@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:ford-prefect@hitchhikers-guide.galaxy" - }, "familyName": "Prefect", - "givenName": "Ford" + "givenName": "Ford", + "preferredName": "Ford Prefect" }, { - "@id": "_:ford-prefect@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "ford-prefect@hitchhikers-guide.galaxy" + "@id": "_:trillian-astra@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "trillian-astra@hitchhikers-guide.galaxy" }, { "@id": "_:tricia-marie-mcmillan", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "alternateName": "Trillian Astra", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:trillian-astra@hitchhikers-guide.galaxy" } ], - "alternateName": [ - "Trillian Astra" - ], - "contactInformation": { - "@id": "_:trillian-astra@hitchhikers-guide.galaxy" - }, "familyName": "McMillan", - "givenName": "Tricia Marie" + "givenName": "Tricia Marie", + "preferredName": "Tricia Marie McMillan" }, { - "@id": "_:trillian-astra@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "trillian-astra@hitchhikers-guide.galaxy" + "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "zaphod-beeblebrox@hitchhikers-guide.galaxy" }, { "@id": "_:zaphod-beeblebrox", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" - }, "familyName": "Beeblebrox", - "givenName": "Zaphod" + "givenName": "Zaphod", + "preferredName": "Zaphod Beeblebrox" }, { - "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "zaphod-beeblebrox@hitchhikers-guide.galaxy" + "@id": "_:heart-of-gold-spacecraft-crew", + "@type": "https://openminds.om-i.org/types/Consortium", + "fullName": "Heart of Gold Spacecraft Crew", + "memberships": [ + { + "member": [ + { + "@id": "_:arthur-dent" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:ford-prefect" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:tricia-marie-mcmillan" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:zaphod-beeblebrox" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + } + ] } ] } From 14971fcf0511f3e4676ab94f3f1d36da2472193c Mon Sep 17 00:00:00 2001 From: ehennestad Date: Tue, 28 Apr 2026 10:19:15 +0200 Subject: [PATCH 04/15] Fix missing words in crew member example --- code/livescripts/crewMemberCollection.mlx | Bin 8396 -> 8397 bytes docs/tutorials/crewMemberCollection.html | 340 ++++++++++++---------- docs/tutorials/crewMemberCollection.md | 2 +- 3 files changed, 194 insertions(+), 148 deletions(-) diff --git a/code/livescripts/crewMemberCollection.mlx b/code/livescripts/crewMemberCollection.mlx index a22e1db844802494ef75b722467b947d9a677c0a..1f8574084140b0b87c9ded4d49bc4225175e0000 100644 GIT binary patch delta 3525 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 -Metadata instances and collections

Metadata instances and collections

In this example we will create a metadata collection for the crew members of the "Heart of Gold" Spacecraft as described in the openMINDS getting started guide.
We start by importing a csv file with crew member information:
filePath = fullfile(openminds.toolboxdir(), "livescripts", "data", "spacecraft_crew_members.csv");
crewMembers = readtable(filePath, "TextType", "String")
crewMembers = 4×5 table
 givenNamefamilyNamealternateNameemailmemberOf
1"Arthur""Dent"<missing>"arthur-dent@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
2"Ford""Prefect"<missing>"ford-prefect@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
3"Tricia Marie""McMillan""Trillian Astra""trillian-astra@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
4"Zaphod""Beeblebrox"<missing>"zaphod-beeblebrox@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"

Create instances

Let us create a set of metadata instances from this table that represents the crew members. We assume that memberOf provides the full name of a consortium each person is affiliated to. Since members might be affiliated to the same consortium we assume further that the same full name means the same consortium. We can also assume that the email is unique for each person.
With these assumptions we will create:
  • a metadata Collection for storing metadata instances
  • a unique set of Consortium instances based on the name given in the memberOf column
  • a ContactInformation instance based on the email column
  • a Person instance for each table row with:
  • the givenName, familyName, and alternateName (if available)
  • a link to the respective ContactInformation instance
  • a person-specific embedded Affiliation instance that links to the respective Consortium instance
We start by creating an empty metadata collection for storing metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Crew Members", ...
"Description", "Crew members of the 'Heart of Gold' spacecraft")
collection =
Collection with properties: +.S9 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } +.S10 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px 4px 0px 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S11 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; }

Metadata instances and collections

In this example we will create a metadata collection for the crew members of the "Heart of Gold" Spacecraft as described in the openMINDS getting started guide.
We start by importing a csv file with crew member information:
filePath = fullfile(openminds.toolboxdir(), "livescripts", "data", "spacecraft_crew_members.csv");
crewMembers = readtable(filePath, "TextType", "String")
crewMembers = 4×5 table
 givenNamefamilyNamealternateNameemailmemberOf
1"Arthur""Dent"<missing>"arthur-dent@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
2"Ford""Prefect"<missing>"ford-prefect@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
3"Tricia Marie""McMillan""Trillian Astra""trillian-astra@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"
4"Zaphod""Beeblebrox"<missing>"zaphod-beeblebrox@hitchhikers-guide.galaxy""Heart of Gold Spacecraft Crew"

Create instances

Let us create a set of metadata instances from this table that represents the crew members. We assume that memberOf provides the full name of the consortium each person belongs to. A Consortium describes the group, while its memberships property lists the actors that belong to it. Since multiple people can belong to the same consortium, identical memberOf values are treated as references to the same Consortium instance. We also assume that email is unique for each person.
With these assumptions we will create:
  • a metadata Collection for storing metadata instances
  • a unique set of Consortium instances based on the name given in the memberOf column
  • a ContactInformation instance for each unique email address
  • a Person instance for each table row, using givenName, familyName, preferredName, alternateName if available, and the corresponding ContactInformation
  • a Membership instance for each person, assigned to the corresponding Consortium
We start by creating an empty metadata collection for storing metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Crew Members", ...
"Description", "Crew members of the 'Heart of Gold' spacecraft")
collection =
Collection with properties: - Name: "Crew Members" - Description: "Crew members of the 'Heart of Gold' spacecraft" - Nodes: dictionary with unset key and value types -
The collection will hold instances in a dictionary object of the Nodes property. Note: the Name and Description are optional and are currently not stored with the metadata instances.
We move on and start creating instances for the consortia:
% Define a utility function for creating instance ids:
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));
 
% Extract the unique "memberOf" names to create dictionary
% with unique "Consortium" instances
uniqueConsortiumNames = unique(crewMembers.memberOf);
 
consortia = dictionary;
for consortiumName = uniqueConsortiumNames'
consortia(consortiumName) = openminds.core.Consortium(...
'id', createId(consortiumName), ...
'fullName', consortiumName );
end
 
disp(consortia)
dictionary (stringopenminds.core.actors.Consortium) with 1 entry: + Name: "Crew Members" + Description: "Crew members of the 'Heart of Gold' spacecraft" + Nodes: dictionary with unset key and value types + LinkResolver: [] + MetadataStore: [0×0 openminds.internal.FileMetadataStore] +
The collection will hold instances in a dictionary object of the Nodes property. Note: the Name and Description are optional and are currently not stored with the metadata instances.
We move on and start creating instances for the consortia:
% Define a utility function for creating instance ids:
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));
 
% Extract the unique "memberOf" names to create dictionary
% with unique "Consortium" instances
uniqueConsortiumNames = unique(crewMembers.memberOf);
 
% Create dictionary. Fall back to containers.Map for MATLAB < R2022b
try consortia = dictionary; catch; consortia = containers.Map; end
for consortiumName = uniqueConsortiumNames'
consortia(consortiumName) = openminds.core.Consortium(...
'id', createId(consortiumName), ...
'fullName', consortiumName );
end
 
disp(consortia)
dictionary (stringopenminds.core.actors.Consortium) with 1 entry: - "Heart of Gold Spacecraft Crew" ⟼ [Heart of Gold Spacecraft Crew] (Consortium)
We have now created a dictionary that holds the Consortium instances. Since all the persons in this example belongs to the same consortium, this dictionary only holds one instance.
We can also look at the Consortium instance in more detail:
disp(consortia("Heart of Gold Spacecraft Crew"))
Consortium (https://openminds.ebrains.eu/core/Consortium) with properties: + "Heart of Gold Spacecraft Crew" ⟼ [Heart of Gold Spacecraft Crew] (Consortium)
We have now created a dictionary that holds the Consortium instances. Since all the persons in this example belong to the same consortium, this dictionary only holds one instance.
We can also look at the Consortium instance in more detail:
disp(consortia("Heart of Gold Spacecraft Crew"))
Consortium (_:heart-of-gold-spacecraft-crew) with properties: - contactInformation: [None] (ContactInformation) + contactInformation: [None] (ContactInformation) fullName: "Heart of Gold Spacecraft Crew" homepage: "" + memberships: [None] (Membership) shortName: "" - Required Properties: fullName
The Consortium instance has four properties, and we have filled out fullName. Whenever you create an openMINDS instance in MATLAB, you can either supply one or more name-value pairs when you create the instance (as we did above), or you can first create the instance and then assign values using dot-indexing on the instance object.
consortium = openminds.core.Consortium();
consortium.fullName = "Heart of Gold Spacecraft Crew"
consortium =
Consortium (https://openminds.ebrains.eu/core/Consortium) with properties: + Required Properties: fullName, memberships
The Consortium instance has five properties, and we have filled out fullName. Whenever you create an openMINDS instance in MATLAB, you can either supply one or more name-value pairs when you create the instance (as we did above), or you can first create the instance and then assign values using dot-indexing on the instance object.
consortium = openminds.core.Consortium();
consortium.fullName = "Heart of Gold Spacecraft Crew"
consortium =
Consortium (_:18b0099e-01fd-482f-81dc-6721c18ab2d8) with properties: - contactInformation: [None] (ContactInformation) + contactInformation: [None] (ContactInformation) fullName: "Heart of Gold Spacecraft Crew" homepage: "" + memberships: [None] (Membership) shortName: "" - Required Properties: fullName -
When the instance is displayed, you will see all the properties that are part of the instance type, and which of those are required (Note: at the moment of writing this guide, required properties are not enforced). The display should also give information about what types are expected for each of the property values. For example, the contactInformation property requires a ContactInformation instance (as indicated by the annotation in the brackets). If you want to learn more about the types as you explore the instances, you can always press the links in the instance display and they will take you to the openMINDS documentation page for that instance.
The consortium in this example does not have contact information, but we will move on and create ContactInformation types for each of the persons:
% Create a dictionary to hold "ContactInformation" instances
contacts = dictionary;
 
for email = crewMembers.email'
contacts(email) = openminds.core.ContactInformation(...
'id', createId(email), ...
'email', email );
end
disp(contacts)
dictionary (stringopenminds.core.actors.ContactInformation) with 4 entries: - - "arthur-dent@hitchhikers-guide.galaxy" ⟼ [arthur-dent@hitchhikers-guide.galaxy] (ContactInformation) - "ford-prefect@hitchhikers-guide.galaxy" ⟼ [ford-prefect@hitchhikers-guide.galaxy] (ContactInformation) - "trillian-astra@hitchhikers-guide.galaxy" ⟼ [trillian-astra@hitchhikers-guide.galaxy] (ContactInformation) - "zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ [zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)
This gave us four ContactInformation instances. Finally we will create Person instances and attach the ContactInformation and Consortium instances:
% Extract data to create a list of "Person" instances where each "Person"
% instance will link to their respective "ContactInformation" instance and
% embed an "Affiliation" instance that links to the respective "Consortium" instance
persons = openminds.core.Person.empty;
 
for iRow = 1:height(crewMembers)
 
person = crewMembers(iRow,:);
fullName = person.givenName + " " + person.familyName;
persons(end+1) = openminds.core.Person( ...
'id', createId(fullName), ...
'givenName', person.givenName, ...
'familyName', person.familyName, ...
'alternateName', person.alternateName, ...
'contactInformation', contacts(person.email), ...
'affiliation', openminds.core.Affiliation('memberOf', consortia(person.memberOf) )); %#ok<SAGROW>
end

Add instances to collection and export collection

Now that we have all the instances, we can add them to the collection. It is sufficient to add the Person instances because the collection will automatically detect linked and embedded instances and add them automatically to the Nodes property.
collection.add(persons)
disp(collection)
Collection with properties: - - Name: "Crew Members" - Description: "Crew members of the 'Heart of Gold' spacecraft" - Nodes: dictionary (string ⟼ cell) with 9 entries
As described above, we see that the Nodes hold 9 instances. We can look at the Nodes and we should expect to see 4 Person instances, 4 ContactInformation instances and 1 Consortium instance.
disp(collection.Nodes)
dictionary (stringcell) with 9 entries: - - "_:arthur-dent" ⟼ {[Dent, Arthur] (Person)} - "_:arthur-dent@hitchhikers-guide.galaxy" ⟼ {[arthur-dent@hitchhikers-guide.galaxy] (ContactInformation)} - "_:heart-of-gold-spacecraft-crew" ⟼ {[Heart of Gold Spacecraft Crew] (Consortium)} - "_:ford-prefect" ⟼ {[Prefect, Ford] (Person)} - "_:ford-prefect@hitchhikers-guide.galaxy" ⟼ {[ford-prefect@hitchhikers-guide.galaxy] (ContactInformation)} - "_:tricia-marie-mcmillan" ⟼ {[McMillan, Tricia Marie] (Person)} - "_:trillian-astra@hitchhikers-guide.galaxy" ⟼ {[trillian-astra@hitchhikers-guide.galaxy] (ContactInformation)} - "_:zaphod-beeblebrox" ⟼ {[Beeblebrox, Zaphod] (Person)} - "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ {[zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)}
A note here: Since the collection holds a mix of different types, each type is inside a cell (as indicated by the curly brackets). In order to get an instance from the Nodes, we need to index into a cell object:
if isMATLABReleaseOlderThan("R2023a")
cellValue = collection.Nodes("_:arthur-dent");
person = cellValue{1}
else
person = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a
end
person =
Person (https://openminds.ebrains.eu/core/Person) with properties: - - affiliation: Heart of Gold Spacecraft Crew (Affiliation) + Required Properties: fullName, memberships +
When the instance is displayed, you will see all the properties that are part of the instance type, and which of those are required (Note: at the moment of writing this guide, required properties are not enforced). The display should also give information about what types are expected for each of the property values. For example, the contactInformation property requires a ContactInformation instance (as indicated by the annotation in the brackets). If you want to learn more about the types as you explore the instances, you can always press the links in the instance display and they will take you to the openMINDS documentation page for that instance.
The consortium in this example does not have contact information, but we will move on and create ContactInformation types for each of the persons:
% Create a dictionary to hold "ContactInformation" instances
try contacts = dictionary; catch; contacts = containers.Map; end
 
for email = crewMembers.email'
contacts(email) = openminds.core.ContactInformation(...
'id', createId(email), ...
'email', email );
end
disp(contacts)
dictionary (stringopenminds.core.actors.ContactInformation) with 4 entries: + + "arthur-dent@hitchhikers-guide.galaxy" ⟼ [arthur-dent@hitchhikers-guide.galaxy] (ContactInformation) + "ford-prefect@hitchhikers-guide.galaxy" ⟼ [ford-prefect@hitchhikers-guide.galaxy] (ContactInformation) + "trillian-astra@hitchhikers-guide.galaxy" ⟼ [trillian-astra@hitchhikers-guide.galaxy] (ContactInformation) + "zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ [zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)
This gave us four ContactInformation instances. Next we create Person instances and link each one to its ContactInformation instance. Finally we create one Membership instance per person and assign those memberships to the consortium.
% Extract data to create a list of "Person" instances where each "Person"
% instance will link to their respective "ContactInformation" instance
 
persons = openminds.core.Person.empty;
 
for iRow = 1:height(crewMembers)
personRow = crewMembers(iRow,:);
fullName = personRow.givenName + " " + personRow.familyName;
 
persons(end+1) = openminds.core.Person( ...
"id", createId(fullName), ...
"givenName", personRow.givenName, ...
"familyName", personRow.familyName, ...
"preferredName", fullName, ...
"alternateName", personRow.alternateName, ...
"contactInformation", contacts(personRow.email)); %#ok<SAGROW>
end
 
% Create memberships for each person / crew member
memberships = openminds.core.miscellaneous.Membership.empty;
for i = 1:numel(persons)
memberships(end+1) = openminds.core.miscellaneous.Membership( ...
"member", persons(i)); %#ok<SAGROW>
end
 
% Add crew members to the crew
crew = consortia("Heart of Gold Spacecraft Crew");
crew.memberships = memberships;

Add instances to collection and export collection

Now that we have all the instances, we can add them to the collection. It is sufficient to add the crew consortium. The collection will follow embedded and linked instances from there and add the detected metadata instances to the Nodes property.
collection.add(crew)
disp(collection)
Collection with properties: + + Name: "Crew Members" + Description: "Crew members of the 'Heart of Gold' spacecraft" + Nodes: dictionary (string ⟼ cell) with 9 entries + LinkResolver: [] + MetadataStore: [0×0 openminds.internal.FileMetadataStore]
The Nodes dictionary contains 9 top-level instances: 4 Person instances, 4 ContactInformation instances, and 1 Consortium instance. The Membership instances are embedded in the Consortium, so they are serialized as part of the consortium rather than as separate top-level nodes.
disp(collection.Nodes)
dictionary (stringcell) with 9 entries: + + "_:arthur-dent@hitchhikers-guide.galaxy" ⟼ {[arthur-dent@hitchhikers-guide.galaxy] (ContactInformation)} + "_:arthur-dent" ⟼ {[Dent, Arthur] (Person)} + "_:ford-prefect@hitchhikers-guide.galaxy" ⟼ {[ford-prefect@hitchhikers-guide.galaxy] (ContactInformation)} + "_:ford-prefect" ⟼ {[Prefect, Ford] (Person)} + "_:trillian-astra@hitchhikers-guide.galaxy" ⟼ {[trillian-astra@hitchhikers-guide.galaxy] (ContactInformation)} + "_:tricia-marie-mcmillan" ⟼ {[McMillan, Tricia Marie] (Person)} + "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ {[zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation)} + "_:zaphod-beeblebrox" ⟼ {[Beeblebrox, Zaphod] (Person)} + "_:heart-of-gold-spacecraft-crew" ⟼ {[Heart of Gold Spacecraft Crew] (Consortium)}
A note here: Since the collection holds a mix of different types, each type is inside a cell (as indicated by the curly brackets). In order to get an instance from the Nodes, we need to index into a cell object:
if isMATLABReleaseOlderThan("R2023a")
cellValue = collection.Nodes("_:arthur-dent");
personRow = cellValue{1}
else
personRow = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a
end
personRow =
Person (_:arthur-dent) with properties: + alternateName: <missing> - associatedAccount: [None] (AccountInformation) - contactInformation: arthur-dent@hitchhikers-guide.galaxy (ContactInformation) - digitalIdentifier: [None] (ORCID) + associatedAccount: [None] (AccountInformation) + contactInformation: arthur-dent@hitchhikers-guide.galaxy (ContactInformation) + digitalIdentifier: [None] (Any of: GenericIdentifier, ORCID) familyName: "Dent" givenName: "Arthur" + preferredName: "Arthur Dent" + + Required Properties: preferredName - Required Properties: givenName -
Finally, we can save the collection
% Save the instances to the openMINDS userdata folder:
savePath = fullfile(userpath, "openMINDS_MATLAB", "demo", "crew_members.jsonld");
collection.save(savePath)
 
% Check out the saved metadata:
str = fileread(savePath);
disp(str)
{ + Show all accessible properties of Person +
Finally, we can save the collection
% Save the instances to the openMINDS userdata folder:
savePath = fullfile(userpath, "openMINDS_MATLAB", "demo", "crew_members.jsonld");
collection.save(savePath)
 
% Check out the saved metadata:
str = fileread(savePath);
disp(str)
{ "@context": { - "@vocab": "https://openminds.ebrains.eu/vocab/" + "@vocab": "https://openminds.om-i.org/props/" }, "@graph": [ + { + "@id": "_:arthur-dent@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "arthur-dent@hitchhikers-guide.galaxy" + }, { "@id": "_:arthur-dent", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:arthur-dent@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:arthur-dent@hitchhikers-guide.galaxy" - }, "familyName": "Dent", - "givenName": "Arthur" - }, - { - "@id": "_:arthur-dent@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "arthur-dent@hitchhikers-guide.galaxy" + "givenName": "Arthur", + "preferredName": "Arthur Dent" }, { - "@id": "_:heart-of-gold-spacecraft-crew", - "@type": "https://openminds.ebrains.eu/core/Consortium", - "fullName": "Heart of Gold Spacecraft Crew" + "@id": "_:ford-prefect@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "ford-prefect@hitchhikers-guide.galaxy" }, { "@id": "_:ford-prefect", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:ford-prefect@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:ford-prefect@hitchhikers-guide.galaxy" - }, "familyName": "Prefect", - "givenName": "Ford" + "givenName": "Ford", + "preferredName": "Ford Prefect" }, { - "@id": "_:ford-prefect@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "ford-prefect@hitchhikers-guide.galaxy" + "@id": "_:trillian-astra@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "trillian-astra@hitchhikers-guide.galaxy" }, { "@id": "_:tricia-marie-mcmillan", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "alternateName": "Trillian Astra", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:trillian-astra@hitchhikers-guide.galaxy" } ], - "alternateName": [ - "Trillian Astra" - ], - "contactInformation": { - "@id": "_:trillian-astra@hitchhikers-guide.galaxy" - }, "familyName": "McMillan", - "givenName": "Tricia Marie" + "givenName": "Tricia Marie", + "preferredName": "Tricia Marie McMillan" }, { - "@id": "_:trillian-astra@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "trillian-astra@hitchhikers-guide.galaxy" + "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "zaphod-beeblebrox@hitchhikers-guide.galaxy" }, { "@id": "_:zaphod-beeblebrox", - "@type": "https://openminds.ebrains.eu/core/Person", - "affiliation": [ + "@type": "https://openminds.om-i.org/types/Person", + "contactInformation": [ { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "memberOf": { - "@id": "_:heart-of-gold-spacecraft-crew" - } + "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" } ], - "contactInformation": { - "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy" - }, "familyName": "Beeblebrox", - "givenName": "Zaphod" + "givenName": "Zaphod", + "preferredName": "Zaphod Beeblebrox" }, { - "@id": "_:zaphod-beeblebrox@hitchhikers-guide.galaxy", - "@type": "https://openminds.ebrains.eu/core/ContactInformation", - "email": "zaphod-beeblebrox@hitchhikers-guide.galaxy" + "@id": "_:heart-of-gold-spacecraft-crew", + "@type": "https://openminds.om-i.org/types/Consortium", + "fullName": "Heart of Gold Spacecraft Crew", + "memberships": [ + { + "member": [ + { + "@id": "_:arthur-dent" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:ford-prefect" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:tricia-marie-mcmillan" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + }, + { + "member": [ + { + "@id": "_:zaphod-beeblebrox" + } + ], + "@type": "https://openminds.om-i.org/types/Membership" + } + ] } ] }
@@ -209,22 +238,23 @@ crewMembers = readtable(filePath, "TextType", "String") %% Create instances % Let us create a set of metadata instances from this table that represents -% the crew members. We assume that |memberOf| provides the full name of a consortium -% each person is affiliated to. Since members might be affiliated to the same -% consortium we assume further that the same full name means the same consortium. -% We can also assume that the |email| is unique for each person. +% the crew members. We assume that |memberOf| provides the full name of the consortium +% each person belongs to. A |*Consortium*| describes the group, while its |memberships| +% property lists the actors that belong to it. Since multiple people can belong +% to the same consortium, identical |memberOf| values are treated as references +% to the same |*Consortium*| instance. We also assume that |email| is unique for +% each person. % % With these assumptions we will create: %% % * a metadata |Collection| for storing metadata instances -% * a unique set of |Consortium| instances based on the name given in the |memberOf| -% column -% * a |ContactInformation| instance based on the |email| column -% * a |Person| instance for each table row with: -% * the |givenName|, |familyName|, and |alternateName| (if available) -% * a link to the respective |ContactInformation| instance -% * a person-specific embedded |Affiliation| instance that links to the respective -% |Consortium| instance +% * a unique set of |*Consortium*| instances based on the name given in the +% |memberOf| column +% * a |*ContactInformation*| instance for each unique email address +% * a |*Person*| instance for each table row, using |givenName|, |familyName|, +% |preferredName|, |alternateName| if available, and the corresponding |*ContactInformation*| +% * a |*Membership*| instance for each person, assigned to the corresponding +% |*Consortium*| %% % We start by creating an empty metadata collection for storing metadata instances. @@ -233,7 +263,7 @@ "Name", "Crew Members", ... "Description", "Crew members of the 'Heart of Gold' spacecraft") %% -% The collection will hold instances in a dictionary object of the Nodes property. +% The collection will hold instances in a dictionary object of the |Nodes| property. % Note: the |Name| and |Description| are optional and are currently not stored % with the metadata instances. % @@ -246,7 +276,8 @@ % with unique "Consortium" instances uniqueConsortiumNames = unique(crewMembers.memberOf); -consortia = dictionary; +% Create dictionary. Fall back to containers.Map for MATLAB < R2022b +try consortia = dictionary; catch; consortia = containers.Map; end for consortiumName = uniqueConsortiumNames' consortia(consortiumName) = openminds.core.Consortium(... 'id', createId(consortiumName), ... @@ -256,14 +287,14 @@ disp(consortia) %% % We have now created a dictionary that holds the |Consortium| instances. Since -% all the persons in this example belongs to the same consortium, this dictionary +% all the persons in this example belong to the same consortium, this dictionary % only holds one instance. % % We can also look at the |Consortium| instance in more detail: disp(consortia("Heart of Gold Spacecraft Crew")) %% -% The |Consortium| instance has four properties, and we have filled out |fullName|. +% The |Consortium| instance has five properties, and we have filled out |fullName|. % Whenever you create an openMINDS instance in MATLAB, you can either supply one % or more name-value pairs when you create the instance (as we did above), or % you can first create the instance and then assign values using dot-indexing @@ -286,7 +317,7 @@ % move on and create |ContactInformation| types for each of the persons: % Create a dictionary to hold "ContactInformation" instances -contacts = dictionary; +try contacts = dictionary; catch; contacts = containers.Map; end for email = crewMembers.email' contacts(email) = openminds.core.ContactInformation(... @@ -295,39 +326,54 @@ end disp(contacts) %% -% This gave us four |ContactInformation| instances. Finally we will create |Person| -% instances and attach the |ContactInformation| and |Consortium| instances: +% This gave us four |*ContactInformation*| instances. Next we create |*Person*| +% instances and link each one to its |*ContactInformation*| instance. Finally +% we create one |*Membership*| instance per person and assign those memberships +% to the consortium. % Extract data to create a list of "Person" instances where each "Person" -% instance will link to their respective "ContactInformation" instance and -% embed an "Affiliation" instance that links to the respective "Consortium" instance +% instance will link to their respective "ContactInformation" instance + persons = openminds.core.Person.empty; for iRow = 1:height(crewMembers) + personRow = crewMembers(iRow,:); + fullName = personRow.givenName + " " + personRow.familyName; - person = crewMembers(iRow,:); - fullName = person.givenName + " " + person.familyName; - persons(end+1) = openminds.core.Person( ... - 'id', createId(fullName), ... - 'givenName', person.givenName, ... - 'familyName', person.familyName, ... - 'alternateName', person.alternateName, ... - 'contactInformation', contacts(person.email), ... - 'affiliation', openminds.core.Affiliation('memberOf', consortia(person.memberOf) )); %#ok + "id", createId(fullName), ... + "givenName", personRow.givenName, ... + "familyName", personRow.familyName, ... + "preferredName", fullName, ... + "alternateName", personRow.alternateName, ... + "contactInformation", contacts(personRow.email)); %#ok end + +% Create memberships for each person / crew member +memberships = openminds.core.miscellaneous.Membership.empty; +for i = 1:numel(persons) + memberships(end+1) = openminds.core.miscellaneous.Membership( ... + "member", persons(i)); %#ok +end + +% Add crew members to the crew +crew = consortia("Heart of Gold Spacecraft Crew"); +crew.memberships = memberships; %% Add instances to collection and export collection % Now that we have all the instances, we can add them to the |collection|. It -% is sufficient to add the |Person| instances because the collection will automatically -% detect linked and embedded instances and add them automatically to the |Nodes| -% property. +% is sufficient to add the |crew| consortium. The collection will follow embedded +% and linked instances from there and add the detected metadata instances to the +% |Nodes| property. -collection.add(persons) +collection.add(crew) disp(collection) %% -% As described above, we see that the |Nodes| hold 9 instances. We can look -% at the |Nodes| and we should expect to see 4 |Person| instances, 4 |ContactInformation| -% instances and 1 |Consortium| instance. +% The |Nodes| dictionary contains 9 top-level instances: 4 |*Person*| instances, +% 4 |*ContactInformation*| instances, and 1 |*Consortium*| instance. The |*Membership*| +% instances are embedded in the |*Consortium*|, so they are serialized as part +% of the consortium rather than as separate top-level nodes. +% +% disp(collection.Nodes) %% @@ -337,9 +383,9 @@ if isMATLABReleaseOlderThan("R2023a") cellValue = collection.Nodes("_:arthur-dent"); - person = cellValue{1} + personRow = cellValue{1} else - person = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a + personRow = collection.Nodes{"_:arthur-dent"} % Curly brace syntax introduced in R2023a end %% % Finally, we can save the collection diff --git a/docs/tutorials/crewMemberCollection.md b/docs/tutorials/crewMemberCollection.md index 60cfaef86..6b9778028 100644 --- a/docs/tutorials/crewMemberCollection.md +++ b/docs/tutorials/crewMemberCollection.md @@ -149,7 +149,7 @@ disp(contacts) "zaphod-beeblebrox@hitchhikers-guide.galaxy" ⟼ [zaphod-beeblebrox@hitchhikers-guide.galaxy] (ContactInformation) ``` -This gave us four **`ContactInformation`** instances. Next we create **`Person`** instances and each one to its **`ContactInformation`**. Finally we create one **`Membership`** instance per person and assign those memberships to the consortium. +This gave us four **`ContactInformation`** instances. Next we create **`Person`** instances and link each one to its **`ContactInformation`** instance. Finally we create one **`Membership`** instance per person and assign those memberships to the consortium. ```matlab % Extract data to create a list of "Person" instances where each "Person" From b8386ca23cf2c7ccc9fa0db5569c83e2858a18fc Mon Sep 17 00:00:00 2001 From: ehennestad Date: Tue, 28 Apr 2026 13:36:24 +0200 Subject: [PATCH 05/15] fix: activate versioned controlled term bases --- .../+openminds/+abstract/ControlledTerm.m | 88 ++++++-- .../+abstract/private/ControlledTerm_v2.m | 209 +++++++++++++++++ .../+abstract/private/ControlledTerm_v3.m | 212 ++++++++++++++++++ .../+internal/activateControlledTermBase.m | 46 ++++ .../+openminds/selectOpenMindsVersion.m | 6 + tools/tests/unitTests/ControlledTermTest.m | 62 +++++ 6 files changed, 602 insertions(+), 21 deletions(-) create mode 100644 code/internal/+openminds/+abstract/private/ControlledTerm_v2.m create mode 100644 code/internal/+openminds/+abstract/private/ControlledTerm_v3.m create mode 100644 code/internal/+openminds/+internal/activateControlledTermBase.m create mode 100644 tools/tests/unitTests/ControlledTermTest.m diff --git a/code/internal/+openminds/+abstract/ControlledTerm.m b/code/internal/+openminds/+abstract/ControlledTerm.m index fce42393d..49cd61a98 100644 --- a/code/internal/+openminds/+abstract/ControlledTerm.m +++ b/code/internal/+openminds/+abstract/ControlledTerm.m @@ -4,7 +4,7 @@ properties (Access = protected) Required = {'name'} end - + properties % Enter one sentence for defining this term. definition (1,1) string @@ -12,26 +12,35 @@ % Enter a short text describing this term. description (1,1) string - % Enter the internationalized resource identifier (IRI) pointing to the integrated ontology entry in the InterLex project. - interlexIdentifier (1,1) string - - % Enter the internationalized resource identifier (IRI) pointing to the wiki page of the corresponding term in the KnowledgeSpace. - knowledgeSpaceLink (1,1) string - % Controlled term originating from a defined terminology. name (1,1) string + % Enter all IRIs pointing to cross-references to external databases or registries that are equivalent to this term. + otherCrossReference (1,:) string {mustBeListOfUniqueItems(otherCrossReference)} + + % Enter all IRIs pointing to ontology entries that are equivalent to this term. + otherOntologyIdentifier (1,:) string {mustBeListOfUniqueItems(otherOntologyIdentifier)} + + % Enter the IRI pointing to the preferred cross-reference to an external database or registry. + preferredCrossReference (1,1) string + % Enter the internationalized resource identifier (IRI) pointing to the preferred ontological term. preferredOntologyIdentifier (1,1) string % Enter one or several synonyms (including abbreviations) for this controlled term. synonym (1,:) string {mustBeListOfUniqueItems(synonym)} + + % Add an existing terminology in which the suggested term should be integrated in. + addExistingTerminology (1,:) openminds.controlledterms.Terminology + + % Propose a name for a new terminology in which the suggested term should be integrated in. + suggestNewTerminology (1,1) string end properties (SetAccess = protected, Hidden) % Todo: Same as id, clean up at_id end - + properties (Constant, Hidden) LINKED_PROPERTIES = struct() EMBEDDED_PROPERTIES = struct() @@ -43,7 +52,7 @@ methods function obj = ControlledTerm(instanceSpec, propValues) - + arguments instanceSpec = [] propValues.?openminds.abstract.ControlledTerm @@ -71,7 +80,7 @@ % Deserialize from name of controlled instance obj.deserializeFromName(instanceSpec); end - elseif isstruct( instanceSpec ) && isfield(instanceSpec, 'at_id') || isfield(instanceSpec, 'x_id') + elseif isstruct( instanceSpec ) && (isfield(instanceSpec, 'at_id') || isfield(instanceSpec, 'x_id')) numInstances = numel(instanceSpec); if numInstances > 1 obj(numInstances) = feval(class(obj)); @@ -93,13 +102,13 @@ obj.warnIfPropValuesSupplied(names) else obj.set(propValues) - if ismissing(obj.id) + if ismissing(obj.id) || obj.id == "" obj.id = obj.generateInstanceId(); end end end end - + methods (Access = protected) % Implement method for the CustomInstanceDisplay mixin function str = getDisplayLabel(obj) str = sprintf('%s', obj.name); @@ -119,18 +128,19 @@ function deserializeFromName(obj, instanceName) import openminds.internal.utility.getSchemaName instanceName = char(instanceName); + instanceIRI = ""; schemaName = getSchemaName(class(obj)); if openminds.utility.isIRI(instanceName) if openminds.utility.isInstanceIRI(instanceName) + instanceIRI = string(instanceName); [~, instanceName] = openminds.utility.parseInstanceIRI(instanceName); else obj.id = instanceName; return end end - - [instanceName, instanceNameOrig] = deal(instanceName); + if ~any(strcmp(obj.CONTROLLED_INSTANCES, instanceName)) % Try to make a valid name instanceName = strrep(instanceName, ' ', ''); @@ -138,13 +148,21 @@ function deserializeFromName(obj, instanceName) end % Todo: Use a proper deserializer - if any(strcmpi(obj.CONTROLLED_INSTANCES, instanceName)) + isMatchingInstance = strcmpi(obj.CONTROLLED_INSTANCES, instanceName); + if any(isMatchingInstance) + instanceName = obj.CONTROLLED_INSTANCES(find(isMatchingInstance, 1, 'first')); + obj.name = instanceName; + if instanceIRI == "" + obj.id = obj.createControlledInstanceIRI(schemaName, instanceName); + else + obj.id = instanceIRI; + end + try data = getControlledInstance(instanceName, schemaName, 'controlledTerms'); catch - s = warning('off', 'backtrace'); - warningCleanup = onCleanup(@() warning(s)); - warning('Controlled instance "%s" is not available.', instanceNameOrig) + % Known instance names are sufficient identifiers. The + % JSON-LD instance file is only used to enrich metadata. return end else @@ -152,15 +170,43 @@ function deserializeFromName(obj, instanceName) return % error('Deserialization from user instance is not implemented yet') end - propNames = {'at_id', 'name', 'definition', 'description', 'interlexIdentifier', 'knowledgeSpaceLink', 'preferredOntologyIdentifier', 'synonym'}; + propNames = [{'at_id'}, properties(obj)']; for i = 1:numel(propNames) - if ~isempty( data.(propNames{i}) ) + if isfield(data, propNames{i}) && ~obj.isEmptyValue(data.(propNames{i})) obj.(propNames{i}) = data.(propNames{i}); end end - obj.id = obj.at_id; + if instanceIRI == "" && ~obj.isEmptyValue(obj.at_id) + obj.id = obj.at_id; + end + end + end + + methods (Static, Access = private) + function instanceIRI = createControlledInstanceIRI(schemaName, instanceName) + instanceIRI = openminds.constant.BaseURI + "/instances/" ... + + openminds.abstract.ControlledTerm.getInstanceTypeName(schemaName) ... + + "/" + string(instanceName); + end + + function typeName = getInstanceTypeName(schemaName) + typeName = char(schemaName); + if ~strcmp(upper(typeName(1:2)), typeName(1:2)) + typeName(1) = lower(typeName(1)); + end + typeName = string(typeName); + end + + function tf = isEmptyValue(value) + if isempty(value) + tf = true; + elseif isstring(value) + tf = all(ismissing(value) | value == ""); + else + tf = false; + end end end end diff --git a/code/internal/+openminds/+abstract/private/ControlledTerm_v2.m b/code/internal/+openminds/+abstract/private/ControlledTerm_v2.m new file mode 100644 index 000000000..15501fe01 --- /dev/null +++ b/code/internal/+openminds/+abstract/private/ControlledTerm_v2.m @@ -0,0 +1,209 @@ +classdef (Abstract) ControlledTerm < openminds.abstract.Schema +%ControlledTerm Abstract base class for metadata types of the controlled terms module + + properties (Access = protected) + Required = {'name'} + end + + properties + % Enter one sentence for defining this term. + definition (1,1) string + + % Enter a short text describing this term. + description (1,1) string + + % Enter the internationalized resource identifier (IRI) pointing to the integrated ontology entry in the InterLex project. + interlexIdentifier (1,1) string + + % Enter the internationalized resource identifier (IRI) pointing to the wiki page of the corresponding term in the KnowledgeSpace. + knowledgeSpaceLink (1,1) string + + % Controlled term originating from a defined terminology. + name (1,1) string + + % Enter the internationalized resource identifier (IRI) pointing to the preferred ontological term. + preferredOntologyIdentifier (1,1) string + + % Enter one or several synonyms (including abbreviations) for this controlled term. + synonym (1,:) string {mustBeListOfUniqueItems(synonym)} + + % Add an existing terminology in which the suggested term should be integrated in. + addExistingTerminology (1,:) openminds.controlledterms.Terminology + + % Propose a name for a new terminology in which the suggested term should be integrated in. + suggestNewTerminology (1,1) string + end + + properties (SetAccess = protected, Hidden) % Todo: Same as id, clean up + at_id + end + + properties (Constant, Hidden) + LINKED_PROPERTIES = struct() + EMBEDDED_PROPERTIES = struct() + end + + properties (Abstract, Constant, Hidden) + CONTROLLED_INSTANCES + end + + methods + function obj = ControlledTerm(instanceSpec, propValues) + + arguments + instanceSpec = [] + propValues.?openminds.abstract.ControlledTerm + propValues.id (1,1) string + end + + if isstring(instanceSpec) && isscalar(instanceSpec) && instanceSpec == "" + instanceSpec = string.empty; + end + + if ~isempty(instanceSpec) + if ischar(instanceSpec) + instanceSpec = string(instanceSpec); + end + + if isstring( instanceSpec ) && ~ismissing(instanceSpec) + % Check IRI first, because isfile will also check IRIs + % and that is expensive (we only want to check local + % files anyway) + if startsWith(instanceSpec, openminds.constant.BaseURI) + obj.deserializeFromName(instanceSpec); + elseif isfile( instanceSpec ) + obj.load( instanceSpec ) % todo: Not implemented?? + else + % Deserialize from name of controlled instance + obj.deserializeFromName(instanceSpec); + end + elseif isstruct( instanceSpec ) && (isfield(instanceSpec, 'at_id') || isfield(instanceSpec, 'x_id')) + numInstances = numel(instanceSpec); + if numInstances > 1 + obj(numInstances) = feval(class(obj)); + end + for i = 1:numel(instanceSpec) + if isfield(instanceSpec(i), 'at_id') + iri = instanceSpec(i).at_id; + elseif isfield(instanceSpec(i), 'x_id') + iri = instanceSpec(i).x_id; + end + obj(i).deserializeFromName(iri); + end + else + error('openMINDS:ControlledTerm:InvalidInput', ... + 'Expected instance spec to be a name, a filename or a structure with `at_id` field.') + end + + names = fieldnames(propValues); + obj.warnIfPropValuesSupplied(names) + else + obj.set(propValues) + if ismissing(obj.id) || obj.id == "" + obj.id = obj.generateInstanceId(); + end + end + end + end + + methods (Access = protected) % Implement method for the CustomInstanceDisplay mixin + function str = getDisplayLabel(obj) + str = sprintf('%s', obj.name); + end + end + + methods (Hidden) + function str = char(obj) + str = char(string(obj.name)); + end + end + + methods (Access = private) + function deserializeFromName(obj, instanceName) + + import openminds.internal.getControlledInstance + import openminds.internal.utility.getSchemaName + + instanceName = char(instanceName); + instanceIRI = ""; + schemaName = getSchemaName(class(obj)); + + if openminds.utility.isIRI(instanceName) + if openminds.utility.isInstanceIRI(instanceName) + instanceIRI = string(instanceName); + [~, instanceName] = openminds.utility.parseInstanceIRI(instanceName); + else + obj.id = instanceName; + return + end + end + + if ~any(strcmp(obj.CONTROLLED_INSTANCES, instanceName)) + % Try to make a valid name + instanceName = strrep(instanceName, ' ', ''); + instanceName = matlab.lang.makeValidName(instanceName, 'ReplacementStyle', 'delete'); + end + + % Todo: Use a proper deserializer + isMatchingInstance = strcmpi(obj.CONTROLLED_INSTANCES, instanceName); + if any(isMatchingInstance) + instanceName = obj.CONTROLLED_INSTANCES(find(isMatchingInstance, 1, 'first')); + obj.name = instanceName; + if instanceIRI == "" + obj.id = obj.createControlledInstanceIRI(schemaName, instanceName); + else + obj.id = instanceIRI; + end + + try + data = getControlledInstance(instanceName, schemaName, 'controlledTerms'); + catch + % Known instance names are sufficient identifiers. The + % JSON-LD instance file is only used to enrich metadata. + return + end + else + warning('No matching instances were found for name "%s"', instanceName) + return + % error('Deserialization from user instance is not implemented yet') + end + + propNames = [{'at_id'}, properties(obj)']; + for i = 1:numel(propNames) + if isfield(data, propNames{i}) && ~obj.isEmptyValue(data.(propNames{i})) + obj.(propNames{i}) = data.(propNames{i}); + end + end + + if instanceIRI == "" && ~obj.isEmptyValue(obj.at_id) + obj.id = obj.at_id; + end + end + end + + methods (Static, Access = private) + function instanceIRI = createControlledInstanceIRI(schemaName, instanceName) + instanceIRI = openminds.constant.BaseURI + "/instances/" ... + + openminds.abstract.ControlledTerm.getInstanceTypeName(schemaName) ... + + "/" + string(instanceName); + end + + function typeName = getInstanceTypeName(schemaName) + typeName = char(schemaName); + if ~strcmp(upper(typeName(1:2)), typeName(1:2)) + typeName(1) = lower(typeName(1)); + end + typeName = string(typeName); + end + + function tf = isEmptyValue(value) + if isempty(value) + tf = true; + elseif isstring(value) + tf = all(ismissing(value) | value == ""); + else + tf = false; + end + end + end +end diff --git a/code/internal/+openminds/+abstract/private/ControlledTerm_v3.m b/code/internal/+openminds/+abstract/private/ControlledTerm_v3.m new file mode 100644 index 000000000..49cd61a98 --- /dev/null +++ b/code/internal/+openminds/+abstract/private/ControlledTerm_v3.m @@ -0,0 +1,212 @@ +classdef (Abstract) ControlledTerm < openminds.abstract.Schema +%ControlledTerm Abstract base class for metadata types of the controlled terms module + + properties (Access = protected) + Required = {'name'} + end + + properties + % Enter one sentence for defining this term. + definition (1,1) string + + % Enter a short text describing this term. + description (1,1) string + + % Controlled term originating from a defined terminology. + name (1,1) string + + % Enter all IRIs pointing to cross-references to external databases or registries that are equivalent to this term. + otherCrossReference (1,:) string {mustBeListOfUniqueItems(otherCrossReference)} + + % Enter all IRIs pointing to ontology entries that are equivalent to this term. + otherOntologyIdentifier (1,:) string {mustBeListOfUniqueItems(otherOntologyIdentifier)} + + % Enter the IRI pointing to the preferred cross-reference to an external database or registry. + preferredCrossReference (1,1) string + + % Enter the internationalized resource identifier (IRI) pointing to the preferred ontological term. + preferredOntologyIdentifier (1,1) string + + % Enter one or several synonyms (including abbreviations) for this controlled term. + synonym (1,:) string {mustBeListOfUniqueItems(synonym)} + + % Add an existing terminology in which the suggested term should be integrated in. + addExistingTerminology (1,:) openminds.controlledterms.Terminology + + % Propose a name for a new terminology in which the suggested term should be integrated in. + suggestNewTerminology (1,1) string + end + + properties (SetAccess = protected, Hidden) % Todo: Same as id, clean up + at_id + end + + properties (Constant, Hidden) + LINKED_PROPERTIES = struct() + EMBEDDED_PROPERTIES = struct() + end + + properties (Abstract, Constant, Hidden) + CONTROLLED_INSTANCES + end + + methods + function obj = ControlledTerm(instanceSpec, propValues) + + arguments + instanceSpec = [] + propValues.?openminds.abstract.ControlledTerm + propValues.id (1,1) string + end + + if isstring(instanceSpec) && isscalar(instanceSpec) && instanceSpec == "" + instanceSpec = string.empty; + end + + if ~isempty(instanceSpec) + if ischar(instanceSpec) + instanceSpec = string(instanceSpec); + end + + if isstring( instanceSpec ) && ~ismissing(instanceSpec) + % Check IRI first, because isfile will also check IRIs + % and that is expensive (we only want to check local + % files anyway) + if startsWith(instanceSpec, openminds.constant.BaseURI) + obj.deserializeFromName(instanceSpec); + elseif isfile( instanceSpec ) + obj.load( instanceSpec ) % todo: Not implemented?? + else + % Deserialize from name of controlled instance + obj.deserializeFromName(instanceSpec); + end + elseif isstruct( instanceSpec ) && (isfield(instanceSpec, 'at_id') || isfield(instanceSpec, 'x_id')) + numInstances = numel(instanceSpec); + if numInstances > 1 + obj(numInstances) = feval(class(obj)); + end + for i = 1:numel(instanceSpec) + if isfield(instanceSpec(i), 'at_id') + iri = instanceSpec(i).at_id; + elseif isfield(instanceSpec(i), 'x_id') + iri = instanceSpec(i).x_id; + end + obj(i).deserializeFromName(iri); + end + else + error('openMINDS:ControlledTerm:InvalidInput', ... + 'Expected instance spec to be a name, a filename or a structure with `at_id` field.') + end + + names = fieldnames(propValues); + obj.warnIfPropValuesSupplied(names) + else + obj.set(propValues) + if ismissing(obj.id) || obj.id == "" + obj.id = obj.generateInstanceId(); + end + end + end + end + + methods (Access = protected) % Implement method for the CustomInstanceDisplay mixin + function str = getDisplayLabel(obj) + str = sprintf('%s', obj.name); + end + end + + methods (Hidden) + function str = char(obj) + str = char(string(obj.name)); + end + end + + methods (Access = private) + function deserializeFromName(obj, instanceName) + + import openminds.internal.getControlledInstance + import openminds.internal.utility.getSchemaName + + instanceName = char(instanceName); + instanceIRI = ""; + schemaName = getSchemaName(class(obj)); + + if openminds.utility.isIRI(instanceName) + if openminds.utility.isInstanceIRI(instanceName) + instanceIRI = string(instanceName); + [~, instanceName] = openminds.utility.parseInstanceIRI(instanceName); + else + obj.id = instanceName; + return + end + end + + if ~any(strcmp(obj.CONTROLLED_INSTANCES, instanceName)) + % Try to make a valid name + instanceName = strrep(instanceName, ' ', ''); + instanceName = matlab.lang.makeValidName(instanceName, 'ReplacementStyle', 'delete'); + end + + % Todo: Use a proper deserializer + isMatchingInstance = strcmpi(obj.CONTROLLED_INSTANCES, instanceName); + if any(isMatchingInstance) + instanceName = obj.CONTROLLED_INSTANCES(find(isMatchingInstance, 1, 'first')); + obj.name = instanceName; + if instanceIRI == "" + obj.id = obj.createControlledInstanceIRI(schemaName, instanceName); + else + obj.id = instanceIRI; + end + + try + data = getControlledInstance(instanceName, schemaName, 'controlledTerms'); + catch + % Known instance names are sufficient identifiers. The + % JSON-LD instance file is only used to enrich metadata. + return + end + else + warning('No matching instances were found for name "%s"', instanceName) + return + % error('Deserialization from user instance is not implemented yet') + end + + propNames = [{'at_id'}, properties(obj)']; + for i = 1:numel(propNames) + if isfield(data, propNames{i}) && ~obj.isEmptyValue(data.(propNames{i})) + obj.(propNames{i}) = data.(propNames{i}); + end + end + + if instanceIRI == "" && ~obj.isEmptyValue(obj.at_id) + obj.id = obj.at_id; + end + end + end + + methods (Static, Access = private) + function instanceIRI = createControlledInstanceIRI(schemaName, instanceName) + instanceIRI = openminds.constant.BaseURI + "/instances/" ... + + openminds.abstract.ControlledTerm.getInstanceTypeName(schemaName) ... + + "/" + string(instanceName); + end + + function typeName = getInstanceTypeName(schemaName) + typeName = char(schemaName); + if ~strcmp(upper(typeName(1:2)), typeName(1:2)) + typeName(1) = lower(typeName(1)); + end + typeName = string(typeName); + end + + function tf = isEmptyValue(value) + if isempty(value) + tf = true; + elseif isstring(value) + tf = all(ismissing(value) | value == ""); + else + tf = false; + end + end + end +end diff --git a/code/internal/+openminds/+internal/activateControlledTermBase.m b/code/internal/+openminds/+internal/activateControlledTermBase.m new file mode 100644 index 000000000..eeca50712 --- /dev/null +++ b/code/internal/+openminds/+internal/activateControlledTermBase.m @@ -0,0 +1,46 @@ +function controlledTermVersion = activateControlledTermBase(modelVersion) +%activateControlledTermBase Activate the controlled-term base for a model version. + + arguments + modelVersion (1,1) openminds.internal.utility.VersionNumber ... + {openminds.mustBeValidVersion(modelVersion)} = "latest" + end + + controlledTermVersion = getControlledTermVersion(modelVersion); + + rootPath = openminds.internal.rootpath(); + abstractFolder = fullfile(rootPath, "internal", "+openminds", "+abstract"); + sourceFile = fullfile(abstractFolder, "private", ... + "ControlledTerm_" + controlledTermVersion + ".m"); + targetFile = fullfile(abstractFolder, "ControlledTerm.m"); + + if ~isfile(sourceFile) + error("openMINDS:ControlledTerm:MissingVersionedBase", ... + 'No controlled-term base exists for version "%s".', controlledTermVersion) + end + + sourceText = fileread(sourceFile); + if isfile(targetFile) + targetText = fileread(targetFile); + else + targetText = ''; + end + + if ~strcmp(sourceText, targetText) + [success, message] = copyfile(sourceFile, targetFile, "f"); + if ~success + error("openMINDS:ControlledTerm:ActivationFailed", ... + 'Could not activate controlled-term base "%s": %s', ... + controlledTermVersion, message) + end + rehash + end +end + +function controlledTermVersion = getControlledTermVersion(modelVersion) + if modelVersion >= 5 + controlledTermVersion = "v3"; + else + controlledTermVersion = "v2"; + end +end diff --git a/code/internal/+openminds/selectOpenMindsVersion.m b/code/internal/+openminds/selectOpenMindsVersion.m index dd9a34120..5e68ebd0a 100644 --- a/code/internal/+openminds/selectOpenMindsVersion.m +++ b/code/internal/+openminds/selectOpenMindsVersion.m @@ -50,6 +50,8 @@ function selectOpenMindsVersion(versionNumber) addpath( genpath( fullfile(rootPath, 'internal') ) ) addpath( genpath( fullfile(rootPath, 'livescripts') ) ) + openminds.internal.activateControlledTermBase(versionNumber); + % Get version number as string matching version numbers of version folders if versionNumber == "latest" versionAsString = 'latest'; @@ -69,6 +71,10 @@ function selectOpenMindsVersion(versionNumber) addpath(genpath( fullfile(rootPath, "mixedtypes", versionAsString) )) addpath(genpath( fullfile(rootPath, "enumerations", versionAsString) )) + % Version selection can replace shared abstract class files. + % Clear cached class definitions so MATLAB sees the active files. + evalin('base', 'clear classes') + % Add a second pause for changes to take effect. pause(1) % Ad hoc value. Usually at least 0.3 - 0.4 seconds is necessary end diff --git a/tools/tests/unitTests/ControlledTermTest.m b/tools/tests/unitTests/ControlledTermTest.m new file mode 100644 index 000000000..ee3d5a19c --- /dev/null +++ b/tools/tests/unitTests/ControlledTermTest.m @@ -0,0 +1,62 @@ +classdef ControlledTermTest < matlab.unittest.TestCase + + methods (Test) + function testKnownInstanceCreatesLightweightReference(testCase) + term = testCase.verifyWarningFree( ... + @() openminds.controlledterms.ContributionType("authoring")); + + testCase.verifyEqual(term.name, "authoring") + testCase.verifyEqual(term.id, ... + openminds.constant.BaseURI + "/instances/contributionType/authoring") + end + + function testOlderControlledTermPropertiesAreAccepted(testCase) + sourceText = fileread(testCase.getControlledTermBasePath("v2")); + + testCase.verifyTrue(contains(sourceText, "interlexIdentifier")) + testCase.verifyTrue(contains(sourceText, "knowledgeSpaceLink")) + testCase.verifyFalse(contains(sourceText, "otherCrossReference")) + end + + function testNewerControlledTermPropertiesAreAccepted(testCase) + term = openminds.controlledterms.ContributionType( ... + [], ... + "name", "authoring", ... + "preferredCrossReference", "https://example.org/preferred", ... + "otherCrossReference", "https://example.org/cross-reference", ... + "otherOntologyIdentifier", "https://example.org/ontology"); + + testCase.verifyEqual(term.preferredCrossReference, "https://example.org/preferred") + testCase.verifyEqual(term.otherCrossReference, "https://example.org/cross-reference") + testCase.verifyEqual(term.otherOntologyIdentifier, "https://example.org/ontology") + end + + function testTermSuggestionPropertiesAreAccepted(testCase) + terminology = openminds.controlledterms.Terminology([], "name", "example"); + term = openminds.controlledterms.TermSuggestion( ... + [], ... + "name", "candidate", ... + "addExistingTerminology", terminology, ... + "suggestNewTerminology", "new terminology"); + + testCase.verifyEqual(term.addExistingTerminology, terminology) + testCase.verifyEqual(term.suggestNewTerminology, "new terminology") + end + + function testLatestControlledTermBaseDoesNotExposeOlderProperties(testCase) + term = openminds.controlledterms.ContributionType(); + propertyNames = string(properties(term)); + + testCase.verifyFalse(ismember("interlexIdentifier", propertyNames)) + testCase.verifyFalse(ismember("knowledgeSpaceLink", propertyNames)) + end + end + + methods (Access = private) + function filePath = getControlledTermBasePath(~, version) + rootPath = openminds.internal.rootpath(); + filePath = fullfile(rootPath, "internal", "+openminds", ... + "+abstract", "private", "ControlledTerm_" + version + ".m"); + end + end +end From ae3e9f10d1f502711a348ef443c9200146634238 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Tue, 28 Apr 2026 17:15:42 +0200 Subject: [PATCH 06/15] Move tutorial export function and add private helper --- {docs => tools/+ommtools}/exportTutorials.m | 3 + .../private/postProcessLivescriptHtml.m | 69 +++++++++++++++++++ 2 files changed, 72 insertions(+) rename {docs => tools/+ommtools}/exportTutorials.m (81%) create mode 100644 tools/+ommtools/private/postProcessLivescriptHtml.m diff --git a/docs/exportTutorials.m b/tools/+ommtools/exportTutorials.m similarity index 81% rename from docs/exportTutorials.m rename to tools/+ommtools/exportTutorials.m index 4dcff3aa9..943469215 100644 --- a/docs/exportTutorials.m +++ b/tools/+ommtools/exportTutorials.m @@ -14,6 +14,9 @@ function exportTutorials() for j = 1:numel(exportFormat) export(sourcePath, strrep(targetPath, '.mlx', exportFormat(j))); + if exportFormat(j) == ".html" + postProcessLivescriptHtml(strrep(targetPath, '.mlx', exportFormat(j))) + end end end end diff --git a/tools/+ommtools/private/postProcessLivescriptHtml.m b/tools/+ommtools/private/postProcessLivescriptHtml.m new file mode 100644 index 000000000..5665634dd --- /dev/null +++ b/tools/+ommtools/private/postProcessLivescriptHtml.m @@ -0,0 +1,69 @@ +function postProcessLivescriptHtml(htmlFile) +%POSTPROCESSLIVESCRIPHTML Postprocess livescript HTMLs for improved online functionality +% +% This function performs the following actions: + +% +% Syntax: +% postProcessLivescriptHtml(htmlFile) +% +% Input: +% htmlFile - (1,1) string: Path to the HTML file to process. +% +% Example: +% postProcessLivescriptHtml("example.html"); + + arguments + htmlFile (1,1) string {mustBeFile} + end + + % Read the content of the HTML file + htmlContent = fileread(htmlFile); + + % Remove unstable MATLAB-generated metadata from output wrappers. + htmlContent = stripOutputWrapperUidAttributes(htmlContent); + htmlContent = normalizeVariableEditorIds(htmlContent); + + % Write the modified content back to the HTML file + try + fid = fopen(htmlFile, 'wt'); + if fid == -1 + error('Could not open the file for writing: %s', htmlFile); + end + fwrite(fid, htmlContent, 'char'); + fclose(fid); + catch + error('Could not write to the file: %s', htmlFile); + end +end + +function htmlContent = stripOutputWrapperUidAttributes(htmlContent) + expr = '(]*class\s*=\s*"[^"]*eoOutputWrapper[^"]*")\s+uid="[^"]*"'; + htmlContent = regexprep(htmlContent, expr, '$1'); +end + +function htmlContent = normalizeVariableEditorIds(htmlContent) + expr = 'variableeditor_(client_Document|views_SummaryBar|TableViewModel)_([0-9]+)'; + tokens = regexp(htmlContent, expr, 'tokens'); + + if isempty(tokens) + return + end + + originalSuffixes = cellfun(@(token) token{2}, tokens, 'UniformOutput', false); + uniqueSuffixes = unique(originalSuffixes, 'stable'); + + for i = 1:numel(uniqueSuffixes) + oldSuffix = uniqueSuffixes{i}; + tempSuffix = sprintf('__omm_variableeditor_%d__', i); + htmlContent = regexprep(htmlContent, ... + ['(variableeditor_(?:client_Document|views_SummaryBar|TableViewModel)_)' oldSuffix], ... + ['$1' tempSuffix]); + end + + for i = 1:numel(uniqueSuffixes) + tempSuffix = sprintf('__omm_variableeditor_%d__', i); + newSuffix = string(i); + htmlContent = strrep(htmlContent, tempSuffix, char(newSuffix)); + end +end From 90e0b8c509066aca7854f2322a3304d17aa851a7 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Tue, 28 Apr 2026 17:19:46 +0200 Subject: [PATCH 07/15] Update basicNeuroscienceDataset.html --- docs/tutorials/basicNeuroscienceDataset.html | 86 ++++++++++++-------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/docs/tutorials/basicNeuroscienceDataset.html b/docs/tutorials/basicNeuroscienceDataset.html index 7c7c0a23e..ff845d162 100644 --- a/docs/tutorials/basicNeuroscienceDataset.html +++ b/docs/tutorials/basicNeuroscienceDataset.html @@ -1,21 +1,27 @@ -Creating openMINDS Metadata: A Basic Introduction

Creating openMINDS Metadata: A Basic Introduction

This tutorial demonstrates how to create, link, and save metadata using the openMINDS MATLAB toolbox. We'll create a simplified neuroscience dataset with subjects, researchers, and experimental details.
The openMINDS (Open Metadata Initiative for Neuroscience Data Structures) provides standardized metadata models for neuroscience data. This standardization facilitates data sharing, discovery, and reuse.
Please refer to the openMINDS documentation to learn more about the available metadata types.
This tutorial covers:

1. Creating a Metadata Collection

We start by creating an empty metadata collection that will hold all our instances. A collection is a container for metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Neuroscience Dataset Example", ...
"Description", "A tutorial dataset for learning openMINDS metadata creation");
 
disp(collection)
Collection with properties: +6. Summary

1. Creating a Metadata Collection

We start by creating an empty metadata collection that will hold all our instances. A collection is a container for metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Neuroscience Dataset Example", ...
"Description", "A tutorial dataset for learning openMINDS metadata creation");
 
disp(collection)
Collection with properties: Name: "Neuroscience Dataset Example" Description: "A tutorial dataset for learning openMINDS metadata creation" Nodes: dictionary with unset key and value types - LinkResolver: []

2. Creating Basic Metadata Instances

Let's create instances for researchers and their organizations. We'll demonstrate two ways to create instances: 1. Using name-value pairs in the constructor 2. Creating an empty instance and setting properties via dot notation
% Define a utility function for creating instance IDs
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));

2.1 Create organization instances

% First approach: Using name-value pairs in the constructor
university = openminds.core.actors.Organization(...
'id', createId('University of Neuroscience'), ...
'fullName', 'University of Neuroscience', ...
'shortName', 'UNS');
 
% Second approach: Create empty instance and set properties
researchCenter = openminds.core.actors.Organization();
researchCenter.id = createId('Brain Research Center');
researchCenter.fullName = 'Brain Research Center';
researchCenter.shortName = 'BRC';
 
% Display the Organization metadata instances:
disp(university);
Organization (https://openminds.om-i.org/types/Organization) with properties: + LinkResolver: []

2. Creating Basic Metadata Instances

Let's create instances for researchers and their organizations. We'll demonstrate two ways to create instances: 1. Using name-value pairs in the constructor 2. Creating an empty instance and setting properties via dot notation
% Define a utility function for creating instance IDs
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));

2.1 Create organization instances

% First approach: Using name-value pairs in the constructor
university = openminds.core.actors.Organization(...
'id', createId('University of Neuroscience'), ...
'fullName', 'University of Neuroscience', ...
'shortName', 'UNS');
 
% Second approach: Create empty instance and set properties
researchCenter = openminds.core.actors.Organization();
researchCenter.id = createId('Brain Research Center');
researchCenter.fullName = 'Brain Research Center';
researchCenter.shortName = 'BRC';
 
% Display the Organization metadata instances:
disp(university);
Organization (https://openminds.om-i.org/types/Organization) with properties: affiliation: [None] (Affiliation) digitalIdentifier: [None] (Any of: GRIDID, RORID, RRID) @@ -107,7 +114,7 @@ homepage: "" shortName: "UNS" - Required Properties: fullName
disp(researchCenter);
Organization (https://openminds.om-i.org/types/Organization) with properties: + Required Properties: fullName
disp(researchCenter);
Organization (https://openminds.om-i.org/types/Organization) with properties: affiliation: [None] (Affiliation) digitalIdentifier: [None] (Any of: GRIDID, RORID, RRID) @@ -116,15 +123,15 @@ homepage: "" shortName: "BRC" - Required Properties: fullName

2.2 Create contact information instances

contactPI = openminds.core.actors.ContactInformation(...
'id', createId('contact-pi'), ...
'email', 'pi@neuroscience.edu');
 
contactPostdoc = openminds.core.actors.ContactInformation(...
'id', createId('contact-postdoc'), ...
'email', 'postdoc@neuroscience.edu');
 
disp(contactPI)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: + Required Properties: fullName

2.2 Create contact information instances

contactPI = openminds.core.actors.ContactInformation(...
'id', createId('contact-pi'), ...
'email', 'pi@neuroscience.edu');
 
contactPostdoc = openminds.core.actors.ContactInformation(...
'id', createId('contact-postdoc'), ...
'email', 'postdoc@neuroscience.edu');
 
disp(contactPI)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: email: "pi@neuroscience.edu" - Required Properties: email
disp(contactPostdoc)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: + Required Properties: email
disp(contactPostdoc)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: email: "postdoc@neuroscience.edu" - Required Properties: email

2.3 Create person instances with affiliations

% Principal Investigator
pi = openminds.core.actors.Person(...
'id', createId('jane-doe'), ...
'givenName', 'Jane', ...
'familyName', 'Doe', ...
'contactInformation', contactPI, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', university));
 
% Postdoc
postdoc = openminds.core.actors.Person(...
'id', createId('john-smith'), ...
'givenName', 'John', ...
'familyName', 'Smith', ...
'contactInformation', contactPostdoc, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', researchCenter));
 
% Display the Person metadata instances:
disp(pi)
Person (https://openminds.om-i.org/types/Person) with properties: + Required Properties: email

2.3 Create person instances with affiliations

% Principal Investigator
pi = openminds.core.actors.Person(...
'id', createId('jane-doe'), ...
'givenName', 'Jane', ...
'familyName', 'Doe', ...
'contactInformation', contactPI, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', university));
 
% Postdoc
postdoc = openminds.core.actors.Person(...
'id', createId('john-smith'), ...
'givenName', 'John', ...
'familyName', 'Smith', ...
'contactInformation', contactPostdoc, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', researchCenter));
 
% Display the Person metadata instances:
disp(pi)
Person (https://openminds.om-i.org/types/Person) with properties: affiliation: University of Neuroscience (Affiliation) alternateName: [1×0 string] @@ -134,7 +141,7 @@ familyName: "Doe" givenName: "Jane" - Required Properties: givenName
disp(postdoc)
Person (https://openminds.om-i.org/types/Person) with properties: + Required Properties: givenName
disp(postdoc)
Person (https://openminds.om-i.org/types/Person) with properties: affiliation: Brain Research Center (Affiliation) alternateName: [1×0 string] @@ -144,14 +151,27 @@ familyName: "Smith" givenName: "John" - Required Properties: givenName

3. Creating Dataset Metadata

Now we'll create a dataset version to describe our neuroscience data. We'll add all the required properties and some optional ones.

3.1 Create a DOI (Digital Object Identifier)

doi = openminds.core.digitalidentifier.DOI(...
'id', createId('dataset-doi'), ...
'identifier', 'https://doi.org/10.1234/example.2023.001');

3.2 Create a license

The License is one of a few types where pre-defined instances already exist. To view available instances, use the listInstances method:
openminds.core.data.License.listInstances()
ans = 31×1 string
"AGPL-3.0-only"
"Apache-2.0"
"BSD-2-Clause"
"BSD-3-Clause"
"BSD-4-Clause"
"CC-BY-4.0"
"CC-BY-NC-4.0"
"CC-BY-NC-ND…
"CC-BY-NC-SA…
"CC-BY-ND-4.0"
% Create a license from a name:
license = openminds.core.data.License.fromName("CC-BY-4.0");
disp(license)
License (https://openminds.om-i.org/types/License) with properties: + Required Properties: givenName

3. Creating Dataset Metadata

Now we'll create a dataset version to describe our neuroscience data. We'll add all the required properties and some optional ones.

3.1 Create a DOI (Digital Object Identifier)

doi = openminds.core.digitalidentifier.DOI(...
'id', createId('dataset-doi'), ...
'identifier', 'https://doi.org/10.1234/example.2023.001');

3.2 Create a license

The License is one of a few types where pre-defined instances already exist. To view available instances, use the listInstances method:
openminds.core.data.License.listInstances()
ans = 31×1 string
"AGPL-3.0-only"
"Apache-2.0"
"BSD-2-Clause"
"BSD-3-Clause"
"BSD-4-Clause"
"CC-BY-4.0"
"CC-BY-NC-4.0"
"CC-BY-NC-ND…
"CC-BY-NC-SA…
"CC-BY-ND-4.0"
% Create a license from a name:
license = openminds.core.data.License.fromName("CC-BY-4.0");
disp(license)
License (https://openminds.om-i.org/types/License) with properties: fullName: "Creative Commons Attribution 4.0 International" legalCode: "https://creativecommons.org/licenses/by/4.0/legalcode" shortName: "CC-BY-4.0" webpage: ["https://creativecommons.org/licenses/by/4.0" "https://spdx.org/licenses/CC-BY-4.0.html"] - Required Properties: fullName, legalCode, shortName
% Alternatively, create it manually
license = openminds.core.data.License(...
'id', createId('cc-by-4'), ...
'fullName', 'Creative Commons Attribution 4.0 International', ...
'shortName', 'CC BY 4.0', ...
'webpage', "https://creativecommons.org/licenses/by/4.0");

3.3 Create a file repository

Note: This is only relevant if data is stored in external repository. If a dataset is submitted via EBRAINS, the file repository is created and added as part of the curation process
repository = openminds.core.data.FileRepository(...
'id', createId('dataset-repository'), ...
'IRI', 'https://example-repository.org/datasets/123', ...
'name', 'Example Dataset Repository', ...
'hostedBy', university);

3.4 Create a behavioral protocol

protocol = openminds.core.research.BehavioralProtocol(...
'id', createId('visual-task-protocol'), ...
'name', 'Visual Go/NoGo Task', ...
'description', ['Mice were trained to discriminate visual stimuli. ', ...
'Each stimulus was associated with a specific outcome (reward, nothing, or punishment).']);

3.5 Create controlled terms

Controlled terms are metatata types which has a corresponding terminology developed by the Open Metadata Initiative, available here: https://github.com/openMetadataInitiative/openMINDS_instances
To see a list of available instance, you can again use the listInstances.
Note: ControlledTerm types accept the instance names as an input to the class constructor directly (See below for examples).
openminds.controlledterms.PreparationType.listInstances()
ans = 6×1 string
"exVivo"
"inSilico"
"inSitu"
"inUtero"
"inVitro"
"inVivo"
% These are predefined terms from controlled vocabularies
preparationType = openminds.controlledterms.PreparationType('inVivo');
 
ethicsAssessment = openminds.controlledterms.EthicsAssessment('EUCompliant');
 
accessibility = openminds.controlledterms.ProductAccessibility('freeAccess');
 
dataType = openminds.controlledterms.SemanticDataType('experimentalData');
 
experimentalApproach1 = openminds.controlledterms.ExperimentalApproach('behavior');
experimentalApproach2 = openminds.controlledterms.ExperimentalApproach('electrophysiology');
 
technique = openminds.controlledterms.Technique('extracellularElectrophysiology');

3.6 Create a custom term suggestion (for keywords that don't exist in controlled vocabularies)

customKeyword = openminds.controlledterms.TermSuggestion(...
'id', createId('custom-brain-region'), ...
'name', 'visual cortex');

3.7 Create the dataset version

datasetVersion = openminds.core.products.DatasetVersion(...
'id', createId('example-dataset-v1'), ...
'fullName', 'Neural activity during visual discrimination task', ...
'shortName', 'Visual Task Dataset', ...
'versionIdentifier', 'v1', ...
'accessibility', accessibility, ...
'author', [pi, postdoc], ...
'custodian', pi, ...
'description', ['This dataset contains neural recordings from mice ', ...
'performing a visual discrimination task.'], ...
'digitalIdentifier', doi, ...
'ethicsAssessment', ethicsAssessment, ...
'experimentalApproach', [experimentalApproach1, experimentalApproach2], ...
'license', license, ...
'preparationDesign', preparationType, ...
'repository', repository, ...
'dataType', dataType, ...
'technique', technique, ...
'behavioralProtocol', protocol, ...
'keyword', customKeyword, ...
'versionInnovation', 'This is the first version of this dataset.');
 
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: + Required Properties: fullName, legalCode, shortName
% Alternatively, create it manually
license = openminds.core.data.License(...
'id', createId('cc-by-4'), ...
'fullName', 'Creative Commons Attribution 4.0 International', ...
'shortName', 'CC BY 4.0', ...
'webpage', "https://creativecommons.org/licenses/by/4.0");

3.3 Create a file repository

Note: This is only relevant if data is stored in external repository. If a dataset is submitted via EBRAINS, the file repository is created and added as part of the curation process
repository = openminds.core.data.FileRepository(...
'id', createId('dataset-repository'), ...
'IRI', 'https://example-repository.org/datasets/123', ...
'name', 'Example Dataset Repository', ...
'hostedBy', university);

3.4 Create a behavioral protocol

protocol = openminds.core.research.BehavioralProtocol(...
'id', createId('visual-task-protocol'), ...
'name', 'Visual Go/NoGo Task', ...
'description', ['Mice were trained to discriminate visual stimuli. ', ...
'Each stimulus was associated with a specific outcome (reward, nothing, or punishment).']);

3.5 Create controlled terms

Controlled terms are metatata types which has a corresponding terminology developed by the Open Metadata Initiative, available here: https://github.com/openMetadataInitiative/openMINDS_instances
To see a list of available instance, you can again use the listInstances.
Note: ControlledTerm types accept the instance names as an input to the class constructor directly (See below for examples).
openminds.controlledterms.PreparationType.listInstances()
ans = 6×1 string
"exVivo"
"inSilico"
"inSitu"
"inUtero"
"inVitro"
"inVivo"
% These are predefined terms from controlled vocabularies
preparationType = openminds.controlledterms.PreparationType('inVivo');
 
ethicsAssessment = openminds.controlledterms.EthicsAssessment('EUCompliant');
 
accessibility = openminds.controlledterms.ProductAccessibility('freeAccess');
 
dataType = openminds.controlledterms.SemanticDataType('experimentalData');
 
experimentalApproach1 = openminds.controlledterms.ExperimentalApproach('behavior');
experimentalApproach2 = openminds.controlledterms.ExperimentalApproach('electrophysiology');
 
technique = openminds.controlledterms.Technique('extracellularElectrophysiology');

3.6 Create a custom term suggestion (for keywords that don't exist in controlled vocabularies)

customKeyword = openminds.controlledterms.TermSuggestion(...
'id', createId('custom-brain-region'), ...
'name', 'visual cortex');

3.7 Create the dataset version

datasetVersion = openminds.core.products.DatasetVersion(...
'id', createId('example-dataset-v1'), ...
'fullName', 'Neural activity during visual discrimination task', ...
'shortName', 'Visual Task Dataset', ...
'versionIdentifier', 'v1', ...
'accessibility', accessibility, ...
'author', [pi, postdoc], ...
'custodian', pi, ...
'description', ['This dataset contains neural recordings from mice ', ...
'performing a visual discrimination task.'], ...
'digitalIdentifier', doi, ...
'ethicsAssessment', ethicsAssessment, ...
'experimentalApproach', [experimentalApproach1, experimentalApproach2], ...
'license', license, ...
'preparationDesign', preparationType, ...
'repository', repository, ...
'dataType', dataType, ...
'technique', technique, ...
'behavioralProtocol', protocol, ...
'keyword', customKeyword, ...
'versionInnovation', 'This is the first version of this dataset.');
 
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: accessibility: free access (ProductAccessibility) author: [Doe, Jane (Person) Smith, John (Person)] (Any of: Consortium, Organization, Person) @@ -189,7 +209,7 @@ Required Properties: accessibility, dataType, digitalIdentifier, ethicsAssessment, experimentalApproach, fullDocumentation, license, releaseDate, shortName, technique, - versionIdentifier, versionInnovation

4. Creating Subject Metadata

Now we'll create subjects and their states, then link them to the dataset.

4.1 Create a species (strain)

strain = openminds.core.research.Strain(...
'id', createId('c57bl6j-strain'), ...
'name', 'C57BL/6J', ...
'species', openminds.controlledterms.Species('musMusculus'));

4.2 Create biological sex controlled term

biologicalSex = openminds.controlledterms.BiologicalSex('male');

4.3 Create subject state attributes

subjectAttribute1 = openminds.controlledterms.SubjectAttribute('alive');
subjectAttribute2 = openminds.controlledterms.SubjectAttribute('awake');
 
ageCategory = openminds.controlledterms.AgeCategory('adult');

4.4 Create a subject

subject1 = openminds.core.research.Subject(...
'id', createId('subject1'), ...
'lookupLabel', 'Subject1', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S1');

4.5 Create another subject

subject2 = openminds.core.research.Subject(...
'id', createId('subject2'), ...
'lookupLabel', 'Subject2', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S2');

4.6 Create and add subject states for each of the subjects

subjectState1 = openminds.core.research.SubjectState(...
'id', createId('subject1-state'), ...
'lookupLabel', 'Subject1-state', ...
'ageCategory', ageCategory, ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject1-state-01');
subject1.studiedState = subjectState1;
 
subjectState2 = openminds.core.research.SubjectState(...
'id', createId('subject2-state'), ...
'lookupLabel', 'Subject2-state', ...
'ageCategory', "adolescent", ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject2-state-01');
subject2.studiedState = subjectState2;
 
% Display the Subject metadata
disp(subject1);
Subject (https://openminds.om-i.org/types/Subject) with properties: + versionIdentifier, versionInnovation

4. Creating Subject Metadata

Now we'll create subjects and their states, then link them to the dataset.

4.1 Create a species (strain)

strain = openminds.core.research.Strain(...
'id', createId('c57bl6j-strain'), ...
'name', 'C57BL/6J', ...
'species', openminds.controlledterms.Species('musMusculus'));

4.2 Create biological sex controlled term

biologicalSex = openminds.controlledterms.BiologicalSex('male');

4.3 Create subject state attributes

subjectAttribute1 = openminds.controlledterms.SubjectAttribute('alive');
subjectAttribute2 = openminds.controlledterms.SubjectAttribute('awake');
 
ageCategory = openminds.controlledterms.AgeCategory('adult');

4.4 Create a subject

subject1 = openminds.core.research.Subject(...
'id', createId('subject1'), ...
'lookupLabel', 'Subject1', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S1');

4.5 Create another subject

subject2 = openminds.core.research.Subject(...
'id', createId('subject2'), ...
'lookupLabel', 'Subject2', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S2');

4.6 Create and add subject states for each of the subjects

subjectState1 = openminds.core.research.SubjectState(...
'id', createId('subject1-state'), ...
'lookupLabel', 'Subject1-state', ...
'ageCategory', ageCategory, ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject1-state-01');
subject1.studiedState = subjectState1;
 
subjectState2 = openminds.core.research.SubjectState(...
'id', createId('subject2-state'), ...
'lookupLabel', 'Subject2-state', ...
'ageCategory', "adolescent", ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject2-state-01');
subject2.studiedState = subjectState2;
 
% Display the Subject metadata
disp(subject1);
Subject (https://openminds.om-i.org/types/Subject) with properties: biologicalSex: male (BiologicalSex) internalIdentifier: "S1" @@ -198,7 +218,7 @@ species: C57BL/6J (One of: Species, Strain) studiedState: Subject1-state (SubjectState) - Required Properties: species, studiedState
disp(subject2);
Subject (https://openminds.om-i.org/types/Subject) with properties: + Required Properties: species, studiedState
disp(subject2);
Subject (https://openminds.om-i.org/types/Subject) with properties: biologicalSex: male (BiologicalSex) internalIdentifier: "S2" @@ -207,7 +227,7 @@ species: C57BL/6J (One of: Species, Strain) studiedState: Subject2-state (SubjectState) - Required Properties: species, studiedState

4.7 Link subjects to the dataset

datasetVersion.studiedSpecimen = [subject1, subject2];
 
% Display the updated dataset version with added specimen:
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: + Required Properties: species, studiedState

4.7 Link subjects to the dataset

datasetVersion.studiedSpecimen = [subject1, subject2];
 
% Display the updated dataset version with added specimen:
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: accessibility: free access (ProductAccessibility) author: [Doe, Jane (Person) Smith, John (Person)] (Any of: Consortium, Organization, Person) @@ -245,12 +265,12 @@ Required Properties: accessibility, dataType, digitalIdentifier, ethicsAssessment, experimentalApproach, fullDocumentation, license, releaseDate, shortName, technique, - versionIdentifier, versionInnovation

5. Adding Instances to Collection and Saving

Now we'll add all instances to the collection and save it to a file.

5.1 Add the dataset to the collection

% Note: The collection will automatically include all linked instances
collection.add(datasetVersion);
 
disp(collection);
Collection with properties: + versionIdentifier, versionInnovation

5. Adding Instances to Collection and Saving

Now we'll add all instances to the collection and save it to a file.

5.1 Add the dataset to the collection

% Note: The collection will automatically include all linked instances
collection.add(datasetVersion);
 
disp(collection);
Collection with properties: Name: "Neuroscience Dataset Example" Description: "A tutorial dataset for learning openMINDS metadata creation" Nodes: dictionary (string ⟼ cell) with 30 entries - LinkResolver: []

5.2 Save the collection to a JSON-LD file

% Define the save path (in the current directory)
savePath = fullfile(pwd, 'example_metadata.jsonld');
collection.save(savePath);
 
disp(['Saved metadata to: ', savePath]);
Saved metadata to: /Users/Eivind/Code/MATLAB/Neuroscience/Repositories/openMetadataInitiative/openMINDS_MATLAB/code/example_metadata.jsonld

5.3 Display the saved JSON-LD content

jsonContent = fileread(savePath);
disp(jsonContent);
{ + LinkResolver: []

5.2 Save the collection to a JSON-LD file

% Define the save path (in the current directory)
savePath = fullfile(pwd, 'example_metadata.jsonld');
collection.save(savePath);
 
disp(['Saved metadata to: ', savePath]);
Saved metadata to: /Users/Eivind/Code/MATLAB/Neuroscience/Repositories/openMetadataInitiative/openMINDS_MATLAB/code/example_metadata.jsonld

5.3 Display the saved JSON-LD content

jsonContent = fileread(savePath);
disp(jsonContent);
{ "@context": { "@vocab": "https://openminds.ebrains.eu/vocab/" }, From 220357b7606b3330e2f994bb3367c9249d433692 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Tue, 28 Apr 2026 17:22:37 +0200 Subject: [PATCH 08/15] Reexport tutorials --- docs/tutorials/basicNeuroscienceDataset.html | 1129 +++++++++------- docs/tutorials/basicNeuroscienceDataset.md | 1234 +++++++++++------- docs/tutorials/crewMemberCollection.html | 20 +- docs/tutorials/gettingStarted.html | 90 +- docs/tutorials/gettingStarted.md | 102 +- 5 files changed, 1448 insertions(+), 1127 deletions(-) diff --git a/docs/tutorials/basicNeuroscienceDataset.html b/docs/tutorials/basicNeuroscienceDataset.html index ff845d162..8e68870e8 100644 --- a/docs/tutorials/basicNeuroscienceDataset.html +++ b/docs/tutorials/basicNeuroscienceDataset.html @@ -46,7 +46,9 @@ .S11 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 0px none rgb(33, 33, 33); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px 0px 4px 4px; padding: 0px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S12 { margin: 15px 10px 5px 4px; padding: 0px; line-height: 18px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 17px; font-weight: 700; text-align: left; } .S13 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } -.S14 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px 4px 0px 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S14 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } +.S15 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S16 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px 4px 0px 0px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .embeddedOutputsMatrixElement,.eoOutputWrapper .matrixElement { min-height: 18px; box-sizing: border-box;} .embeddedOutputsMatrixElement .matrixElement,.eoOutputWrapper .matrixElement,.rtcDataTipElement .matrixElement { position: relative;} .matrixElement .variableValue,.rtcDataTipElement .matrixElement .variableValue { white-space: pre; display: inline-block; vertical-align: top; overflow: hidden;} @@ -71,467 +73,432 @@ .variableNameElement { margin-bottom: 3px; display: inline-block;} /* * Ellipses as base64 for HTML export. */.matrixElement .horizontalEllipsis,.rtcDataTipElement .matrixElement .horizontalEllipsis { display: inline-block; margin-top: 3px; /* base64 encoded version of images-liveeditor/HEllipsis.png */ width: 30px; height: 12px; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAJCAYAAADO1CeCAAAAJUlEQVR42mP4//8/A70xw0i29BUDFPxnAEtTW37wWDqakIa4pQDvOOG89lHX2gAAAABJRU5ErkJggg==");} .matrixElement .verticalEllipsis,.textElement .verticalEllipsis,.rtcDataTipElement .matrixElement .verticalEllipsis,.rtcDataTipElement .textElement .verticalEllipsis { margin-left: 35px; /* base64 encoded version of images-liveeditor/VEllipsis.png */ width: 12px; height: 30px; background-repeat: no-repeat; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAZCAYAAAAIcL+IAAAALklEQVR42mP4//8/AzGYgWyFMECMwv8QddRS+P//KyimlmcGUOFoOI6GI/UVAgDnd8Dd4+NCwgAAAABJRU5ErkJggg==");} -.S15 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 0px none rgb(33, 33, 33); border-radius: 0px; padding: 6px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } -.S16 { margin: 10px 10px 9px 4px; padding: 0px; line-height: 21px; min-height: 0px; white-space: pre-wrap; color: rgb(33, 33, 33); font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-style: normal; font-size: 14px; font-weight: 400; text-align: left; } -.S17 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 1px solid rgb(217, 217, 217); border-radius: 4px; padding: 6px 45px 4px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } +.S17 { border-left: 1px solid rgb(217, 217, 217); border-right: 1px solid rgb(217, 217, 217); border-top: 1px solid rgb(217, 217, 217); border-bottom: 0px none rgb(33, 33, 33); border-radius: 0px; padding: 6px 45px 0px 13px; line-height: 18.004px; min-height: 0px; white-space: nowrap; color: rgb(33, 33, 33); font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; } .S18 { margin: 10px 0px 20px; padding-left: 0px; font-family: Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-size: 14px; } -.S19 { margin-left: 56px; line-height: 21px; min-height: 0px; text-align: left; white-space: pre-wrap; }

Creating openMINDS Metadata: A Basic Introduction

This tutorial demonstrates how to create, link, and save metadata using the openMINDS MATLAB toolbox. We'll create a simplified neuroscience dataset with subjects, researchers, and experimental details.
The openMINDS (Open Metadata Initiative for Neuroscience Data Structures) provides standardized metadata models for neuroscience data. This standardization facilitates data sharing, discovery, and reuse.
Please refer to the openMINDS documentation to learn more about the available metadata types.
This tutorial covers:

1. Creating a Metadata Collection

We start by creating an empty metadata collection that will hold all our instances. A collection is a container for metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Neuroscience Dataset Example", ...
"Description", "A tutorial dataset for learning openMINDS metadata creation");
 
disp(collection)
Collection with properties: - - Name: "Neuroscience Dataset Example" - Description: "A tutorial dataset for learning openMINDS metadata creation" - Nodes: dictionary with unset key and value types - LinkResolver: []

2. Creating Basic Metadata Instances

Let's create instances for researchers and their organizations. We'll demonstrate two ways to create instances: 1. Using name-value pairs in the constructor 2. Creating an empty instance and setting properties via dot notation
% Define a utility function for creating instance IDs
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));

2.1 Create organization instances

% First approach: Using name-value pairs in the constructor
university = openminds.core.actors.Organization(...
'id', createId('University of Neuroscience'), ...
'fullName', 'University of Neuroscience', ...
'shortName', 'UNS');
 
% Second approach: Create empty instance and set properties
researchCenter = openminds.core.actors.Organization();
researchCenter.id = createId('Brain Research Center');
researchCenter.fullName = 'Brain Research Center';
researchCenter.shortName = 'BRC';
 
% Display the Organization metadata instances:
disp(university);
Organization (https://openminds.om-i.org/types/Organization) with properties: - - affiliation: [None] (Affiliation) - digitalIdentifier: [None] (Any of: GRIDID, RORID, RRID) - fullName: "University of Neuroscience" - hasParent: [None] (Organization) - homepage: "" - shortName: "UNS" - - Required Properties: fullName
disp(researchCenter);
Organization (https://openminds.om-i.org/types/Organization) with properties: - - affiliation: [None] (Affiliation) - digitalIdentifier: [None] (Any of: GRIDID, RORID, RRID) - fullName: "Brain Research Center" - hasParent: [None] (Organization) - homepage: "" - shortName: "BRC" - - Required Properties: fullName

2.2 Create contact information instances

contactPI = openminds.core.actors.ContactInformation(...
'id', createId('contact-pi'), ...
'email', 'pi@neuroscience.edu');
 
contactPostdoc = openminds.core.actors.ContactInformation(...
'id', createId('contact-postdoc'), ...
'email', 'postdoc@neuroscience.edu');
 
disp(contactPI)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: +.S19 { margin-left: 56px; line-height: 21px; min-height: 0px; text-align: left; white-space: pre-wrap; }

Creating openMINDS Metadata: A Basic Introduction

This tutorial demonstrates how to create, link, and save metadata using the openMINDS MATLAB toolbox. We will create a simplified neuroscience dataset with subjects, contributors, organizations, and experimental details.
The openMINDS (Open Metadata Initiative for Neuroscience Data Structures) provides standardized metadata models for neuroscience data. This standardization facilitates data sharing, discovery, and reuse.
Please refer to the openMINDS documentation to learn more about the available metadata types.
This tutorial covers:

1. Creating a Metadata Collection

We start by creating an empty metadata collection that will hold all our instances. A collection is a container for metadata instances.
% Create an empty metadata collection
collection = openminds.Collection(...
"Name", "Neuroscience Dataset Example", ...
"Description", "A tutorial dataset for learning openMINDS metadata creation");
 
disp(collection)
Collection with properties: + + Name: "Neuroscience Dataset Example" + Description: "A tutorial dataset for learning openMINDS metadata creation" + Nodes: dictionary with unset key and value types + LinkResolver: [] + MetadataStore: [0×0 openminds.internal.FileMetadataStore]

2. Creating Basic Metadata Instances

Let us create instances for researchers, their contact information, and their organizations. The examples show two common construction patterns: using name-value pairs in the constructor, and creating an empty instance before assigning properties with dot notation.
% Define a utility function for creating instance IDs
createId = @(str) lower(sprintf('_:%s', replace(str, ' ', '-')));

2.1 Create organization instances

% First approach: Using name-value pairs in the constructor
university = openminds.core.actors.Organization(...
"id", createId("University of Neuroscience"), ...
"name", "University of Neuroscience", ...
"acronym", "UNS", ...
"countryOfFormation", openminds.controlledterms.SovereignState("Germany"), ...
"type", openminds.controlledterms.OrganizationType("legalEntity"));
 
% Second approach: Create empty instance and set properties
researchCenter = openminds.core.actors.Organization();
researchCenter.id = createId("Brain Research Center");
researchCenter.name = "Brain Research Center";
researchCenter.acronym = "BRC";
researchCenter.countryOfFormation = openminds.controlledterms.SovereignState("Germany");
researchCenter.type = openminds.controlledterms.OrganizationType("organizationalUnit");
researchCenter.hasParent = university;
 
% Display selected Organization fields:
fprintf("%s (%s)\n", university.name, university.acronym)
University of Neuroscience (UNS)
fprintf("%s (%s)\n", researchCenter.name, researchCenter.acronym)
Brain Research Center (BRC)

2.2 Create contact information instances

contactPI = openminds.core.actors.ContactInformation(...
'id', createId('contact-pi'), ...
'email', 'pi@neuroscience.edu');
contactPostdoc = openminds.core.actors.ContactInformation(...
'id', createId('contact-postdoc'), ...
'email', 'postdoc@neuroscience.edu');
disp(contactPI)
ContactInformation (_:contact-pi) with properties: email: "pi@neuroscience.edu" - Required Properties: email
disp(contactPostdoc)
ContactInformation (https://openminds.om-i.org/types/ContactInformation) with properties: + Required Properties: email
disp(contactPostdoc)
ContactInformation (_:contact-postdoc) with properties: email: "postdoc@neuroscience.edu" - Required Properties: email

2.3 Create person instances with affiliations

% Principal Investigator
pi = openminds.core.actors.Person(...
'id', createId('jane-doe'), ...
'givenName', 'Jane', ...
'familyName', 'Doe', ...
'contactInformation', contactPI, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', university));
 
% Postdoc
postdoc = openminds.core.actors.Person(...
'id', createId('john-smith'), ...
'givenName', 'John', ...
'familyName', 'Smith', ...
'contactInformation', contactPostdoc, ...
'affiliation', openminds.core.actors.Affiliation('memberOf', researchCenter));
 
% Display the Person metadata instances:
disp(pi)
Person (https://openminds.om-i.org/types/Person) with properties: + Required Properties: email

2.3 Create person and affiliation instances

A Person describes an individual. The affiliation is represented as a separate Affiliation instance linking that person to an organization.
% Principal Investigator
pi = openminds.core.actors.Person(...
"id", createId("jane-doe"), ...
"givenName", "Jane", ...
"familyName", "Doe", ...
"preferredName", "Jane Doe", ...
"contactInformation", contactPI);
 
% Postdoc
postdoc = openminds.core.actors.Person(...
"id", createId("john-smith"), ...
"givenName", "John", ...
"familyName", "Smith", ...
"preferredName", "John Smith", ...
"contactInformation", contactPostdoc);
 
% Affiliations are represented separately from the Person instances
piAffiliation = openminds.core.actors.Affiliation(...
"person", pi, ...
"organization", university);
 
postdocAffiliation = openminds.core.actors.Affiliation(...
"person", postdoc, ...
"organization", researchCenter);
 
% Display the Person metadata instances:
disp(pi)
Person (_:jane-doe) with properties: - affiliation: University of Neuroscience (Affiliation) alternateName: [1×0 string] - associatedAccount: [None] (AccountInformation) - contactInformation: pi@neuroscience.edu (ContactInformation) - digitalIdentifier: [None] (ORCID) + associatedAccount: [None] (AccountInformation) + contactInformation: pi@neuroscience.edu (ContactInformation) + digitalIdentifier: [None] (Any of: GenericIdentifier, ORCID) familyName: "Doe" givenName: "Jane" + preferredName: "Jane Doe" - Required Properties: givenName
disp(postdoc)
Person (https://openminds.om-i.org/types/Person) with properties: + Required Properties: preferredName
disp(postdoc)
Person (_:john-smith) with properties: - affiliation: Brain Research Center (Affiliation) alternateName: [1×0 string] - associatedAccount: [None] (AccountInformation) - contactInformation: postdoc@neuroscience.edu (ContactInformation) - digitalIdentifier: [None] (ORCID) + associatedAccount: [None] (AccountInformation) + contactInformation: postdoc@neuroscience.edu (ContactInformation) + digitalIdentifier: [None] (Any of: GenericIdentifier, ORCID) familyName: "Smith" givenName: "John" + preferredName: "John Smith" - Required Properties: givenName

3. Creating Dataset Metadata

Now we'll create a dataset version to describe our neuroscience data. We'll add all the required properties and some optional ones.

3.1 Create a DOI (Digital Object Identifier)

doi = openminds.core.digitalidentifier.DOI(...
'id', createId('dataset-doi'), ...
'identifier', 'https://doi.org/10.1234/example.2023.001');

3.2 Create a license

The License is one of a few types where pre-defined instances already exist. To view available instances, use the listInstances method:
openminds.core.data.License.listInstances()
ans = 31×1 string
"AGPL-3.0-only"
"Apache-2.0"
"BSD-2-Clause"
"BSD-3-Clause"
"BSD-4-Clause"
"CC-BY-4.0"
"CC-BY-NC-4.0"
"CC-BY-NC-ND…
"CC-BY-NC-SA…
"CC-BY-ND-4.0"
% Create a license from a name:
license = openminds.core.data.License.fromName("CC-BY-4.0");
disp(license)
License (https://openminds.om-i.org/types/License) with properties: + Required Properties: preferredName

3. Creating Dataset Metadata

Next we create metadata for the dataset and for one concrete dataset version. Contributor roles are represented with Contribution instances. Contributor affiliations are represented separately with Affiliation instances that link people to organizations.

3.1 Create a DOI (Digital Object Identifier)

doi = openminds.core.digitalidentifier.DOI(...
'id', createId('dataset-doi'), ...
'identifier', 'https://doi.org/10.1234/example.2023.001');

3.2 Create a license

A License describes the conditions under which the dataset version can be reused. For this example, we create a small license instance directly.
% Create a license manually for this example:
license = openminds.core.data.License(...
"id", createId("cc-by-4"), ...
"fullName", "Creative Commons Attribution 4.0 International", ...
"shortName", "CC-BY-4.0", ...
"legalCode", "https://creativecommons.org/licenses/by/4.0/legalcode", ...
"webpage", "https://creativecommons.org/licenses/by/4.0");
 
disp(license)
License (_:cc-by-4) with properties: fullName: "Creative Commons Attribution 4.0 International" legalCode: "https://creativecommons.org/licenses/by/4.0/legalcode" shortName: "CC-BY-4.0" - webpage: ["https://creativecommons.org/licenses/by/4.0" "https://spdx.org/licenses/CC-BY-4.0.html"] + webpage: "https://creativecommons.org/licenses/by/4.0" - Required Properties: fullName, legalCode, shortName
% Alternatively, create it manually
license = openminds.core.data.License(...
'id', createId('cc-by-4'), ...
'fullName', 'Creative Commons Attribution 4.0 International', ...
'shortName', 'CC BY 4.0', ...
'webpage', "https://creativecommons.org/licenses/by/4.0");

3.3 Create a file repository

Note: This is only relevant if data is stored in external repository. If a dataset is submitted via EBRAINS, the file repository is created and added as part of the curation process
repository = openminds.core.data.FileRepository(...
'id', createId('dataset-repository'), ...
'IRI', 'https://example-repository.org/datasets/123', ...
'name', 'Example Dataset Repository', ...
'hostedBy', university);

3.4 Create a behavioral protocol

protocol = openminds.core.research.BehavioralProtocol(...
'id', createId('visual-task-protocol'), ...
'name', 'Visual Go/NoGo Task', ...
'description', ['Mice were trained to discriminate visual stimuli. ', ...
'Each stimulus was associated with a specific outcome (reward, nothing, or punishment).']);

3.5 Create controlled terms

Controlled terms are metatata types which has a corresponding terminology developed by the Open Metadata Initiative, available here: https://github.com/openMetadataInitiative/openMINDS_instances
To see a list of available instance, you can again use the listInstances.
Note: ControlledTerm types accept the instance names as an input to the class constructor directly (See below for examples).
openminds.controlledterms.PreparationType.listInstances()
ans = 6×1 string
"exVivo"
"inSilico"
"inSitu"
"inUtero"
"inVitro"
"inVivo"
% The variable "license" now holds the reusable License instance.

3.3 Create a file repository

Note: This is only relevant if data is stored in external repository. If a dataset is submitted via EBRAINS, the file repository is created and added as part of the curation process
repository = openminds.core.data.FileRepository(...
'id', createId('dataset-repository'), ...
'IRI', 'https://example-repository.org/datasets/123', ...
'name', 'Example Dataset Repository', ...
'hostedBy', university);

3.4 Create a behavioral protocol

protocol = openminds.core.research.BehavioralProtocol(...
'id', createId('visual-task-protocol'), ...
'name', 'Visual Go/NoGo Task', ...
'description', ['Mice were trained to discriminate visual stimuli. ', ...
'Each stimulus was associated with a specific outcome (reward, nothing, or punishment).']);

3.5 Create controlled terms

Controlled terms are metadata types with a corresponding terminology developed by the Open Metadata Initiative, available here: https://github.com/openMetadataInitiative/openMINDS_instances
To see a list of available instances, use the listInstances.
Note: ControlledTerm types accept the instance names as an input to the class constructor directly (See below for examples).
openminds.controlledterms.PreparationType.listInstances()
ans = 6×1 string
"exVivo"
"inSilico"
"inSitu"
"inUtero"
"inVitro"
"inVivo"
% These are predefined terms from controlled vocabularies
preparationType = openminds.controlledterms.PreparationType('inVivo');
 
ethicsAssessment = openminds.controlledterms.EthicsAssessment('EUCompliant');
 
accessibility = openminds.controlledterms.ProductAccessibility('freeAccess');
 
dataType = openminds.controlledterms.SemanticDataType('experimentalData');
 
experimentalApproach1 = openminds.controlledterms.ExperimentalApproach('behavior');
experimentalApproach2 = openminds.controlledterms.ExperimentalApproach('electrophysiology');
 
technique = openminds.controlledterms.Technique('extracellularElectrophysiology');

3.6 Create a custom term suggestion (for keywords that don't exist in controlled vocabularies)

customKeyword = openminds.controlledterms.TermSuggestion(...
'id', createId('custom-brain-region'), ...
'name', 'visual cortex');

3.7 Create the dataset version

datasetVersion = openminds.core.products.DatasetVersion(...
'id', createId('example-dataset-v1'), ...
'fullName', 'Neural activity during visual discrimination task', ...
'shortName', 'Visual Task Dataset', ...
'versionIdentifier', 'v1', ...
'accessibility', accessibility, ...
'author', [pi, postdoc], ...
'custodian', pi, ...
'description', ['This dataset contains neural recordings from mice ', ...
'performing a visual discrimination task.'], ...
'digitalIdentifier', doi, ...
'ethicsAssessment', ethicsAssessment, ...
'experimentalApproach', [experimentalApproach1, experimentalApproach2], ...
'license', license, ...
'preparationDesign', preparationType, ...
'repository', repository, ...
'dataType', dataType, ...
'technique', technique, ...
'behavioralProtocol', protocol, ...
'keyword', customKeyword, ...
'versionInnovation', 'This is the first version of this dataset.');
 
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: - - accessibility: free access (ProductAccessibility) - author: [Doe, Jane (Person) Smith, John (Person)] (Any of: Consortium, Organization, Person) - behavioralProtocol: Visual Go/NoGo Task (BehavioralProtocol) - copyright: [None] (Copyright) - custodian: Doe, Jane (Any of: Consortium, Organization, Person) - dataType: experimental data (SemanticDataType) - description: "This dataset contains neural recordings from mice performing a visual discrimination task." - digitalIdentifier: https://doi.org/10.1234/example.2023.001 (One of: DOI, IdentifiersDotOrgID) - ethicsAssessment: EU compliant (EthicsAssessment) - experimentalApproach: [behavior electrophysiology] (ExperimentalApproach) - fullDocumentation: [None] (One of: File, DOI, ISBN, WebResource) - fullName: "Neural activity during visual discrimination task" - funding: [None] (Funding) - homepage: "" - howToCite: "" - inputData: [None] (Any of: File, FileBundle, DOI, WebResource, BrainAtlas, BrainAtlasVersion, CommonCoordinateSpace, CommonCoordinateSpaceVersion) - isAlternativeVersionOf: [None] (DatasetVersion) - isNewVersionOf: [None] (DatasetVersion) - keyword: [1×1 Keyword] - license: Creative Commons Attribution 4.0 International (One of: License, WebResource) - otherContribution: [None] (Contribution) - preparationDesign: in vivo (PreparationType) - protocol: [None] (Protocol) - relatedPublication: [None] (Any of: DOI, HANDLE, ISBN, ISSN, Book, Chapter, ScholarlyArticle) - releaseDate: [1×0 datetime] - repository: Example Dataset Repository (FileRepository) - shortName: "Visual Task Dataset" - studiedSpecimen: [None] (Any of: Subject, SubjectGroup, TissueSample, TissueSampleCollection) - studyTarget: [1×0 StudyTarget] - supportChannel: [1×0 string] - technique: extracellular electrophysiology (Any of: AnalysisTechnique, MRIPulseSequence, MRIWeighting, StimulationApproach, StimulationTechnique, Technique) - versionIdentifier: "v1" - versionInnovation: "This is the first version of this dataset." - - Required Properties: accessibility, dataType, digitalIdentifier, ethicsAssessment, - experimentalApproach, fullDocumentation, license, releaseDate, shortName, technique, - versionIdentifier, versionInnovation

4. Creating Subject Metadata

Now we'll create subjects and their states, then link them to the dataset.

4.1 Create a species (strain)

strain = openminds.core.research.Strain(...
'id', createId('c57bl6j-strain'), ...
'name', 'C57BL/6J', ...
'species', openminds.controlledterms.Species('musMusculus'));

4.2 Create biological sex controlled term

biologicalSex = openminds.controlledterms.BiologicalSex('male');

4.3 Create subject state attributes

subjectAttribute1 = openminds.controlledterms.SubjectAttribute('alive');
subjectAttribute2 = openminds.controlledterms.SubjectAttribute('awake');
 
ageCategory = openminds.controlledterms.AgeCategory('adult');

4.4 Create a subject

subject1 = openminds.core.research.Subject(...
'id', createId('subject1'), ...
'lookupLabel', 'Subject1', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S1');

4.5 Create another subject

subject2 = openminds.core.research.Subject(...
'id', createId('subject2'), ...
'lookupLabel', 'Subject2', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S2');

4.6 Create and add subject states for each of the subjects

subjectState1 = openminds.core.research.SubjectState(...
'id', createId('subject1-state'), ...
'lookupLabel', 'Subject1-state', ...
'ageCategory', ageCategory, ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject1-state-01');
subject1.studiedState = subjectState1;
 
subjectState2 = openminds.core.research.SubjectState(...
'id', createId('subject2-state'), ...
'lookupLabel', 'Subject2-state', ...
'ageCategory', "adolescent", ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject2-state-01');
subject2.studiedState = subjectState2;
 
% Display the Subject metadata
disp(subject1);
Subject (https://openminds.om-i.org/types/Subject) with properties: - - biologicalSex: male (BiologicalSex) +" style="white-space: normal; font-style: normal; color: rgb(33, 33, 33); font-size: 12px;">
% These are predefined terms from controlled vocabularies
preparationType = openminds.controlledterms.PreparationType("inVivo");
 
dataType = openminds.controlledterms.SemanticDataType("experimentalData");
 
experimentalApproach1 = openminds.controlledterms.ExperimentalApproach("behavior");
experimentalApproach2 = openminds.controlledterms.ExperimentalApproach("electrophysiology");
 
technique = openminds.controlledterms.Technique("extracellularElectrophysiology");
 
ethicsJurisdiction = openminds.controlledterms.SovereignState("Germany");
 
accessibility = openminds.core.miscellaneous.Accessibility(...
"channel", openminds.controlledterms.AccessChannel("virtualAccess"), ...
"eligibility", openminds.controlledterms.AccessEligibilityType("openAccess"), ...
"form", openminds.controlledterms.AccessForm("directAccess"), ...
"paymentModel", openminds.controlledterms.PaymentModelType("zero-costPaymentModel"), ...
"process", openminds.controlledterms.AccessProcessType("immediateAccess"));
 
contributionTypeAuthor = openminds.controlledterms.ContributionType("authoring");
contributionTypeCustodian = openminds.controlledterms.ContributionType("custodianship");

3.6 Create a custom term suggestion (for keywords that don't exist in controlled vocabularies)

customKeyword = openminds.controlledterms.TermSuggestion(...
'id', createId('custom-brain-region'), ...
'name', 'visual cortex');

3.7 Create dataset and dataset version

The Dataset instance stores version-independent metadata. The DatasetVersion instance stores metadata for this specific release, including accessibility, release date, protocols, studied specimens, and version-specific documentation.
documentation = openminds.core.miscellaneous.WebResource(...
"id", createId("dataset-documentation"), ...
"IRI", "https://example-repository.org/datasets/123/documentation");
 
authorContribution = openminds.core.actors.Contribution(...
"contributor", [pi, postdoc], ...
"type", contributionTypeAuthor);
 
custodianContribution = openminds.core.actors.Contribution(...
"contributor", pi, ...
"type", contributionTypeCustodian);
 
contributions = [authorContribution, custodianContribution];
contributorAffiliations = [piAffiliation, postdocAffiliation];
 
dataset = openminds.core.products.Dataset(...
"id", createId("example-dataset"), ...
"fullName", "Neural activity during visual discrimination task", ...
"shortName", "Visual Task Dataset", ...
"description", "This dataset contains neural recordings from mice " + ...
"performing a visual discrimination task.", ...
"contribution", contributions, ...
"contributorAffiliation", contributorAffiliations);
 
datasetVersion = openminds.core.products.DatasetVersion(...
"id", createId("example-dataset-v1"), ...
"fullName", "Neural activity during visual discrimination task", ...
"shortName", "Visual Task Dataset", ...
"versionIdentifier", "v1", ...
"accessibility", accessibility, ...
"contribution", contributions, ...
"contributorAffiliation", contributorAffiliations, ...
"description", "This dataset contains neural recordings from mice " + ...
"performing a visual discrimination task.", ...
"digitalIdentifier", doi, ...
"documentation", documentation, ...
"ethicsJurisdiction", ethicsJurisdiction, ...
"experimentalApproach", [experimentalApproach1, experimentalApproach2], ...
"usageCondition", license, ...
"preparationType", preparationType, ...
"repository", repository, ...
"dataType", dataType, ...
"technique", technique, ...
"protocol", protocol, ...
"keyword", customKeyword, ...
"isVersionOf", dataset, ...
"releaseDate", datetime(2023, 1, 1), ...
"versionSpecification", "This is the first version of this dataset.");
 
fprintf("Created dataset version: %s (%s)\n", ...
datasetVersion.fullName, datasetVersion.versionIdentifier)
Created dataset version: Neural activity during visual discrimination task (v1)

4. Creating Subject Metadata

Next we create subjects and their states, then link the studied specimens to the dataset version.

4.1 Create a species (strain)

strain = openminds.core.research.Strain(...
'id', createId('c57bl6j-strain'), ...
'name', 'C57BL/6J', ...
'species', openminds.controlledterms.Species('musMusculus'));

4.2 Create biological sex controlled term

biologicalSex = openminds.controlledterms.BiologicalSex('male');

4.3 Create subject state attributes

subjectAttribute1 = openminds.controlledterms.SubjectAttribute('alive');
subjectAttribute2 = openminds.controlledterms.SubjectAttribute('awake');
 
ageCategory = openminds.controlledterms.AgeCategory('adult');

4.4 Create a subject

subject1 = openminds.core.research.Subject(...
'id', createId('subject1'), ...
'lookupLabel', 'Subject1', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S1');

4.5 Create another subject

subject2 = openminds.core.research.Subject(...
'id', createId('subject2'), ...
'lookupLabel', 'Subject2', ...
'biologicalSex', biologicalSex, ...
'species', strain, ...
'internalIdentifier', 'S2');

4.6 Create and add subject states for each of the subjects

subjectState1 = openminds.core.research.SubjectState(...
'id', createId('subject1-state'), ...
'lookupLabel', 'Subject1-state', ...
'ageCategory', ageCategory, ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject1-state-01');
subject1.studiedState = subjectState1;
 
subjectState2 = openminds.core.research.SubjectState(...
'id', createId('subject2-state'), ...
'lookupLabel', 'Subject2-state', ...
'ageCategory', "adolescent", ...
'attribute', [subjectAttribute1, subjectAttribute2], ...
'internalIdentifier', 'Subject2-state-01');
subject2.studiedState = subjectState2;
 
% Display the Subject metadata
disp(subject1);
Subject (_:subject1) with properties: + + biologicalSex: male (BiologicalSex) internalIdentifier: "S1" - isPartOf: [None] (SubjectGroup) + isPartOf: [None] (SubjectGroup) lookupLabel: "Subject1" - species: C57BL/6J (One of: Species, Strain) - studiedState: Subject1-state (SubjectState) + species: C57BL/6J (Strain) + studiedState: Subject1-state (SubjectState) - Required Properties: species, studiedState
disp(subject2);
Subject (https://openminds.om-i.org/types/Subject) with properties: + Required Properties: species, studiedState
disp(subject2);
Subject (_:subject2) with properties: - biologicalSex: male (BiologicalSex) + biologicalSex: male (BiologicalSex) internalIdentifier: "S2" - isPartOf: [None] (SubjectGroup) + isPartOf: [None] (SubjectGroup) lookupLabel: "Subject2" - species: C57BL/6J (One of: Species, Strain) - studiedState: Subject2-state (SubjectState) - - Required Properties: species, studiedState

4.7 Link subjects to the dataset

datasetVersion.studiedSpecimen = [subject1, subject2];
 
% Display the updated dataset version with added specimen:
disp(datasetVersion);
DatasetVersion (https://openminds.om-i.org/types/DatasetVersion) with properties: - - accessibility: free access (ProductAccessibility) - author: [Doe, Jane (Person) Smith, John (Person)] (Any of: Consortium, Organization, Person) - behavioralProtocol: Visual Go/NoGo Task (BehavioralProtocol) - copyright: [None] (Copyright) - custodian: Doe, Jane (Any of: Consortium, Organization, Person) - dataType: experimental data (SemanticDataType) - description: "This dataset contains neural recordings from mice performing a visual discrimination task." - digitalIdentifier: https://doi.org/10.1234/example.2023.001 (One of: DOI, IdentifiersDotOrgID) - ethicsAssessment: EU compliant (EthicsAssessment) - experimentalApproach: [behavior electrophysiology] (ExperimentalApproach) - fullDocumentation: [None] (One of: File, DOI, ISBN, WebResource) - fullName: "Neural activity during visual discrimination task" - funding: [None] (Funding) - homepage: "" - howToCite: "" - inputData: [None] (Any of: File, FileBundle, DOI, WebResource, BrainAtlas, BrainAtlasVersion, CommonCoordinateSpace, CommonCoordinateSpaceVersion) - isAlternativeVersionOf: [None] (DatasetVersion) - isNewVersionOf: [None] (DatasetVersion) - keyword: [1×1 Keyword] - license: Creative Commons Attribution 4.0 International (One of: License, WebResource) - otherContribution: [None] (Contribution) - preparationDesign: in vivo (PreparationType) - protocol: [None] (Protocol) - relatedPublication: [None] (Any of: DOI, HANDLE, ISBN, ISSN, Book, Chapter, ScholarlyArticle) - releaseDate: [1×0 datetime] - repository: Example Dataset Repository (FileRepository) - shortName: "Visual Task Dataset" - studiedSpecimen: [Subject1 (Subject) Subject2 (Subject)] (Any of: Subject, SubjectGroup, TissueSample, TissueSampleCollection) - studyTarget: [1×0 StudyTarget] - supportChannel: [1×0 string] - technique: extracellular electrophysiology (Any of: AnalysisTechnique, MRIPulseSequence, MRIWeighting, StimulationApproach, StimulationTechnique, Technique) - versionIdentifier: "v1" - versionInnovation: "This is the first version of this dataset." - - Required Properties: accessibility, dataType, digitalIdentifier, ethicsAssessment, - experimentalApproach, fullDocumentation, license, releaseDate, shortName, technique, - versionIdentifier, versionInnovation

5. Adding Instances to Collection and Saving

Now we'll add all instances to the collection and save it to a file.

5.1 Add the dataset to the collection

% Note: The collection will automatically include all linked instances
collection.add(datasetVersion);
 
disp(collection);
Collection with properties: - - Name: "Neuroscience Dataset Example" - Description: "A tutorial dataset for learning openMINDS metadata creation" - Nodes: dictionary (string ⟼ cell) with 30 entries - LinkResolver: []

5.2 Save the collection to a JSON-LD file

% Define the save path (in the current directory)
savePath = fullfile(pwd, 'example_metadata.jsonld');
collection.save(savePath);
 
disp(['Saved metadata to: ', savePath]);
Saved metadata to: /Users/Eivind/Code/MATLAB/Neuroscience/Repositories/openMetadataInitiative/openMINDS_MATLAB/code/example_metadata.jsonld

5.3 Display the saved JSON-LD content

jsonContent = fileread(savePath);
disp(jsonContent);
{ + species: C57BL/6J (Strain) + studiedState: Subject2-state (SubjectState) + + Required Properties: species, studiedState

4.7 Link subjects to the dataset

datasetVersion.studiedSpecimen = [subject1, subject2];
 
% Display a short confirmation for the updated dataset version:
fprintf("Dataset version now links to %d studied specimens.\n", ...
numel(datasetVersion.studiedSpecimen))
Dataset version now links to 2 studied specimens.

5. Adding Instances to Collection and Saving

Finally, we add the dataset version to the collection and save the detected metadata graph to a file.

5.1 Add the dataset version to the collection

% Note: The collection will automatically include all linked instances
collection.add(datasetVersion);
 
disp(collection);
Collection with properties: + + Name: "Neuroscience Dataset Example" + Description: "A tutorial dataset for learning openMINDS metadata creation" + Nodes: dictionary (string ⟼ cell) with 41 entries + LinkResolver: [] + MetadataStore: [0×0 openminds.internal.FileMetadataStore]

5.2 Save the collection to a JSON-LD file

% Define the save path (in the current directory)
savePath = fullfile(pwd, 'example_metadata.jsonld');
collection.save(savePath);
 
disp(['Saved metadata to: ', savePath]);
Saved metadata to: /Users/eivind/Code/MATLAB/Neuroscience/Repositories/openMetadataInitiative/openMINDS_MATLAB/example_metadata.jsonld

5.3 Display the saved JSON-LD content

jsonContent = fileread(savePath);
disp(jsonContent);
{ "@context": { - "@vocab": "https://openminds.ebrains.eu/vocab/" + "@vocab": "https://openminds.om-i.org/props/" }, "@graph": [ { - "@id": "_:example-dataset-v1", - "@type": "https://openminds.om-i.org/types/DatasetVersion", - "accessibility": { - "@id": "https://openminds.om-i.org/instances/productAccessibility/freeAccess" - }, - "author": [ - { - "@id": "_:jane-doe" - }, - { - "@id": "_:john-smith" - } - ], - "behavioralProtocol": [ - { - "@id": "_:visual-task-protocol" - } - ], - "custodian": [ + "@id": "https://openminds.om-i.org/instances/accessChannel/virtualAccess", + "@type": "https://openminds.om-i.org/types/AccessChannel", + "definition": "Refers to the ability of users to connect to, interact with, and utilize resources, systems, or other individuals remotely via digital interfaces.", + "name": "virtual access", + "synonym": [ + "digital access", + "online access" + ] + }, + { + "@id": "https://openminds.om-i.org/instances/accessEligibilityType/openAccess", + "@type": "https://openminds.om-i.org/types/AccessEligibilityType", + "definition": "Access without prior registration, authentication, or authorisation.", + "name": "open access" + }, + { + "@id": "https://openminds.om-i.org/instances/accessForm/directAccess", + "@type": "https://openminds.om-i.org/types/AccessForm", + "definition": "Users interact directly with the product or service through integrated interfaces, or authorised environments.", + "name": "direct access" + }, + { + "@id": "https://openminds.om-i.org/instances/paymentModelType/zero-costPaymentModel", + "@type": "https://openminds.om-i.org/types/PaymentModelType", + "definition": "No payment is required for any billable units (entitlement, consumption, event, monetary value, outcome, or capacity units).", + "name": "zero-cost payment model" + }, + { + "@id": "https://openminds.om-i.org/instances/accessProcessType/immediateAccess", + "@type": "https://openminds.om-i.org/types/AccessProcessType", + "definition": "Automatic access upon acceptance of the applicable terms.", + "name": "immediate access" + }, + { + "@id": "_:2f1d8cef-f64a-4795-9a9a-c3e237236085", + "@type": "https://openminds.om-i.org/types/Accessibility", + "channel": [ { - "@id": "_:jane-doe" + "@id": "https://openminds.om-i.org/instances/accessChannel/virtualAccess" } ], - "dataType": [ + "eligibility": [ { - "@id": "https://openminds.om-i.org/instances/semanticDataType/experimentalData" + "@id": "https://openminds.om-i.org/instances/accessEligibilityType/openAccess" } ], - "description": "This dataset contains neural recordings from mice performing a visual discrimination task.", - "digitalIdentifier": { - "@id": "_:dataset-doi" - }, - "ethicsAssessment": { - "@id": "https://openminds.om-i.org/instances/ethicsAssessment/EUCompliant" - }, - "experimentalApproach": [ - { - "@id": "https://openminds.om-i.org/instances/experimentalApproach/behavior" - }, + "form": [ { - "@id": "https://openminds.om-i.org/instances/experimentalApproach/electrophysiology" + "@id": "https://openminds.om-i.org/instances/accessForm/directAccess" } ], - "fullName": "Neural activity during visual discrimination task", - "keyword": [ + "paymentModel": [ { - "@id": "_:custom-brain-region" + "@id": "https://openminds.om-i.org/instances/paymentModelType/zero-costPaymentModel" } ], - "license": { - "@id": "_:cc-by-4" - }, - "preparationDesign": [ + "process": [ { - "@id": "https://openminds.om-i.org/instances/preparationType/inVivo" + "@id": "https://openminds.om-i.org/instances/accessProcessType/immediateAccess" } - ], - "repository": { - "@id": "_:dataset-repository" - }, - "shortName": "Visual Task Dataset", - "studiedSpecimen": [ - { - "@id": "_:subject1" - }, - { - "@id": "_:subject2" - } - ], - "technique": [ - { - "@id": "https://openminds.om-i.org/instances/technique/extracellularElectrophysiology" - } - ], - "versionIdentifier": "v1", - "versionInnovation": "This is the first version of this dataset." + ] }, { - "@id": "https://openminds.om-i.org/instances/productAccessibility/freeAccess", - "@type": "https://openminds.om-i.org/types/ProductAccessibility", - "definition": "With ''free access'' selected, data and metadata are both released and become immediately available without any access restrictions.", - "name": "free access" + "@id": "https://openminds.om-i.org/instances/semanticDataType/experimentalData", + "@type": "https://openminds.om-i.org/types/SemanticDataType", + "name": "experimental data" + }, + { + "@id": "_:dataset-doi", + "@type": "https://openminds.om-i.org/types/DOI", + "identifier": "https://doi.org/10.1234/example.2023.001" + }, + { + "@id": "_:dataset-documentation", + "@type": "https://openminds.om-i.org/types/WebResource", + "IRI": "https://example-repository.org/datasets/123/documentation" + }, + { + "@id": "https://openminds.om-i.org/instances/SovereignState/Germany", + "@type": "https://openminds.om-i.org/types/SovereignState", + "definition": "Country in Central Europe. [auto-generated from ''schema:description'' property of the [Wikidata entity](http://www.wikidata.org/entity/Q183)]", + "name": "Germany", + "preferredCrossReference": "http://www.wikidata.org/entity/Q183", + "synonym": [ + "BR Deutschland", + "BRD", + "Bundesrepublik Deutschland", + "DE", + "de", + "DEU", + "Deutschland", + "Federal Republic of Germany", + "GER" + ] + }, + { + "@id": "https://openminds.om-i.org/instances/experimentalApproach/behavior", + "@type": "https://openminds.om-i.org/types/ExperimentalApproach", + "definition": "Any experimental approach focused on the mechanical activity or cognitive processes underlying mechanical activity of living organisms often in response to external sensory stimuli.", + "name": "behavior", + "otherOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/readable/modality/Behavior", + "preferredOntologyIdentifier": "http://uri.interlex.org/base/ilx_0739413", + "synonym": "behavioral approach" + }, + { + "@id": "https://openminds.om-i.org/instances/experimentalApproach/electrophysiology", + "@type": "https://openminds.om-i.org/types/ExperimentalApproach", + "definition": "Any experimental approach focused on electrical phenomena associated with living systems, most notably the nervous system, cardiac system, and musculoskeletal system.", + "name": "electrophysiology", + "otherOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/readable/modality/Electrophysiology", + "preferredOntologyIdentifier": "http://uri.interlex.org/base/ilx_0741202" + }, + { + "@id": "_:contact-pi", + "@type": "https://openminds.om-i.org/types/ContactInformation", + "email": "pi@neuroscience.edu" }, { "@id": "_:jane-doe", "@type": "https://openminds.om-i.org/types/Person", - "affiliation": [ + "contactInformation": [ { - "@type": "https://openminds.om-i.org/types/Affiliation", - "memberOf": { - "@id": "_:university-of-neuroscience" - } + "@id": "_:contact-pi" } ], - "contactInformation": { - "@id": "_:contact-pi" - }, "familyName": "Doe", - "givenName": "Jane" + "givenName": "Jane", + "preferredName": "Jane Doe" }, { - "@id": "_:contact-pi", + "@id": "_:contact-postdoc", "@type": "https://openminds.om-i.org/types/ContactInformation", - "email": "pi@neuroscience.edu" - }, - { - "@id": "_:university-of-neuroscience", - "@type": "https://openminds.om-i.org/types/Organization", - "fullName": "University of Neuroscience", - "shortName": "UNS" + "email": "postdoc@neuroscience.edu" }, { "@id": "_:john-smith", "@type": "https://openminds.om-i.org/types/Person", - "affiliation": [ + "contactInformation": [ { - "@type": "https://openminds.om-i.org/types/Affiliation", - "memberOf": { - "@id": "_:brain-research-center" - } + "@id": "_:contact-postdoc" } ], - "contactInformation": { - "@id": "_:contact-postdoc" - }, "familyName": "Smith", - "givenName": "John" - }, - { - "@id": "_:contact-postdoc", - "@type": "https://openminds.om-i.org/types/ContactInformation", - "email": "postdoc@neuroscience.edu" + "givenName": "John", + "preferredName": "John Smith" }, { - "@id": "_:brain-research-center", - "@type": "https://openminds.om-i.org/types/Organization", - "fullName": "Brain Research Center", - "shortName": "BRC" + "@id": "https://openminds.om-i.org/instances/contributionType/authoring", + "@type": "https://openminds.om-i.org/types/ContributionType", + "definition": "A contribution type of a role-bearing entity realized by creating textual, visual, or other expressive intellectual content about or for a target entity.", + "name": "authoring" }, { - "@id": "_:visual-task-protocol", - "@type": "https://openminds.om-i.org/types/BehavioralProtocol", - "description": "Mice were trained to discriminate visual stimuli. Each stimulus was associated with a specific outcome (reward, nothing, or punishment).", - "name": "Visual Go/NoGo Task" + "@id": "https://openminds.om-i.org/instances/contributionType/custodianship", + "@type": "https://openminds.om-i.org/types/ContributionType", + "definition": "A contribution type of a role-bearing entity realized by assuming responsibility for the long-term stewardship and oversight of a target entity.", + "name": "custodianship" }, { - "@id": "https://openminds.om-i.org/instances/semanticDataType/experimentalData", - "@type": "https://openminds.om-i.org/types/SemanticDataType", - "name": "experimental data" + "@id": "https://openminds.om-i.org/instances/organizationType/legalEntity", + "@type": "https://openminds.om-i.org/types/OrganizationType", + "definition": "An organization classified as a type of legal entity recognized within a specific legal system.", + "name": "legal entity", + "preferredCrossReference": "https://www.wikidata.org/entity/Q10541491" }, { - "@id": "_:dataset-doi", - "@type": "https://openminds.om-i.org/types/DOI", - "identifier": "https://doi.org/10.1234/example.2023.001" + "@id": "_:university-of-neuroscience", + "@type": "https://openminds.om-i.org/types/Organization", + "acronym": "UNS", + "countryOfFormation": [ + { + "@id": "https://openminds.om-i.org/instances/SovereignState/Germany" + } + ], + "name": "University of Neuroscience", + "type": [ + { + "@id": "https://openminds.om-i.org/instances/organizationType/legalEntity" + } + ] }, { - "@id": "https://openminds.om-i.org/instances/ethicsAssessment/EUCompliant", - "@type": "https://openminds.om-i.org/types/EthicsAssessment", - "definition": "Data are ethically approved in compliance with EU law. No additional ethics assessment was made by the data sharing initiative.", - "description": "Data are ethically approved in compliance with EU law. No additional ethics assessment was made by the data sharing initiative. This is typically true for all, human post-mortem data, human cross-subject statistics, non-primate vertebrate animals as well as cephalopods.", - "name": "EU compliant" + "@id": "https://openminds.om-i.org/instances/organizationType/organizationalUnit", + "@type": "https://openminds.om-i.org/types/OrganizationType", + "definition": "A distinct unit within a larger organization.", + "name": "organizational unit" }, { - "@id": "https://openminds.om-i.org/instances/experimentalApproach/behavior", - "@type": "https://openminds.om-i.org/types/ExperimentalApproach", - "definition": "Any experimental approach focused on the mechanical activity or cognitive processes underlying mechanical activity of living organisms often in response to external sensory stimuli.", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0739413", - "name": "behavior", - "preferredOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/readable/modality/Behavior", - "synonym": [ - "behavioral approach" + "@id": "_:brain-research-center", + "@type": "https://openminds.om-i.org/types/Organization", + "acronym": "BRC", + "countryOfFormation": [ + { + "@id": "https://openminds.om-i.org/instances/SovereignState/Germany" + } + ], + "hasParent": [ + { + "@id": "_:university-of-neuroscience" + } + ], + "name": "Brain Research Center", + "type": [ + { + "@id": "https://openminds.om-i.org/instances/organizationType/organizationalUnit" + } ] }, { - "@id": "https://openminds.om-i.org/instances/experimentalApproach/electrophysiology", - "@type": "https://openminds.om-i.org/types/ExperimentalApproach", - "definition": "Any experimental approach focused on electrical phenomena associated with living systems, most notably the nervous system, cardiac system, and musculoskeletal system.", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0741202", - "name": "electrophysiology", - "preferredOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/readable/modality/Electrophysiology" + "@id": "_:example-dataset", + "@type": "https://openminds.om-i.org/types/Dataset", + "contribution": [ + { + "contributor": [ + { + "@id": "_:jane-doe" + }, + { + "@id": "_:john-smith" + } + ], + "type": [ + { + "@id": "https://openminds.om-i.org/instances/contributionType/authoring" + } + ], + "@type": "https://openminds.om-i.org/types/Contribution" + }, + { + "contributor": [ + { + "@id": "_:jane-doe" + } + ], + "type": [ + { + "@id": "https://openminds.om-i.org/instances/contributionType/custodianship" + } + ], + "@type": "https://openminds.om-i.org/types/Contribution" + } + ], + "contributorAffiliation": [ + { + "organization": [ + { + "@id": "_:university-of-neuroscience" + } + ], + "person": [ + { + "@id": "_:jane-doe" + } + ], + "@type": "https://openminds.om-i.org/types/Affiliation" + }, + { + "organization": [ + { + "@id": "_:brain-research-center" + } + ], + "person": [ + { + "@id": "_:john-smith" + } + ], + "@type": "https://openminds.om-i.org/types/Affiliation" + } + ], + "description": "This dataset contains neural recordings from mice performing a visual discrimination task.", + "fullName": "Neural activity during visual discrimination task", + "shortName": "Visual Task Dataset" }, { "@id": "_:custom-brain-region", "@type": "https://openminds.om-i.org/types/TermSuggestion", "name": "visual cortex" }, - { - "@id": "_:cc-by-4", - "@type": "https://openminds.om-i.org/types/License", - "fullName": "Creative Commons Attribution 4.0 International", - "shortName": "CC BY 4.0", - "webpage": [ - "https://creativecommons.org/licenses/by/4.0" - ] - }, { "@id": "https://openminds.om-i.org/instances/preparationType/inVivo", "@type": "https://openminds.om-i.org/types/PreparationType", "definition": "Something happening or existing inside a living body.", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0739622", "name": "in vivo", - "preferredOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/indexes/ontologies/methods/89", - "synonym": [ - "in vivo technique" - ] + "otherOntologyIdentifier": "http://uri.interlex.org/tgbugs/uris/indexes/ontologies/methods/89", + "preferredOntologyIdentifier": "http://uri.interlex.org/base/ilx_0739622", + "synonym": "in vivo technique" + }, + { + "@id": "_:visual-task-protocol", + "@type": "https://openminds.om-i.org/types/BehavioralProtocol", + "description": "Mice were trained to discriminate visual stimuli. Each stimulus was associated with a specific outcome (reward, nothing, or punishment).", + "name": "Visual Go/NoGo Task" }, { "@id": "_:dataset-repository", "@type": "https://openminds.om-i.org/types/FileRepository", "IRI": "https://example-repository.org/datasets/123", - "hostedBy": { - "@id": "_:university-of-neuroscience" - }, - "name": "Example Dataset Repository" - }, - { - "@id": "_:subject1", - "@type": "https://openminds.om-i.org/types/Subject", - "biologicalSex": { - "@id": "https://openminds.om-i.org/instances/biologicalSex/male" - }, - "internalIdentifier": "S1", - "lookupLabel": "Subject1", - "species": { - "@id": "_:c57bl6j-strain" - }, - "studiedState": [ + "hostedBy": [ { - "@id": "_:subject1-state" + "@id": "_:university-of-neuroscience" } - ] + ], + "name": "Example Dataset Repository" }, { "@id": "https://openminds.om-i.org/instances/biologicalSex/male", "@type": "https://openminds.om-i.org/types/BiologicalSex", "definition": "Biological sex that produces sperm cells (spermatozoa).", "description": "A male organism typically has the capacity to produce relatively small, usually mobile gametes (reproductive cells), called sperm cells (or spermatozoa). In the process of fertilization, these sperm cells fuse with a larger, usually immobile female gamete, called egg cell (or ovum).", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0106489", "name": "male", + "otherOntologyIdentifier": "http://uri.interlex.org/base/ilx_0106489", "preferredOntologyIdentifier": "http://purl.obolibrary.org/obo/PATO_0000384" }, - { - "@id": "_:c57bl6j-strain", - "@type": "https://openminds.om-i.org/types/Strain", - "name": "C57BL/6J", - "species": { - "@id": "https://openminds.om-i.org/instances/species/musMusculus" - } - }, { "@id": "https://openminds.om-i.org/instances/species/musMusculus", "@type": "https://openminds.om-i.org/types/Species", "definition": "The species *Mus musculus* (house mouse) belongs to the family of *muridae* (murids).", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0107134", - "knowledgeSpaceLink": "https://knowledge-space.org/wiki/NCBITaxon:10090#mouse", "name": "Mus musculus", + "otherOntologyIdentifier": "http://uri.interlex.org/base/ilx_0107134", + "preferredCrossReference": "https://knowledge-space.org/wiki/NCBITaxon:10090#mouse", "preferredOntologyIdentifier": "http://purl.obolibrary.org/obo/NCBITaxon_10090", "synonym": [ "house mouse", @@ -539,28 +506,21 @@ ] }, { - "@id": "_:subject1-state", - "@type": "https://openminds.om-i.org/types/SubjectState", - "ageCategory": { - "@id": "https://openminds.om-i.org/instances/ageCategory/adult" - }, - "attribute": [ - { - "@id": "https://openminds.om-i.org/instances/subjectAttribute/alive" - }, + "@id": "_:c57bl6j-strain", + "@type": "https://openminds.om-i.org/types/Strain", + "name": "C57BL/6J", + "species": [ { - "@id": "https://openminds.om-i.org/instances/subjectAttribute/awake" + "@id": "https://openminds.om-i.org/instances/species/musMusculus" } - ], - "internalIdentifier": "Subject1-state-01", - "lookupLabel": "Subject1-state" + ] }, { "@id": "https://openminds.om-i.org/instances/ageCategory/adult", "@type": "https://openminds.om-i.org/types/AgeCategory", "definition": "''Adult'' categorizes the life cycle stage of an animal or human that reached sexual maturity.", - "interlexIdentifier": "http://uri.interlex.org/base/ilx_0729043", "name": "adult", + "otherOntologyIdentifier": "http://uri.interlex.org/base/ilx_0729043", "preferredOntologyIdentifier": "http://purl.obolibrary.org/obo/UBERON_0000113", "synonym": [ "adult stage", @@ -581,28 +541,60 @@ "name": "awake" }, { - "@id": "_:subject2", + "@id": "_:subject1-state", + "@type": "https://openminds.om-i.org/types/SubjectState", + "ageCategory": [ + { + "@id": "https://openminds.om-i.org/instances/ageCategory/adult" + } + ], + "attribute": [ + { + "@id": "https://openminds.om-i.org/instances/subjectAttribute/alive" + }, + { + "@id": "https://openminds.om-i.org/instances/subjectAttribute/awake" + } + ], + "internalIdentifier": "Subject1-state-01", + "lookupLabel": "Subject1-state" + }, + { + "@id": "_:subject1", "@type": "https://openminds.om-i.org/types/Subject", - "biologicalSex": { - "@id": "https://openminds.om-i.org/instances/biologicalSex/male" - }, - "internalIdentifier": "S2", - "lookupLabel": "Subject2", - "species": { - "@id": "_:c57bl6j-strain" - }, + "biologicalSex": [ + { + "@id": "https://openminds.om-i.org/instances/biologicalSex/male" + } + ], + "internalIdentifier": "S1", + "lookupLabel": "Subject1", + "species": [ + { + "@id": "_:c57bl6j-strain" + } + ], "studiedState": [ { - "@id": "_:subject2-state" + "@id": "_:subject1-state" } ] }, + { + "@id": "https://openminds.om-i.org/instances/ageCategory/adolescent", + "@type": "https://openminds.om-i.org/types/AgeCategory", + "definition": "''Adolescent'' categorizes a transitional life cycle stage of growth and development between childhood and adulthood, often described as ''puberty''.", + "name": "adolescent", + "synonym": "puberty" + }, { "@id": "_:subject2-state", "@type": "https://openminds.om-i.org/types/SubjectState", - "ageCategory": { - "@id": "https://openminds.om-i.org/instances/ageCategory/adolescent" - }, + "ageCategory": [ + { + "@id": "https://openminds.om-i.org/instances/ageCategory/adolescent" + } + ], "attribute": [ { "@id": "https://openminds.om-i.org/instances/subjectAttribute/alive" @@ -615,12 +607,24 @@ "lookupLabel": "Subject2-state" }, { - "@id": "https://openminds.om-i.org/instances/ageCategory/adolescent", - "@type": "https://openminds.om-i.org/types/AgeCategory", - "definition": "''Adolescent'' categorizes a transitional life cycle stage of growth and development between childhood and adulthood, often described as ''puberty''.", - "name": "adolescent", - "synonym": [ - "puberty" + "@id": "_:subject2", + "@type": "https://openminds.om-i.org/types/Subject", + "biologicalSex": [ + { + "@id": "https://openminds.om-i.org/instances/biologicalSex/male" + } + ], + "internalIdentifier": "S2", + "lookupLabel": "Subject2", + "species": [ + { + "@id": "_:c57bl6j-strain" + } + ], + "studiedState": [ + { + "@id": "_:subject2-state" + } ] }, { @@ -628,16 +632,169 @@ "@type": "https://openminds.om-i.org/types/Technique", "definition": "In ''extracellular electrophysiology'' electrodes are inserted into living tissue, but remain outside the cells in the extracellular environment to measure or stimulate electrical activity coming from adjacent cells, usually neurons.", "name": "extracellular electrophysiology" + }, + { + "@id": "_:cc-by-4", + "@type": "https://openminds.om-i.org/types/License", + "fullName": "Creative Commons Attribution 4.0 International", + "legalCode": "https://creativecommons.org/licenses/by/4.0/legalcode", + "shortName": "CC-BY-4.0", + "webpage": "https://creativecommons.org/licenses/by/4.0" + }, + { + "@id": "_:example-dataset-v1", + "@type": "https://openminds.om-i.org/types/DatasetVersion", + "accessibility": [ + { + "@id": "_:2f1d8cef-f64a-4795-9a9a-c3e237236085" + } + ], + "contribution": [ + { + "contributor": [ + { + "@id": "_:jane-doe" + }, + { + "@id": "_:john-smith" + } + ], + "type": [ + { + "@id": "https://openminds.om-i.org/instances/contributionType/authoring" + } + ], + "@type": "https://openminds.om-i.org/types/Contribution" + }, + { + "contributor": [ + { + "@id": "_:jane-doe" + } + ], + "type": [ + { + "@id": "https://openminds.om-i.org/instances/contributionType/custodianship" + } + ], + "@type": "https://openminds.om-i.org/types/Contribution" + } + ], + "contributorAffiliation": [ + { + "organization": [ + { + "@id": "_:university-of-neuroscience" + } + ], + "person": [ + { + "@id": "_:jane-doe" + } + ], + "@type": "https://openminds.om-i.org/types/Affiliation" + }, + { + "organization": [ + { + "@id": "_:brain-research-center" + } + ], + "person": [ + { + "@id": "_:john-smith" + } + ], + "@type": "https://openminds.om-i.org/types/Affiliation" + } + ], + "dataType": [ + { + "@id": "https://openminds.om-i.org/instances/semanticDataType/experimentalData" + } + ], + "description": "This dataset contains neural recordings from mice performing a visual discrimination task.", + "digitalIdentifier": [ + { + "@id": "_:dataset-doi" + } + ], + "documentation": [ + { + "@id": "_:dataset-documentation" + } + ], + "ethicsJurisdiction": [ + { + "@id": "https://openminds.om-i.org/instances/SovereignState/Germany" + } + ], + "experimentalApproach": [ + { + "@id": "https://openminds.om-i.org/instances/experimentalApproach/behavior" + }, + { + "@id": "https://openminds.om-i.org/instances/experimentalApproach/electrophysiology" + } + ], + "fullName": "Neural activity during visual discrimination task", + "isVersionOf": [ + { + "@id": "_:example-dataset" + } + ], + "keyword": [ + { + "@id": "_:custom-brain-region" + } + ], + "preparationType": [ + { + "@id": "https://openminds.om-i.org/instances/preparationType/inVivo" + } + ], + "protocol": [ + { + "@id": "_:visual-task-protocol" + } + ], + "releaseDate": "01-Jan-2023", + "repository": [ + { + "@id": "_:dataset-repository" + } + ], + "shortName": "Visual Task Dataset", + "studiedSpecimen": [ + { + "@id": "_:subject1" + }, + { + "@id": "_:subject2" + } + ], + "technique": [ + { + "@id": "https://openminds.om-i.org/instances/technique/extracellularElectrophysiology" + } + ], + "usageCondition": [ + { + "@id": "_:cc-by-4" + } + ], + "versionIdentifier": "v1", + "versionSpecification": "This is the first version of this dataset." } ] -}

6. Summary

In this tutorial, we've learned how to:
  1. Create a metadata collection
  2. Create various metadata instances (people, organizations, etc.)
  3. Link instances together
  4. Use controlled terms from predefined vocabularies
  5. Create custom term suggestions
  6. Add instances to a collection
  7. Save the collection to a JSON-LD file
This provides a foundation for creating more complex metadata for neuroscience datasets using the openMINDS MATLAB toolbox.
+}

6. Summary

In this tutorial, we've learned how to:
  1. Create a metadata collection
  2. Create various metadata instances (people, organizations, etc.)
  3. Link people, organizations, contributions, affiliations, and dataset metadata
  4. Use controlled terms from predefined vocabularies
  5. Create custom term suggestions
  6. Add instances to a collection
  7. Save the collection to a JSON-LD file
This provides a foundation for creating more complex metadata for neuroscience datasets using the openMINDS MATLAB toolbox.