Skip to content

Commit

Permalink
Various cleanup and changes inspired from openMINDS-MATLAB-UI (#20)
Browse files Browse the repository at this point in the history
* unreviewed changes

* Update getSchemaShortName.m

* fix

* Change: do not add controlled instances to collection

* Update Collection, ensure compatibility with R2022b

* Minor changes

* Update functions providing setup and startup routines

* Add isInstance and isMixedInstance utility functions

* Fix change namespace reference "om" to "openminds"

* ...

* Minor changes from reviewing PR #20

* Add newline eof

* Minor changes for PR #20

* Update loadInstances.m

fix typo

* Fix isMixedInstance: mixed up namespace order

* Update: Allow comparison of empty object in LinkedCategory class
  • Loading branch information
ehennestad authored Sep 4, 2024
1 parent 07dc77a commit f4260f1
Show file tree
Hide file tree
Showing 89 changed files with 766 additions and 184 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Vocab files
code/resources/.vocab

# Downloaded files / temporary folders
downloads/
target/
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ See also: [Crew Member Collection Tutorial](./docs/tutorials/crewMemberCollectio
```matlab
% Schema classes for all the openMINDS model versions are available in this
% toolbox. To ensure the latest version is used, run the following command:
selectOpenMindsVersion("latest")
openminds.startup("latest")
```
### Import schemas from the core model
```matlab
Expand Down
71 changes: 54 additions & 17 deletions code/+openminds/@Collection/Collection.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
Description (1,1) string
end

properties (SetAccess = private)
properties (SetAccess = protected)
Nodes (1,1) dictionary
end

Expand Down Expand Up @@ -105,7 +105,7 @@
if ~isempty(instance) && ~isempty(instance{1})
isFilePath = @(x) (ischar(x) || isstring(x)) && isfile(x);
isFolderPath = @(x) (ischar(x) || isstring(x)) && isfolder(x);
isMetadata = @(x) isa(x, 'openminds.abstract.Schema');
isMetadata = @(x) openminds.utility.isInstance(x);

% Initialize from file(s)
if all( cellfun(isFilePath, instance) )
Expand Down Expand Up @@ -134,7 +134,7 @@
len = numEntries(obj.Nodes);
end

function add(obj, instance)
function add(obj, instance, options)
%add Add single or multiple instances to a collection.
%
% Example usage:
Expand All @@ -149,26 +149,44 @@ function add(obj, instance)
arguments (Repeating)
instance % openminds.abstract.Schema
end
arguments
options.AddSubNodesOnly = false;
end

for i = 1:numel(instance)
thisInstance = instance{i};
for j = 1:numel(thisInstance) % If thisInstance is an array
obj.addNode(thisInstance(j));
obj.addNode(thisInstance(j), "AddSubNodesOnly", options.AddSubNodesOnly);
end
end
end

function tf = contains(obj, instance)
% Todo:work for arrays
if isKey(obj.Nodes, instance.id)
tf = true;
else
tf = false;
tf = false;

if isConfigured(obj.Nodes)
if isKey(obj.Nodes, instance.id)
tf = true;
end
end
end

function remove(obj, instance)
error('not implemented')

if isstring(instance) || ischar(instance)
instanceId = instance;
elseif openminds.utility.isInstance(instance)
instanceId = instance.id;
else
error('Unexpected type "%s" for instance argument', class(instance))
end

if isConfigured(obj.Nodes) && isKey(obj.Nodes, instanceId)
obj.Nodes(instanceId) = [];
else
error('Instance with id %s is not found in collection')
end
end

function instance = get(obj, nodeKey)
Expand All @@ -191,13 +209,22 @@ function remove(obj, instance)
return
end

keys = obj.Nodes.keys;
isMatch = startsWith(keys, type);
keys = keys(isMatch);

typeKeys = obj.TypeMap.keys;
isMatch = endsWith(typeKeys, "."+type); %i.e ".Person"
if any(isMatch)
if isMATLABReleaseOlderThan("R2023b")
keys = string( obj.TypeMap(typeKeys(isMatch)) );
else
keys = obj.TypeMap{typeKeys(isMatch)};
end
else
return
end

instances = obj.Nodes(keys);
instances = [instances{:}];

% Filter by property values:
for i = 1:numel(propertyName)
thisName = propertyName{i};
thisValue = propertyValue{i};
Expand Down Expand Up @@ -298,7 +325,11 @@ function load(obj, filePath)%, options)

instances = obj.loadInstances(jsonldFilePaths);
for i = 1:numel(instances)
obj.addNode(instances{i})
if openminds.utility.isInstance(instances{i})
obj.addNode(instances{i})
else
warning('todo')
end
end
end

Expand All @@ -312,7 +343,7 @@ function load(obj, filePath)%, options)

end

methods (Access = private)
methods (Access = protected)

%Add an instance to the Node container.
function addNode(obj, instance, options)
Expand All @@ -328,6 +359,11 @@ function addNode(obj, instance, options)
instance.id = obj.getBlankNodeIdentifier();
end

% Do not add openminds controlled term instances
if startsWith(instance.id, "https://openminds.ebrains.eu/instances/")
return
end

if isConfigured(obj.Nodes)
if isKey(obj.Nodes, instance.id)
%warning('Node with id %s already exists in collection', instance.id)
Expand Down Expand Up @@ -362,7 +398,9 @@ function addSubNodes(obj, instance)
% Add links.
linkedTypes = instance.getLinkedTypes();
for i = 1:numel(linkedTypes)
obj.addNode(linkedTypes{i});
if openminds.utility.isInstance(linkedTypes{i})
obj.addNode(linkedTypes{i});
end
end

% Add embeddings.
Expand All @@ -377,7 +415,6 @@ function addSubNodes(obj, instance)
identifier = length(obj) + 1;
identifier = sprintf(fmt, identifier);
end

end

end
77 changes: 55 additions & 22 deletions code/+openminds/@Collection/loadInstances.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,50 @@
switch serializationFormat
case ".jsonld"

%str = fileread(filePath);
% Read one or more files
str = arrayfun(@fileread, filePath, 'UniformOutput', false);

%structInstances = jsonld2struct(str);
% Produce a cell array of instances represented as structs
if numel(str) == 1
structInstances = jsonld2struct(str);
if ~iscell(structInstances); structInstances={structInstances};end
else
structInstances = cellfun(@jsonld2struct, str, 'UniformOutput', false);
end

% Create instance objects
instances = cell(size(structInstances));

% Create instances...
for i = 1:numel(structInstances)

thisInstance = structInstances{i};

openMindsType = thisInstance.at_type;
className = openminds.internal.utility.string.type2class(openMindsType);

assert( isequal( eval(sprintf('%s.X_TYPE', className)), openMindsType), ...
"Instance type does not match schema type. This is not supposed to happen, please report!")

instances{i} = feval(className, thisInstance);
if ~isfield(thisInstance, 'at_type')
continue % Todo: Why skip?
%instances{i} = struct('id', thisInstance.at_id);
else
openMindsType = thisInstance.at_type;
className = openminds.internal.utility.string.type2class(openMindsType);

assert( isequal( eval(sprintf('%s.X_TYPE', className)), openMindsType), ...
"Instance type does not match schema type. This is not supposed to happen, please report!")

try
instances{i} = feval(className, thisInstance);
catch ME
warning(ME.message)
end
end
end

isEmpty = cellfun(@(c) isempty(c), instances);
instances(isEmpty) = [];

instanceIds = cellfun(@(instance) instance.id, instances, 'UniformOutput', false);
instanceIds = string(instanceIds);

% Link instances / Resolve linked objects...
for i = 1:numel(instances)
resolveLinks(instances{i}, instances)
resolveLinks(instances{i}, instanceIds, instances)
end

otherwise
Expand All @@ -60,14 +75,26 @@
end
end

function resolveLinks(instance, instanceCollection)
function resolveLinks(instance, instanceIds, instanceCollection)
%resolveLinks Resolve linked types, i.e replace an @id with the actual
% instance object.

schemaInspector = openminds.internal.SchemaInspector(instance);

instanceIds = cellfun(@(instance) instance.id, instanceCollection, 'UniformOutput', false);
if isstruct(instance) % Instance is not resolvable (E.g belongs to remote collection)
return
end

persistent schemaInspectorMap
if isempty(schemaInspectorMap)
schemaInspectorMap = dictionary;
end

instanceType = class(instance);
if ~isConfigured(schemaInspectorMap) || ~isKey(schemaInspectorMap, instanceType)
schemaInspectorMap(instanceType) = openminds.internal.SchemaInspector(instance);
end

schemaInspector = schemaInspectorMap(instanceType);

for i = 1:schemaInspector.NumProperties
thisPropertyName = schemaInspector.PropertyNames{i};
if schemaInspector.isPropertyWithLinkedType(thisPropertyName)
Expand All @@ -76,7 +103,7 @@ function resolveLinks(instance, instanceCollection)
resolvedInstances = cell(size(linkedInstances));

for j = 1:numel(linkedInstances)
if isa(linkedInstances(j), 'openminds.internal.abstract.LinkedCategory')
if openminds.utility.isMixedInstance(linkedInstances(j))
try
instanceId = linkedInstances(j).Instance.id;
catch
Expand All @@ -86,16 +113,23 @@ function resolveLinks(instance, instanceCollection)
instanceId = linkedInstances(j).id;
end

isMatchedInstance = strcmp(instanceIds, instanceId);
isMatchedInstance = instanceIds == string(instanceId);

if any(isMatchedInstance)
resolvedInstances{j} = instanceCollection{isMatchedInstance};
resolveLinks(resolvedInstances{j}, instanceCollection)
resolveLinks(resolvedInstances{j}, instanceIds, instanceCollection)
else
% Check if instance is a controlled instance
if startsWith(instanceId, "https://openminds.ebrains.eu/instances/")
resolvedInstances{j} = om.instance.getControlledInstance(instanceId);
end
end
end

try
resolvedInstances = [resolvedInstances{:}];
catch
% pass. Todo, should there be error handling here?
end

if ~isempty(resolvedInstances)
Expand All @@ -106,14 +140,13 @@ function resolveLinks(instance, instanceCollection)
embeddedInstances = instance.(thisPropertyName);

for j = 1:numel(embeddedInstances)
if isa(embeddedInstances(j), 'openminds.internal.abstract.LinkedCategory')
if openminds.utility.isMixedInstance(embeddedInstances(j))
embeddedInstance = embeddedInstances(j).Instance;
else
embeddedInstance = embeddedInstances(j);
end
resolveLinks(embeddedInstance, instanceCollection)
resolveLinks(embeddedInstance, instanceIds, instanceCollection)
end
end
end
end

2 changes: 1 addition & 1 deletion code/+openminds/getSchemaVersion.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
end

versionStr = strrep(pathSplit{matchedIdx(1)}, schemaFolder, '');
end
end
20 changes: 20 additions & 0 deletions code/+openminds/startup.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function startup(version)
% startup - Startup routines for openMINDS_MATLAB
%
% This function ensures that only one version of openMINDS schema classes
% are on MATLAB's search path.

arguments
version (1,1) string = "latest"
end

disp('Initializing openMINDS_MATLAB...')

% NB: Assumes this function is located in code/+openminds:
codePath = fileparts( fileparts( mfilename('fullpath') ) );
addpath( fullfile(codePath, 'internal') )

% Run internal function that correctly configures the search path
openminds.selectOpenMindsVersion(version)
disp('Added schemas from the "latest" version to path')
end
17 changes: 13 additions & 4 deletions code/internal/+openminds/+abstract/ControlledTerm.m
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
end
end
end


methods (Access = protected) % Implement method for the CustomInstanceDisplay mixin

Expand Down Expand Up @@ -101,16 +100,26 @@ function deserializeFromName(obj, instanceName)
[~, instanceName] = openminds.utility.parseAtID(instanceName);
end

[instanceName, instanceNameOrig] = deal(instanceName);
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
if any(strcmpi(obj.CONTROLLED_INSTANCES, instanceName))
try
data = getControlledInstance(instanceName, schemaName, 'controlledTerms');
catch
warning('Controlled instance "%s" is not available.', instanceName)
s = warning('off', 'backtrace');
warning('Controlled instance "%s" is not available.', instanceNameOrig)
warning(s);
return
end
else
error('Deserialization from user instance is not implemented yet')
error('No matching instances were found for name "%s"', instanceName)
%error('Deserialization from user instance is not implemented yet')
end
propNames = {'at_id', 'name', 'definition', 'description', 'interlexIdentifier', 'knowledgeSpaceLink', 'preferredOntologyIdentifier', 'synonym'};

Expand Down Expand Up @@ -150,4 +159,4 @@ function deserializeFromName(obj, instanceName)
end
end

end
end
Loading

0 comments on commit f4260f1

Please sign in to comment.