Skip to content

Checks

Base

Base validation helpers shared across all catalog check modules.

ChecksBase

Core infrastructure: registry init, error helpers, shared checks, and publication.

Source code in MaRDMO/checks/base.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class ChecksBase:
    '''Core infrastructure: registry init, error helpers, shared checks, and publication.'''

    def __init__(self):
        '''Initialise with ontology registries and an empty error list.'''
        self.mathmoddb = get_mathmoddb()
        self.mathalgodb = get_mathalgodb()
        self.err = []

    # -------------------------------------------------------------------------
    # Helpers
    # -------------------------------------------------------------------------

    @staticmethod
    def _error(section, page, message):
        '''Format an error string ``"Section (Page X): message"``.'''
        return f"{section} (Page {page}): {message}"

    def _check_static(self, data, page_name, relation, from_class, to_class):
        '''Append an error if a mandatory single-value relation is missing or ``"not found"``.

        Args:
            data:       Entity answer dict.
            page_name:  Human-readable page label used in the error message.
            relation:   Key in *data* for the relation (e.g. ``"RelationRP"``).
            from_class: Display name of the source entity (for the error message).
            to_class:   Display name of the expected target entity.
        '''
        if not data.get(relation):
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Missing {to_class}'
                )
            )
        elif 'not found' in data[relation].values():
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Selected {to_class} not found in {to_class} Section'
                )
            )

    def _check_optional_static(self, data, page_name, relation, from_class, to_class):
        '''Append an error if an optional single-value relation is selected but ``"not found"``.

        Unlike :meth:`_check_static`, a missing relation is not an error.

        Args:
            data:       Entity answer dict.
            page_name:  Human-readable page label used in the error message.
            relation:   Key in *data* for the relation.
            from_class: Display name of the source entity.
            to_class:   Display name of the expected target entity.
        '''
        if data.get(relation) and 'not found' in data[relation].values():
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Selected {to_class} not found in {to_class} Section'
                )
            )

    def _check_without_section_items(self, items, parent_page, parent_class, item_class):
        '''Validate name and description for inline items that have no dedicated section.

        Only validates user-defined entries (``ID == "not found"``); portal-matched
        items are skipped.  Mirrors the checks in ``_name.html`` and
        ``_short_description.html``.

        Args:
            items:        Dict of item dicts (e.g. ``ivalue.get("programminglanguage", {})``)
            parent_page:  Human-readable page label of the parent entity.
            parent_class: Display name of the parent entity (for error section).
            item_class:   Display name of the inline item type (e.g. ``"Programming Language"``).
        '''
        for item in items.values():
            if item.get('ID') != 'not found':
                continue
            name = item.get('Name', '')
            desc = item.get('Description', '')
            label = name or '(unnamed)'
            if not name:
                self.err.append(self._error(
                    parent_class, parent_page,
                    f'Missing {item_class} Name ({label})'
                ))
            if not desc:
                self.err.append(self._error(
                    parent_class, parent_page,
                    f'Missing {item_class} Short Description ({label})'
                ))
            elif len(desc) > 250:
                self.err.append(self._error(
                    parent_class, parent_page,
                    f'{item_class} Short Description Too Long ({label})'
                ))
            elif desc == name:
                self.err.append(self._error(
                    parent_class, parent_page,
                    f'Equal {item_class} Name and Short Description Forbidden ({label})'
                ))

    def _check_doc_entries(self, doc, options, page_name, context_label):
        '''Validate that each selected doc entry has a non-empty text value.

        Args:
            doc:           Dict ``{collection_index: [option_uri, text]}``.
            options:       Options dict from :func:`~MaRDMO.getters.get_options`.
            page_name:     Human-readable page label for error messages.
            context_label: Label prefix for the error (e.g. ``"Software Requirements"``).
        '''
        for entry in doc.values():
            if not entry[1]:
                if entry[0] == options['DOI']:
                    label = 'DOI'
                elif entry[0] == options['URL']:
                    label = 'URL'
                else:
                    label = 'Value'
                self.err.append(self._error(
                    'Process Step', page_name,
                    f'Missing {context_label} {label}'
                ))
            elif entry[0] == options['URL'] and not is_valid_url(entry[1]):
                self.err.append(self._error(
                    'Process Step', page_name,
                    f'Invalid {context_label} URL: must start with http:// or https://'
                ))

    def _check_flexible(
        self, data, page_name, relation, from_class, to_class=None, optional=True
    ):
        '''Append errors for a typed multi-value relation block.

        Checks for: missing block (when *optional* is ``False``), entries
        with no relation type, entries pointing to ``"MISSING OBJECT ITEM"``,
        and entries pointing to ``"not found"`` items.

        Args:
            data:       Entity answer dict.
            page_name:  Human-readable page label for error messages.
            relation:   Key in *data* for the relation block.
            from_class: Display name of the source entity.
            to_class:   Display name of the target entity; defaults to
                        *from_class* when omitted.
            optional:   If ``False``, an empty relation block is also an error.
        '''
        to_class = to_class or from_class
        entries = data.get(relation, {})

        if not optional and not entries:
            self.err.append(self._error(from_class, page_name, f'Missing {to_class}'))

        if any(v['relation'] is None for v in entries.values()):
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Missing Relation Type ({to_class})'
                )
            )

        if any(v['relatant'] == 'MISSING OBJECT ITEM' for v in entries.values()):
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Missing Object Item ({to_class})'
                )
            )

        if any(v['relatant'] == 'not found' for v in entries.values()):
            self.err.append(
                self._error(
                    section = from_class,
                    page    = page_name,
                    message = f'Selected {to_class} not found in {to_class} Section'
                )
            )

    def id_name_description(self, project, data, catalog):
        '''Check that every entity page has a non-empty ID, Name, and Description.

        Also flags equal Name/Description pairs and descriptions exceeding
        250 characters.  Skips entity types not relevant to *catalog*.

        Args:
            project: RDMO project instance (used to look up page labels).
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        if catalog == CATALOG_ALGORITHM:
            section_map = SECTION_MAP_ALGO
            okeys = ('algorithm', 'problem', 'software', 'benchmark', 'publication')
        elif catalog in (CATALOG_MODEL, CATALOG_MODEL_BASICS):
            section_map = SECTION_MAP_MODEL
            okeys = (
                'model', 'formulation', 'quantity', 'task',
                'problem', 'field', 'publication'
            )
        elif catalog == CATALOG_WORKFLOW:
            section_map = SECTION_MAP_WORKFLOW
            okeys = (
                'workflow', 'processstep', 'algorithm', 'software',
                'hardware', 'dataset', 'publication'
            )
        else:
            return

        for okey, ovalue in data.items():
            if okey not in okeys:
                continue
            values = project.values.filter(
                snapshot  = None,
                attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/{okey}')
            )
            for ikey, ivalue in ovalue.items():
                page_name = values.get(set_index=ikey).text
                if not ivalue.get('ID'):
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page    = page_name,
                            message = 'Missing ID'
                        )
                    )
                if not ivalue.get('Name'):
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page    = page_name,
                            message = 'Missing Name'
                        )
                    )
                if not ivalue.get('Description'):
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page    = page_name,
                            message = 'Missing Short Description'
                        )
                    )
                if ivalue.get('Name') == ivalue.get('Description'):
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page    = page_name,
                            message = 'Equal Name and Short Description Forbidden'
                        )
                    )
                if ivalue.get('Description') and len(ivalue['Description']) > 250:
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page    = page_name,
                            message = 'Short Description Too Long'
                        )
                    )

    def _pairs(self, mapping, url_0, url_1):
        '''Return a set of the two URL strings for the given MathModDB/MathAlgoDB keys.'''
        return {
            mapping.get(key = url_0)["url"],
            mapping.get(key = url_1)["url"],
        }

    # -------------------------------------------------------------------------
    # Publication Check (shared, behaviour differs by mode)
    # -------------------------------------------------------------------------

    def publication(self, project, data, catalog):
        '''Check Publication documentation completeness.

        Flags user-defined publications that have no DOI reference.  In
        algorithm mode, at least one Algorithm or Benchmark/Software link is
        required; in model mode, a Mathematical Model Entity link is mandatory.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/publication')
        )
        for ikey, ivalue in data.get('publication', {}).items():
            page_name = values.get(set_index=ikey).text
            if ivalue.get('ID') == 'not found' and not ivalue.get('reference'):
                self.err.append(
                    self._error(
                        section = 'Publication',
                        page    = page_name,
                        message = 'Missing Publication DOI'
                    )
                )

            if catalog == CATALOG_ALGORITHM:
                # Algorithm mode: optional links, but at least one required
                if ivalue.get('RelationA') or ivalue.get('RelationBS'):
                    self._check_flexible(
                        data       = ivalue,
                        page_name  = page_name,
                        relation   = 'RelationA',
                        from_class = 'Publication',
                        to_class   = 'Algorithm',
                        optional   = True
                    )
                    self._check_flexible(
                        data       = ivalue,
                        page_name  = page_name,
                        relation   = 'RelationBS',
                        from_class = 'Publication',
                        to_class   = 'Benchmark/Software',
                        optional   = True
                    )
                else:
                    self.err.append(
                        self._error(
                            section = 'Publication',
                            page    = page_name,
                            message = 'Missing Algorithm, Benchmark, or Software'
                        )
                    )
            elif catalog == CATALOG_WORKFLOW:
                # Workflow mode: at least one of Algorithm, Hardware/Software, or Entity required
                if ivalue.get('RelationA') or ivalue.get('RelationHS') or ivalue.get('RelationP'):
                    self._check_flexible(
                        data       = ivalue,
                        page_name  = page_name,
                        relation   = 'RelationA',
                        from_class = 'Publication',
                        to_class   = 'Algorithm',
                        optional   = True
                    )
                    self._check_flexible(
                        data       = ivalue,
                        page_name  = page_name,
                        relation   = 'RelationHS',
                        from_class = 'Publication',
                        to_class   = 'Hardware or Software',
                        optional   = True
                    )
                    self._check_flexible(
                        data       = ivalue,
                        page_name  = page_name,
                        relation   = 'RelationP',
                        from_class = 'Publication',
                        to_class   = 'Interdisciplinary Workflow, Process Step, or Data Set',
                        optional   = True
                    )
                else:
                    self.err.append(
                        self._error(
                            section = 'Publication',
                            page    = page_name,
                            message = 'Missing Algorithm, Hardware/Software, or Workflow Entity'
                        )
                    )
            else:
                # Model mode: mandatory link to a model entity
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationP',
                    from_class = 'Publication',
                    to_class   = 'Mathematical Model Entity',
                    optional   = False
                )

    def _finalise(self):
        '''Sort errors, prepend a header, and return the list.

        Returns:
            ``self.err`` — sorted and with a leading header string when errors
            are present, or an empty list when no issues were found.
        '''
        if self.err:
            self.err.sort()
            self.err.insert(0, "Following aspects prevented the export:")
        return self.err

__init__()

Initialise with ontology registries and an empty error list.

Source code in MaRDMO/checks/base.py
19
20
21
22
23
def __init__(self):
    '''Initialise with ontology registries and an empty error list.'''
    self.mathmoddb = get_mathmoddb()
    self.mathalgodb = get_mathalgodb()
    self.err = []

id_name_description(project, data, catalog)

Check that every entity page has a non-empty ID, Name, and Description.

Also flags equal Name/Description pairs and descriptions exceeding 250 characters. Skips entity types not relevant to catalog.

Parameters:

Name Type Description Default
project

RDMO project instance (used to look up page labels).

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/base.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def id_name_description(self, project, data, catalog):
    '''Check that every entity page has a non-empty ID, Name, and Description.

    Also flags equal Name/Description pairs and descriptions exceeding
    250 characters.  Skips entity types not relevant to *catalog*.

    Args:
        project: RDMO project instance (used to look up page labels).
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    if catalog == CATALOG_ALGORITHM:
        section_map = SECTION_MAP_ALGO
        okeys = ('algorithm', 'problem', 'software', 'benchmark', 'publication')
    elif catalog in (CATALOG_MODEL, CATALOG_MODEL_BASICS):
        section_map = SECTION_MAP_MODEL
        okeys = (
            'model', 'formulation', 'quantity', 'task',
            'problem', 'field', 'publication'
        )
    elif catalog == CATALOG_WORKFLOW:
        section_map = SECTION_MAP_WORKFLOW
        okeys = (
            'workflow', 'processstep', 'algorithm', 'software',
            'hardware', 'dataset', 'publication'
        )
    else:
        return

    for okey, ovalue in data.items():
        if okey not in okeys:
            continue
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/{okey}')
        )
        for ikey, ivalue in ovalue.items():
            page_name = values.get(set_index=ikey).text
            if not ivalue.get('ID'):
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page    = page_name,
                        message = 'Missing ID'
                    )
                )
            if not ivalue.get('Name'):
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page    = page_name,
                        message = 'Missing Name'
                    )
                )
            if not ivalue.get('Description'):
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page    = page_name,
                        message = 'Missing Short Description'
                    )
                )
            if ivalue.get('Name') == ivalue.get('Description'):
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page    = page_name,
                        message = 'Equal Name and Short Description Forbidden'
                    )
                )
            if ivalue.get('Description') and len(ivalue['Description']) > 250:
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page    = page_name,
                        message = 'Short Description Too Long'
                    )
                )

publication(project, data, catalog)

Check Publication documentation completeness.

Flags user-defined publications that have no DOI reference. In algorithm mode, at least one Algorithm or Benchmark/Software link is required; in model mode, a Mathematical Model Entity link is mandatory.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/base.py
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
def publication(self, project, data, catalog):
    '''Check Publication documentation completeness.

    Flags user-defined publications that have no DOI reference.  In
    algorithm mode, at least one Algorithm or Benchmark/Software link is
    required; in model mode, a Mathematical Model Entity link is mandatory.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/publication')
    )
    for ikey, ivalue in data.get('publication', {}).items():
        page_name = values.get(set_index=ikey).text
        if ivalue.get('ID') == 'not found' and not ivalue.get('reference'):
            self.err.append(
                self._error(
                    section = 'Publication',
                    page    = page_name,
                    message = 'Missing Publication DOI'
                )
            )

        if catalog == CATALOG_ALGORITHM:
            # Algorithm mode: optional links, but at least one required
            if ivalue.get('RelationA') or ivalue.get('RelationBS'):
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationA',
                    from_class = 'Publication',
                    to_class   = 'Algorithm',
                    optional   = True
                )
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationBS',
                    from_class = 'Publication',
                    to_class   = 'Benchmark/Software',
                    optional   = True
                )
            else:
                self.err.append(
                    self._error(
                        section = 'Publication',
                        page    = page_name,
                        message = 'Missing Algorithm, Benchmark, or Software'
                    )
                )
        elif catalog == CATALOG_WORKFLOW:
            # Workflow mode: at least one of Algorithm, Hardware/Software, or Entity required
            if ivalue.get('RelationA') or ivalue.get('RelationHS') or ivalue.get('RelationP'):
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationA',
                    from_class = 'Publication',
                    to_class   = 'Algorithm',
                    optional   = True
                )
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationHS',
                    from_class = 'Publication',
                    to_class   = 'Hardware or Software',
                    optional   = True
                )
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationP',
                    from_class = 'Publication',
                    to_class   = 'Interdisciplinary Workflow, Process Step, or Data Set',
                    optional   = True
                )
            else:
                self.err.append(
                    self._error(
                        section = 'Publication',
                        page    = page_name,
                        message = 'Missing Algorithm, Hardware/Software, or Workflow Entity'
                    )
                )
        else:
            # Model mode: mandatory link to a model entity
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationP',
                from_class = 'Publication',
                to_class   = 'Mathematical Model Entity',
                optional   = False
            )

Algorithm

Algorithm Documentation check mixin.

AlgorithmMixin

Checks for Algorithm catalog entries.

Source code in MaRDMO/checks/algorithm.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class AlgorithmMixin:
    '''Checks for Algorithm catalog entries.'''

    # -------------------------------------------------------------------------
    # Algorithm Documentation Checks
    # -------------------------------------------------------------------------

    def algorithm(self, project, data):
        '''Check Algorithm documentation completeness.

        Verifies that each algorithm page has mandatory Algorithmic Task and
        Software links, and valid Algorithm–to–Algorithm relations.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot=None,
            attribute=Attribute.objects.get(uri=f'{BASE_URI}domain/algorithm')
        )
        for ikey, ivalue in data.get('algorithm', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationP',
                from_class = 'Algorithm',
                to_class   = 'Algorithmic Task'
            )
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationS',
                from_class = 'Algorithm',
                to_class   = 'Software'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationA',
                from_class = 'Algorithm')

    def algo_problem(self, project, data):
        '''Check Algorithmic Task documentation completeness.

        Verifies valid Algorithmic Task–to–Task relations and, if a Benchmark
        is selected, that it exists in the Benchmark section.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/problem')
        )
        for ikey, ivalue in data.get('problem', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_optional_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationB',
                from_class = 'Algorithmic Task',
                to_class   = 'Benchmark'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationP',
                from_class = 'Algorithmic Task'
            )

    def software(self, project, data):
        '''Check Software documentation completeness.

        Verifies that any selected Benchmark or Software dependency exists in
        its section, and that reference entries have a corresponding value when
        their option is selected.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/software')
        )
        for ikey, ivalue in data.get('software', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_optional_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationB',
                from_class = 'Software',
                to_class   = 'Benchmark'
            )
            self._check_optional_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationS',
                from_class = 'Software',
                to_class   = 'Software'
            )
            self._check_without_section_items(
                items        = ivalue.get('programminglanguage', {}),
                parent_page  = page_name,
                parent_class = 'Software',
                item_class   = 'Programming Language'
            )
            if not ivalue.get('reference'):
                self.err.append(self._error('Software', page_name, 'Missing Reference'))
            else:
                ref = ivalue['reference']
                ref_list = [
                    (0, 'DOI', 'ID'),
                    (1, 'swMath ID', 'ID'),
                    (2, 'Description URL', 'URL'),
                    (3, 'Repository URL', 'URL')
                ]
                for idx, label, noun in ref_list:
                    if ref.get(idx) and not ref[idx][1]:
                        self.err.append(
                            self._error(
                                section = 'Software',
                                page    = page_name,
                                message = f'{label} selected, but no {noun} provided!'
                            )
                        )
                    elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                        if not is_valid_url(ref[idx][1]):
                            self.err.append(self._error(
                                section = 'Software',
                                page    = page_name,
                                message = f'Invalid {label}: must start with http:// or https://'
                            ))

    def benchmark(self, project, data):
        '''Check Benchmark documentation completeness.

        Verifies that any reference entries (DOI, MORwiki ID, URL fields) have
        a corresponding value when their option is selected.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/benchmark')
        )
        for ikey, ivalue in data.get('benchmark', {}).items():
            page_name = values.get(set_index=ikey).text
            if not ivalue.get('reference'):
                self.err.append(self._error('Benchmark', page_name, 'Missing Reference'))
            else:
                ref = ivalue['reference']
                ref_list = [
                    (0, 'DOI', 'ID'),
                    (1, 'MORwiki ID', 'ID'),
                    (2, 'Description URL', 'URL'),
                    (3, 'Repository URL', 'URL')
                ]
                for idx, label, noun in ref_list:
                    if ref.get(idx) and not ref[idx][1]:
                        self.err.append(
                            self._error(
                                section = 'Benchmark',
                                page    = page_name,
                                message = f'{label} selected, but no {noun} provided!'
                            )
                        )
                    elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                        if not is_valid_url(ref[idx][1]):
                            self.err.append(self._error(
                                section = 'Benchmark',
                                page    = page_name,
                                message = f'Invalid {label}: must start with http:// or https://'
                            ))

    # -------------------------------------------------------------------------
    # Run method
    # -------------------------------------------------------------------------

    def run_algorithm(self, project, data, catalog=None):
        '''Run all algorithm-catalog checks and return the collected error list.

        Executes, in order: ID/Name/Description, algorithm, algorithmic task,
        software, benchmark, and publication checks.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.

        Returns:
            List of human-readable error strings, or an empty list when all
            checks pass.
        '''
        catalog = CATALOG_ALGORITHM
        self.id_name_description(project, data, catalog)
        self.algorithm(project, data)
        self.algo_problem(project, data)
        self.software(project, data)
        self.benchmark(project, data)
        self.publication(project, data, catalog)
        return self._finalise()

algo_problem(project, data)

Check Algorithmic Task documentation completeness.

Verifies valid Algorithmic Task–to–Task relations and, if a Benchmark is selected, that it exists in the Benchmark section.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/algorithm.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def algo_problem(self, project, data):
    '''Check Algorithmic Task documentation completeness.

    Verifies valid Algorithmic Task–to–Task relations and, if a Benchmark
    is selected, that it exists in the Benchmark section.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/problem')
    )
    for ikey, ivalue in data.get('problem', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_optional_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationB',
            from_class = 'Algorithmic Task',
            to_class   = 'Benchmark'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationP',
            from_class = 'Algorithmic Task'
        )

algorithm(project, data)

Check Algorithm documentation completeness.

Verifies that each algorithm page has mandatory Algorithmic Task and Software links, and valid Algorithm–to–Algorithm relations.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/algorithm.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def algorithm(self, project, data):
    '''Check Algorithm documentation completeness.

    Verifies that each algorithm page has mandatory Algorithmic Task and
    Software links, and valid Algorithm–to–Algorithm relations.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot=None,
        attribute=Attribute.objects.get(uri=f'{BASE_URI}domain/algorithm')
    )
    for ikey, ivalue in data.get('algorithm', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationP',
            from_class = 'Algorithm',
            to_class   = 'Algorithmic Task'
        )
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationS',
            from_class = 'Algorithm',
            to_class   = 'Software'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationA',
            from_class = 'Algorithm')

benchmark(project, data)

Check Benchmark documentation completeness.

Verifies that any reference entries (DOI, MORwiki ID, URL fields) have a corresponding value when their option is selected.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/algorithm.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def benchmark(self, project, data):
    '''Check Benchmark documentation completeness.

    Verifies that any reference entries (DOI, MORwiki ID, URL fields) have
    a corresponding value when their option is selected.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/benchmark')
    )
    for ikey, ivalue in data.get('benchmark', {}).items():
        page_name = values.get(set_index=ikey).text
        if not ivalue.get('reference'):
            self.err.append(self._error('Benchmark', page_name, 'Missing Reference'))
        else:
            ref = ivalue['reference']
            ref_list = [
                (0, 'DOI', 'ID'),
                (1, 'MORwiki ID', 'ID'),
                (2, 'Description URL', 'URL'),
                (3, 'Repository URL', 'URL')
            ]
            for idx, label, noun in ref_list:
                if ref.get(idx) and not ref[idx][1]:
                    self.err.append(
                        self._error(
                            section = 'Benchmark',
                            page    = page_name,
                            message = f'{label} selected, but no {noun} provided!'
                        )
                    )
                elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                    if not is_valid_url(ref[idx][1]):
                        self.err.append(self._error(
                            section = 'Benchmark',
                            page    = page_name,
                            message = f'Invalid {label}: must start with http:// or https://'
                        ))

run_algorithm(project, data, catalog=None)

Run all algorithm-catalog checks and return the collected error list.

Executes, in order: ID/Name/Description, algorithm, algorithmic task, software, benchmark, and publication checks.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required

Returns:

Type Description

List of human-readable error strings, or an empty list when all

checks pass.

Source code in MaRDMO/checks/algorithm.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def run_algorithm(self, project, data, catalog=None):
    '''Run all algorithm-catalog checks and return the collected error list.

    Executes, in order: ID/Name/Description, algorithm, algorithmic task,
    software, benchmark, and publication checks.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.

    Returns:
        List of human-readable error strings, or an empty list when all
        checks pass.
    '''
    catalog = CATALOG_ALGORITHM
    self.id_name_description(project, data, catalog)
    self.algorithm(project, data)
    self.algo_problem(project, data)
    self.software(project, data)
    self.benchmark(project, data)
    self.publication(project, data, catalog)
    return self._finalise()

software(project, data)

Check Software documentation completeness.

Verifies that any selected Benchmark or Software dependency exists in its section, and that reference entries have a corresponding value when their option is selected.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/algorithm.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def software(self, project, data):
    '''Check Software documentation completeness.

    Verifies that any selected Benchmark or Software dependency exists in
    its section, and that reference entries have a corresponding value when
    their option is selected.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/software')
    )
    for ikey, ivalue in data.get('software', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_optional_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationB',
            from_class = 'Software',
            to_class   = 'Benchmark'
        )
        self._check_optional_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationS',
            from_class = 'Software',
            to_class   = 'Software'
        )
        self._check_without_section_items(
            items        = ivalue.get('programminglanguage', {}),
            parent_page  = page_name,
            parent_class = 'Software',
            item_class   = 'Programming Language'
        )
        if not ivalue.get('reference'):
            self.err.append(self._error('Software', page_name, 'Missing Reference'))
        else:
            ref = ivalue['reference']
            ref_list = [
                (0, 'DOI', 'ID'),
                (1, 'swMath ID', 'ID'),
                (2, 'Description URL', 'URL'),
                (3, 'Repository URL', 'URL')
            ]
            for idx, label, noun in ref_list:
                if ref.get(idx) and not ref[idx][1]:
                    self.err.append(
                        self._error(
                            section = 'Software',
                            page    = page_name,
                            message = f'{label} selected, but no {noun} provided!'
                        )
                    )
                elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                    if not is_valid_url(ref[idx][1]):
                        self.err.append(self._error(
                            section = 'Software',
                            page    = page_name,
                            message = f'Invalid {label}: must start with http:// or https://'
                        ))

Model

Model Documentation check mixin.

ModelMixin

Checks for Mathematical Model catalog entries.

Source code in MaRDMO/checks/model.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
class ModelMixin:
    '''Checks for Mathematical Model catalog entries.'''

    # -------------------------------------------------------------------------
    # Model Documentation Checks
    # -------------------------------------------------------------------------

    def properties(self, project, data, catalog):
        '''Check for mutually exclusive (conflicting) data-property combinations.

        Uses ``data_properties_check`` pairs to detect invalid co-occurrences
        (e.g. *linear* and *nonlinear* both selected for the same entity).

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        section_map = SECTION_MAP_MODEL

        if catalog == CATALOG_MODEL_BASICS:
            okeys = ('model', 'formulation', 'task')
        elif catalog == CATALOG_MODEL:
            okeys = ('model', 'formulation', 'quantity', 'task')
        else:
            return

        for okey, ovalue in data.items():
            if okey not in okeys:
                continue
            values = project.values.filter(
                snapshot  = None,
                attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/{okey}')
            )
            for ikey, ivalue in ovalue.items():
                page_name = values.get(set_index=ikey).text
                if not ivalue.get('Properties'):
                    continue
                properties = ivalue['Properties'].values()
                for url in data_properties_check:
                    if not self._pairs(self.mathmoddb, url[0], url[1]).issubset(properties):
                        continue
                    self.err.append(
                        self._error(
                            section = section_map[okey],
                            page = page_name,
                            message = f'Inconsistent Properties ({self.mathmoddb.get(key=url[0])["label"]}'
                                      f' and {self.mathmoddb.get(key=url[1])["label"]})'
                        )
                    )

    def model(self, project, data, catalog):
        '''Check Mathematical Model documentation completeness and consistency.

        Verifies that each model page has mandatory Research Problem and Task
        links, valid Formula relations, and (for the full
        catalog) consistent specialisation assumptions and expression ordering.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/model')
        )
        for ikey, ivalue in data.get('model', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationRP',
                from_class = 'Mathematical Model',
                to_class   = 'Research Problem'
            )
            self._check_optional_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationT',
                from_class = 'Mathematical Model',
                to_class   = 'Computational Task'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationMM',
                from_class = 'Mathematical Model'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationMF',
                from_class = 'Mathematical Model',
                to_class   = 'Formula',
                optional   = False
            )

            if catalog == CATALOG_MODEL_BASICS:
                return

            if any(
                mval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and not mval.get('assumption')
                for mval in ivalue.get('RelationMM', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Mathematical Model',
                        page    = page_name,
                        message = 'Missing Assumption (Mathematical Model Specialization)'
                    )
                )

            if any(
                mval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and 'not found' in mval.get('assumption', {}).values()
                for mval in ivalue.get('RelationMM', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Mathematical Model',
                        page    = page_name,
                        message = 'Selected Formula not found in Section'
                    )
                )

            relation_mf = ivalue.get('RelationMF', {})
            if relation_mf:
                orders = [val.get('order') for val in relation_mf.values()]
                if any(order is not None for order in orders):
                    if not all(order is not None for order in orders):
                        self.err.append(
                            self._error(
                                section = 'Mathematical Model',
                                page    = page_name,
                                message = 'Missing Order Number (Formula)'
                            )
                        )
                    else:
                        order_numbers = set(int(order) for order in orders)
                        if order_numbers != set(range(1, len(relation_mf) + 1)):
                            self.err.append(
                                self._error(
                                    section = 'Mathematical Model',
                                    page    = page_name,
                                    message = 'Incorrect Order Number (Formula)'
                                )
                            )

    def task(self, project, data, catalog):
        '''Check Computational Task documentation completeness and consistency.

        Verifies that each task page has mandatory Formula links,
        valid task–task relations, and (for the full catalog) specialisation
        assumptions, containment order numbers, and Quantity links.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/task')
        )
        for ikey, ivalue in data.get('task', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationT',
                from_class = 'Computational Task'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationMF',
                from_class = 'Computational Task',
                to_class   = 'Formula',
                optional   = False
            )

            if catalog == CATALOG_MODEL_BASICS:
                return

            if any(
                tval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and not tval.get('assumption')
                for tval in ivalue.get('RelationT', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Computational Task',
                        page    = page_name,
                        message = 'Missing Assumption (Mathematical Model Specialization)'
                    )
                )

            if any(
                tval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and 'not found' in tval.get('assumption', {}).values()
                for tval in ivalue.get('RelationT', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Computational Task',
                        page    = page_name,
                        message = 'Selected Formula not found in Section'
                    )
                )

            if any(
                tval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'contains_task', 'contained_in_task'
                )
                and not tval.get('order')
                for tval in ivalue.get('RelationT', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Computational Task',
                        page    = page_name,
                        message = 'Missing Order Number (Computational Task Containment)'
                    )
                )

            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationQQK',
                from_class = 'Computational Task',
                to_class   = 'Quantity',
                optional   = False
            )

    def formulation(self, project, data, catalog):
        '''Check Formula documentation completeness and consistency.

        For the basics catalog, flags missing references on user-defined entries.
        For the full catalog, also verifies specialisation assumptions, the
        presence of a formula, and that every element has a symbol and quantity.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/formulation')
        )
        for ikey, ivalue in data.get('formulation', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationMF2',
                from_class ='Formula'
            )

            if catalog == CATALOG_MODEL_BASICS:
                if ivalue.get('ID') == 'not found' and not ivalue.get('reference'):
                    self.err.append(
                        self._error(
                            section = 'Formula',
                            page    = page_name,
                            message = 'Missing Reference'
                        )
                    )
                return

            if any(
                mval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and not mval.get('assumption')
                for mval in ivalue.get('RelationMF2', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Assumption (Formula Specialization)'))

            if any(
                mval['relation']['url'] in self._pairs(
                    self.mathmoddb, 'specializes', 'specialized_by'
                )
                and 'not found' in mval.get('assumption', {}).values()
                for mval in ivalue.get('RelationMF2', {}).values()
            ):
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Selected Formula not found in Section'
                    )
                )

            if not ivalue.get('Formula'):
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Formula Formula'
                    )
                )

            if not ivalue.get('element'):
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Formula Element Information'
                    )
                )
            else:
                not_symbol = any(not ev.get('symbol') for ev in ivalue['element'].values())
                not_quantity = any(not ev.get('quantity') for ev in ivalue['element'].values())
                if not_symbol:
                    self.err.append(
                        self._error(
                            section = 'Formula',
                            page    = page_name,
                            message = 'Missing Formula Symbol'
                        )
                    )
                if not_quantity:
                    self.err.append(
                        self._error(
                            section = 'Formula',
                            page    = page_name,
                            message = 'Missing Formula Quantity'
                        )
                    )

            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationMF1',
                from_class = 'Formula'
            )

    def quantity(self, project, data, catalog):
        '''Check Quantity [Kind] documentation completeness and consistency.

        Skipped entirely for the basics catalog.  Validates the Quantity/QuantityKind
        class selection, QUDT reference ID presence, formula ``\\equiv`` sign, formula
        element completeness, and relation blocks for Quantity and QuantityKind pages.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        if catalog == CATALOG_MODEL_BASICS:
            return
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/quantity')
        )
        for ikey, ivalue in data.get('quantity', {}).items():
            page_name = values.get(set_index=ikey).text
            if not ivalue.get('QorQK'):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'Missing Quantity [Kind] Class'
                    )
                )

            if ivalue.get('reference'):
                ref = ivalue['reference']
                if ref.get(0) and not ref[0][1]:
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'QUDT Quantity Kind ID selected, but no ID provided!'
                        )
                    )
                elif ref.get(0) and ref[0][1] and not _QUDT_ID_RE.match(ref[0][1]):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = f'Invalid QUDT Quantity Kind ID format: "{ref[0][1]}"'
                        )
                    )
                if ref.get(1) and not ref[1][1]:
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'QUDT Constant ID selected, but no ID provided!'
                        )
                    )
                elif ref.get(1) and ref[1][1] and not _QUDT_ID_RE.match(ref[1][1]):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = f'Invalid QUDT Constant ID format: "{ref[1][1]}"'
                        )
                    )
                if ivalue.get('QorQK') == self.mathmoddb.get(key='Quantity')["url"] and ref.get(0):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'QUDT Quantity Kind ID limited to Quantity Kinds!'
                        )
                    )
                if (
                    ivalue.get('QorQK') == self.mathmoddb.get(key='QuantityKind')["url"]
                    and ref.get(1)
                ):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'QUDT Constant ID limited to Quantities!'
                        )
                    )

            if ivalue.get('Formula'):
                equiv_sign_encodings = ('>≡</', '>&#x2261;</', '>&equiv;</', '\\equiv', '\\Equiv')
                for formula in ivalue['Formula'].values():
                    if not any(equiv in formula for equiv in equiv_sign_encodings):
                        self.err.append(
                            self._error(
                                section = 'Quantity [Kind]',
                                page    = page_name,
                                message = r'Inconsistent Quantity Definition (missing \equiv)'
                            )
                        )
                if not ivalue.get('element'):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'Missing Quantity Definition Element Information'
                        )
                    )
                else:
                    if any(not ev.get('symbol') for ev in ivalue['element'].values()):
                        self.err.append(
                            self._error(
                                section = 'Quantity [Kind]',
                                page    = page_name,
                                message = 'Missing Quantity Definition Symbol'
                            )
                        )
                    if any(not ev.get('quantity') for ev in ivalue['element'].values()):
                        self.err.append(
                            self._error(
                                section = 'Quantity [Kind]',
                                page    = page_name,
                                message = 'Missing Quantity Definition Quantity'
                            )
                        )

            if ivalue.get('QorQK') == self.mathmoddb.get(key='Quantity')["url"]:
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationQQ',
                    from_class = 'Quantity'
                )
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationQQK',
                    from_class = 'Quantity'
                )
            elif ivalue.get('QorQK') == self.mathmoddb.get(key='QuantityKind')["url"]:
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationQKQK',
                    from_class = 'Quantity'
                )
                self._check_flexible(
                    data       = ivalue,
                    page_name  = page_name,
                    relation   = 'RelationQKQ',
                    from_class = 'Quantity'
                )

    def model_problem(self, project, data, catalog):
        '''Check Research Problem documentation completeness.

        Verifies that each problem page has valid Research Problem relations and,
        for the full catalog, a mandatory Academic Discipline link.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/problem')
        )
        for ikey, ivalue in data.get('problem', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationRP',
                from_class = 'Research Problem'
            )
            if catalog == CATALOG_MODEL_BASICS:
                return
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationRF',
                from_class = 'Research Problem',
                to_class   = 'Academic Discipline'
            )

    def field(self, project, data, catalog):
        '''Check Academic Discipline relation completeness.

        Skipped for the basics catalog.  Verifies that each discipline page
        has valid Academic Discipline–to–discipline relations.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.
        '''
        if catalog == CATALOG_MODEL_BASICS:
            return
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/field')
        )
        for ikey, ivalue in data.get('field', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationRF',
                from_class = 'Academic Discipline'
            )

    # -------------------------------------------------------------------------
    # Run method
    # -------------------------------------------------------------------------

    def run_model(self, project, data, catalog):
        '''Run all model-catalog checks and return the collected error list.

        Executes, in order: ID/Name/Description, data properties, model, task,
        formulation, quantity, research problem, academic discipline, and
        publication checks.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
            catalog: Active catalog URI suffix.

        Returns:
            List of human-readable error strings, or an empty list when all
            checks pass.
        '''
        self.id_name_description(project, data, catalog)
        self.properties(project, data, catalog)
        self.model(project, data, catalog)
        self.task(project, data, catalog)
        self.formulation(project, data, catalog)
        self.quantity(project, data, catalog)
        self.model_problem(project, data, catalog)
        self.field(project, data, catalog)
        self.publication(project, data, catalog)
        return self._finalise()

field(project, data, catalog)

Check Academic Discipline relation completeness.

Skipped for the basics catalog. Verifies that each discipline page has valid Academic Discipline–to–discipline relations.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def field(self, project, data, catalog):
    '''Check Academic Discipline relation completeness.

    Skipped for the basics catalog.  Verifies that each discipline page
    has valid Academic Discipline–to–discipline relations.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    if catalog == CATALOG_MODEL_BASICS:
        return
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/field')
    )
    for ikey, ivalue in data.get('field', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationRF',
            from_class = 'Academic Discipline'
        )

formulation(project, data, catalog)

Check Formula documentation completeness and consistency.

For the basics catalog, flags missing references on user-defined entries. For the full catalog, also verifies specialisation assumptions, the presence of a formula, and that every element has a symbol and quantity.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def formulation(self, project, data, catalog):
    '''Check Formula documentation completeness and consistency.

    For the basics catalog, flags missing references on user-defined entries.
    For the full catalog, also verifies specialisation assumptions, the
    presence of a formula, and that every element has a symbol and quantity.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/formulation')
    )
    for ikey, ivalue in data.get('formulation', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationMF2',
            from_class ='Formula'
        )

        if catalog == CATALOG_MODEL_BASICS:
            if ivalue.get('ID') == 'not found' and not ivalue.get('reference'):
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Reference'
                    )
                )
            return

        if any(
            mval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and not mval.get('assumption')
            for mval in ivalue.get('RelationMF2', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Formula',
                    page    = page_name,
                    message = 'Missing Assumption (Formula Specialization)'))

        if any(
            mval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and 'not found' in mval.get('assumption', {}).values()
            for mval in ivalue.get('RelationMF2', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Formula',
                    page    = page_name,
                    message = 'Selected Formula not found in Section'
                )
            )

        if not ivalue.get('Formula'):
            self.err.append(
                self._error(
                    section = 'Formula',
                    page    = page_name,
                    message = 'Missing Formula Formula'
                )
            )

        if not ivalue.get('element'):
            self.err.append(
                self._error(
                    section = 'Formula',
                    page    = page_name,
                    message = 'Missing Formula Element Information'
                )
            )
        else:
            not_symbol = any(not ev.get('symbol') for ev in ivalue['element'].values())
            not_quantity = any(not ev.get('quantity') for ev in ivalue['element'].values())
            if not_symbol:
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Formula Symbol'
                    )
                )
            if not_quantity:
                self.err.append(
                    self._error(
                        section = 'Formula',
                        page    = page_name,
                        message = 'Missing Formula Quantity'
                    )
                )

        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationMF1',
            from_class = 'Formula'
        )

model(project, data, catalog)

Check Mathematical Model documentation completeness and consistency.

Verifies that each model page has mandatory Research Problem and Task links, valid Formula relations, and (for the full catalog) consistent specialisation assumptions and expression ordering.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def model(self, project, data, catalog):
    '''Check Mathematical Model documentation completeness and consistency.

    Verifies that each model page has mandatory Research Problem and Task
    links, valid Formula relations, and (for the full
    catalog) consistent specialisation assumptions and expression ordering.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/model')
    )
    for ikey, ivalue in data.get('model', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationRP',
            from_class = 'Mathematical Model',
            to_class   = 'Research Problem'
        )
        self._check_optional_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationT',
            from_class = 'Mathematical Model',
            to_class   = 'Computational Task'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationMM',
            from_class = 'Mathematical Model'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationMF',
            from_class = 'Mathematical Model',
            to_class   = 'Formula',
            optional   = False
        )

        if catalog == CATALOG_MODEL_BASICS:
            return

        if any(
            mval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and not mval.get('assumption')
            for mval in ivalue.get('RelationMM', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Mathematical Model',
                    page    = page_name,
                    message = 'Missing Assumption (Mathematical Model Specialization)'
                )
            )

        if any(
            mval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and 'not found' in mval.get('assumption', {}).values()
            for mval in ivalue.get('RelationMM', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Mathematical Model',
                    page    = page_name,
                    message = 'Selected Formula not found in Section'
                )
            )

        relation_mf = ivalue.get('RelationMF', {})
        if relation_mf:
            orders = [val.get('order') for val in relation_mf.values()]
            if any(order is not None for order in orders):
                if not all(order is not None for order in orders):
                    self.err.append(
                        self._error(
                            section = 'Mathematical Model',
                            page    = page_name,
                            message = 'Missing Order Number (Formula)'
                        )
                    )
                else:
                    order_numbers = set(int(order) for order in orders)
                    if order_numbers != set(range(1, len(relation_mf) + 1)):
                        self.err.append(
                            self._error(
                                section = 'Mathematical Model',
                                page    = page_name,
                                message = 'Incorrect Order Number (Formula)'
                            )
                        )

model_problem(project, data, catalog)

Check Research Problem documentation completeness.

Verifies that each problem page has valid Research Problem relations and, for the full catalog, a mandatory Academic Discipline link.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def model_problem(self, project, data, catalog):
    '''Check Research Problem documentation completeness.

    Verifies that each problem page has valid Research Problem relations and,
    for the full catalog, a mandatory Academic Discipline link.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/problem')
    )
    for ikey, ivalue in data.get('problem', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationRP',
            from_class = 'Research Problem'
        )
        if catalog == CATALOG_MODEL_BASICS:
            return
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationRF',
            from_class = 'Research Problem',
            to_class   = 'Academic Discipline'
        )

properties(project, data, catalog)

Check for mutually exclusive (conflicting) data-property combinations.

Uses data_properties_check pairs to detect invalid co-occurrences (e.g. linear and nonlinear both selected for the same entity).

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def properties(self, project, data, catalog):
    '''Check for mutually exclusive (conflicting) data-property combinations.

    Uses ``data_properties_check`` pairs to detect invalid co-occurrences
    (e.g. *linear* and *nonlinear* both selected for the same entity).

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    section_map = SECTION_MAP_MODEL

    if catalog == CATALOG_MODEL_BASICS:
        okeys = ('model', 'formulation', 'task')
    elif catalog == CATALOG_MODEL:
        okeys = ('model', 'formulation', 'quantity', 'task')
    else:
        return

    for okey, ovalue in data.items():
        if okey not in okeys:
            continue
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/{okey}')
        )
        for ikey, ivalue in ovalue.items():
            page_name = values.get(set_index=ikey).text
            if not ivalue.get('Properties'):
                continue
            properties = ivalue['Properties'].values()
            for url in data_properties_check:
                if not self._pairs(self.mathmoddb, url[0], url[1]).issubset(properties):
                    continue
                self.err.append(
                    self._error(
                        section = section_map[okey],
                        page = page_name,
                        message = f'Inconsistent Properties ({self.mathmoddb.get(key=url[0])["label"]}'
                                  f' and {self.mathmoddb.get(key=url[1])["label"]})'
                    )
                )

quantity(project, data, catalog)

Check Quantity [Kind] documentation completeness and consistency.

Skipped entirely for the basics catalog. Validates the Quantity/QuantityKind class selection, QUDT reference ID presence, formula \equiv sign, formula element completeness, and relation blocks for Quantity and QuantityKind pages.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
def quantity(self, project, data, catalog):
    '''Check Quantity [Kind] documentation completeness and consistency.

    Skipped entirely for the basics catalog.  Validates the Quantity/QuantityKind
    class selection, QUDT reference ID presence, formula ``\\equiv`` sign, formula
    element completeness, and relation blocks for Quantity and QuantityKind pages.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    if catalog == CATALOG_MODEL_BASICS:
        return
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/quantity')
    )
    for ikey, ivalue in data.get('quantity', {}).items():
        page_name = values.get(set_index=ikey).text
        if not ivalue.get('QorQK'):
            self.err.append(
                self._error(
                    section = 'Quantity [Kind]',
                    page    = page_name,
                    message = 'Missing Quantity [Kind] Class'
                )
            )

        if ivalue.get('reference'):
            ref = ivalue['reference']
            if ref.get(0) and not ref[0][1]:
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'QUDT Quantity Kind ID selected, but no ID provided!'
                    )
                )
            elif ref.get(0) and ref[0][1] and not _QUDT_ID_RE.match(ref[0][1]):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = f'Invalid QUDT Quantity Kind ID format: "{ref[0][1]}"'
                    )
                )
            if ref.get(1) and not ref[1][1]:
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'QUDT Constant ID selected, but no ID provided!'
                    )
                )
            elif ref.get(1) and ref[1][1] and not _QUDT_ID_RE.match(ref[1][1]):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = f'Invalid QUDT Constant ID format: "{ref[1][1]}"'
                    )
                )
            if ivalue.get('QorQK') == self.mathmoddb.get(key='Quantity')["url"] and ref.get(0):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'QUDT Quantity Kind ID limited to Quantity Kinds!'
                    )
                )
            if (
                ivalue.get('QorQK') == self.mathmoddb.get(key='QuantityKind')["url"]
                and ref.get(1)
            ):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'QUDT Constant ID limited to Quantities!'
                    )
                )

        if ivalue.get('Formula'):
            equiv_sign_encodings = ('>≡</', '>&#x2261;</', '>&equiv;</', '\\equiv', '\\Equiv')
            for formula in ivalue['Formula'].values():
                if not any(equiv in formula for equiv in equiv_sign_encodings):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = r'Inconsistent Quantity Definition (missing \equiv)'
                        )
                    )
            if not ivalue.get('element'):
                self.err.append(
                    self._error(
                        section = 'Quantity [Kind]',
                        page    = page_name,
                        message = 'Missing Quantity Definition Element Information'
                    )
                )
            else:
                if any(not ev.get('symbol') for ev in ivalue['element'].values()):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'Missing Quantity Definition Symbol'
                        )
                    )
                if any(not ev.get('quantity') for ev in ivalue['element'].values()):
                    self.err.append(
                        self._error(
                            section = 'Quantity [Kind]',
                            page    = page_name,
                            message = 'Missing Quantity Definition Quantity'
                        )
                    )

        if ivalue.get('QorQK') == self.mathmoddb.get(key='Quantity')["url"]:
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationQQ',
                from_class = 'Quantity'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationQQK',
                from_class = 'Quantity'
            )
        elif ivalue.get('QorQK') == self.mathmoddb.get(key='QuantityKind')["url"]:
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationQKQK',
                from_class = 'Quantity'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationQKQ',
                from_class = 'Quantity'
            )

run_model(project, data, catalog)

Run all model-catalog checks and return the collected error list.

Executes, in order: ID/Name/Description, data properties, model, task, formulation, quantity, research problem, academic discipline, and publication checks.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required

Returns:

Type Description

List of human-readable error strings, or an empty list when all

checks pass.

Source code in MaRDMO/checks/model.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
def run_model(self, project, data, catalog):
    '''Run all model-catalog checks and return the collected error list.

    Executes, in order: ID/Name/Description, data properties, model, task,
    formulation, quantity, research problem, academic discipline, and
    publication checks.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.

    Returns:
        List of human-readable error strings, or an empty list when all
        checks pass.
    '''
    self.id_name_description(project, data, catalog)
    self.properties(project, data, catalog)
    self.model(project, data, catalog)
    self.task(project, data, catalog)
    self.formulation(project, data, catalog)
    self.quantity(project, data, catalog)
    self.model_problem(project, data, catalog)
    self.field(project, data, catalog)
    self.publication(project, data, catalog)
    return self._finalise()

task(project, data, catalog)

Check Computational Task documentation completeness and consistency.

Verifies that each task page has mandatory Formula links, valid task–task relations, and (for the full catalog) specialisation assumptions, containment order numbers, and Quantity links.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
catalog

Active catalog URI suffix.

required
Source code in MaRDMO/checks/model.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def task(self, project, data, catalog):
    '''Check Computational Task documentation completeness and consistency.

    Verifies that each task page has mandatory Formula links,
    valid task–task relations, and (for the full catalog) specialisation
    assumptions, containment order numbers, and Quantity links.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
        catalog: Active catalog URI suffix.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/task')
    )
    for ikey, ivalue in data.get('task', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationT',
            from_class = 'Computational Task'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationMF',
            from_class = 'Computational Task',
            to_class   = 'Formula',
            optional   = False
        )

        if catalog == CATALOG_MODEL_BASICS:
            return

        if any(
            tval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and not tval.get('assumption')
            for tval in ivalue.get('RelationT', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Computational Task',
                    page    = page_name,
                    message = 'Missing Assumption (Mathematical Model Specialization)'
                )
            )

        if any(
            tval['relation']['url'] in self._pairs(
                self.mathmoddb, 'specializes', 'specialized_by'
            )
            and 'not found' in tval.get('assumption', {}).values()
            for tval in ivalue.get('RelationT', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Computational Task',
                    page    = page_name,
                    message = 'Selected Formula not found in Section'
                )
            )

        if any(
            tval['relation']['url'] in self._pairs(
                self.mathmoddb, 'contains_task', 'contained_in_task'
            )
            and not tval.get('order')
            for tval in ivalue.get('RelationT', {}).values()
        ):
            self.err.append(
                self._error(
                    section = 'Computational Task',
                    page    = page_name,
                    message = 'Missing Order Number (Computational Task Containment)'
                )
            )

        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationQQK',
            from_class = 'Computational Task',
            to_class   = 'Quantity',
            optional   = False
        )

Workflow

Workflow Documentation check mixin.

WorkflowMixin

Checks for Interdisciplinary Workflow catalog entries.

Source code in MaRDMO/checks/workflow.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
class WorkflowMixin:
    '''Checks for Interdisciplinary Workflow catalog entries.'''

    # -------------------------------------------------------------------------
    # Workflow Documentation Checks
    # -------------------------------------------------------------------------

    def workflow(self, project, data):
        '''Check Interdisciplinary Workflow documentation completeness.

        Verifies mandatory Research Objective, Process Step link,
        Model/Task cross-dependency, and all five reproducibility fields.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/workflow')
        )
        for ikey, ivalue in data.get('workflow', {}).items():
            page_name = values.get(set_index=ikey).text

            if not ivalue.get('objective'):
                self.err.append(self._error(
                    'Interdisciplinary Workflow', page_name,
                    'Missing Research Objective'
                ))

            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationPS',
                from_class = 'Interdisciplinary Workflow',
                to_class   = 'Process Step'
            )
            self._check_flexible(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationWF',
                from_class = 'Interdisciplinary Workflow'
            )

            for pair in ivalue.get('model_task_pairs', []):
                if pair['has_model'] and not pair['has_tasks']:
                    self.err.append(self._error(
                        'Interdisciplinary Workflow', page_name,
                        'Missing Computational Task'
                    ))
                elif pair['has_tasks'] and not pair['has_model']:
                    self.err.append(self._error(
                        'Interdisciplinary Workflow', page_name,
                        'Missing Mathematical Model'
                    ))

            repro_fields = [
                ('mathematical',    'Mathematical Reproducibility'),
                ('runtime',         'Runtime Reproducibility'),
                ('result',          'Reproducibility of Results'),
                ('originalplatform', 'Reproducibility on Original Platform'),
                ('otherplatform',   'Reproducibility on Other Platform'),
            ]
            for key, label in repro_fields:
                if not ivalue.get(key):
                    self.err.append(self._error(
                        'Interdisciplinary Workflow', page_name,
                        f'Missing {label}'
                    ))

    def step(self, project, data):
        '''Check Process Step documentation completeness.

        Verifies mandatory Input/Output Data Sets, Academic Discipline, and
        that at least one complete path (Algorithm+Software+Hardware+SoftwareReq
        or Method+Instrument+MethodProtocol) is documented.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        options = get_options()
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/processstep')
        )
        for ikey, ivalue in data.get('processstep', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationIDS',
                from_class = 'Process Step',
                to_class   = 'Input Data Set'
            )
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationODS',
                from_class = 'Process Step',
                to_class   = 'Output Data Set'
            )
            if not ivalue.get('RFRelatant'):
                self.err.append(self._error(
                    'Process Step', page_name, 'Missing Academic Discipline'
                ))
            else:
                self._check_without_section_items(
                    items        = ivalue.get('RFRelatant', {}),
                    parent_page  = page_name,
                    parent_class = 'Process Step',
                    item_class   = 'Academic Discipline'
                )

            if ivalue.get('algorithm_software_pairs'):
                for pair in ivalue['algorithm_software_pairs']:
                    if not pair['has_primary']:
                        self.err.append(self._error(
                            'Process Step', page_name, 'Missing Algorithm'
                        ))
                    elif 'not found' in pair['primary']:
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Selected Algorithm not found in Algorithm Section'
                        ))
                    if not pair['has_qualifier']:
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Missing Software (Algorithm Path)'
                        ))
                    elif pair['qualifier'] == 'not found':
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Selected Software not found in Software Section'
                        ))
                    if not pair.get('hardware'):
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Missing Hardware (Algorithm Path)'
                        ))
                    elif pair['hardware'] == 'not found':
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Selected Hardware not found in Hardware Section'
                        ))
                    if not pair.get('software_doc'):
                        self.err.append(self._error(
                            'Process Step', page_name,
                            'Missing Software Requirements'
                        ))
                    else:
                        self._check_doc_entries(
                            pair['software_doc'], options, page_name,
                            'Software Requirements'
                        )

            if ivalue.get('method_instrument_pairs'):
                self._check_without_section_items(
                    items        = ivalue.get('MRelatant', {}),
                    parent_page  = page_name,
                    parent_class = 'Process Step',
                    item_class   = 'Method'
                )
                self._check_without_section_items(
                    items        = ivalue.get('IRelatant', {}),
                    parent_page  = page_name,
                    parent_class = 'Process Step',
                    item_class   = 'Instrument'
                )
                for pair in ivalue['method_instrument_pairs']:
                    if not pair['has_primary']:
                        self.err.append(self._error(
                            'Process Step', page_name, 'Missing Method'
                        ))
                    if not pair['has_qualifier']:
                        self.err.append(self._error(
                            'Process Step', page_name, 'Missing Instrument'
                        ))
                    if not pair.get('method_doc'):
                        self.err.append(self._error(
                            'Process Step', page_name, 'Missing Method Protocol'
                        ))
                    else:
                        self._check_doc_entries(
                            pair['method_doc'], options, page_name,
                            'Method Protocol'
                        )

    def workflow_algorithm(self, project, data):
        '''Check Algorithm documentation completeness for the workflow catalog.

        Verifies that each algorithm page has at least one Algorithmic Task
        and one Software link.  For user-defined tasks (ID == "not found"),
        also validates name and description.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/algorithm')
        )
        for ikey, ivalue in data.get('algorithm', {}).items():
            page_name = values.get(set_index=ikey).text
            if not ivalue.get('PRelatant'):
                self.err.append(self._error(
                    'Algorithm', page_name, 'Missing Algorithmic Task'
                ))
            else:
                self._check_without_section_items(
                    items        = ivalue.get('PRelatant', {}),
                    parent_page  = page_name,
                    parent_class = 'Algorithm',
                    item_class   = 'Algorithmic Task'
                )
            self._check_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationS',
                from_class = 'Algorithm',
                to_class   = 'Software'
            )

    def workflow_software(self, project, data):
        '''Check Software documentation completeness for the workflow catalog.

        Validates programming language inline items, optional software
        dependencies, and reference entries.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/software')
        )
        for ikey, ivalue in data.get('software', {}).items():
            page_name = values.get(set_index=ikey).text
            self._check_without_section_items(
                items        = ivalue.get('programminglanguage', {}),
                parent_page  = page_name,
                parent_class = 'Software',
                item_class   = 'Programming Language'
            )
            self._check_optional_static(
                data       = ivalue,
                page_name  = page_name,
                relation   = 'RelationS',
                from_class = 'Software',
                to_class   = 'Software'
            )
            if not ivalue.get('reference'):
                self.err.append(self._error('Software', page_name, 'Missing Reference'))
            else:
                ref = ivalue['reference']
                ref_list = [
                    (0, 'DOI', 'ID'),
                    (1, 'swMath ID', 'ID'),
                    (2, 'Description URL', 'URL'),
                    (3, 'Repository URL', 'URL')
                ]
                for idx, label, noun in ref_list:
                    if ref.get(idx) and not ref[idx][1]:
                        self.err.append(self._error(
                            'Software', page_name,
                            f'{label} selected, but no {noun} provided!'
                        ))
                    elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                        if not is_valid_url(ref[idx][1]):
                            self.err.append(self._error(
                                'Software', page_name,
                                f'Invalid {label}: must start with http:// or https://'
                            ))

    def hardware(self, project, data):
        '''Check Hardware documentation completeness.

        Verifies that each hardware page has a valid integer compute-node count,
        at least one CPU, valid name/description for user-defined CPUs, and
        integer occurrence and core counts per CPU entry.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/hardware')
        )
        for ikey, ivalue in data.get('hardware', {}).items():
            page_name = values.get(set_index=ikey).text

            nodes = ivalue.get('nodes')
            if not nodes:
                self.err.append(self._error(
                    'Hardware', page_name, 'Missing Number of Compute Nodes'
                ))
            elif not str(nodes).strip().isdigit():
                self.err.append(self._error(
                    'Hardware', page_name,
                    'Invalid Number of Compute Nodes (must be an integer)'
                ))

            cpu_entries = ivalue.get('cpu_entries', [])
            if not cpu_entries:
                self.err.append(self._error('Hardware', page_name, 'Missing CPU'))
            else:
                self._check_without_section_items(
                    items        = ivalue.get('cpu', {}),
                    parent_page  = page_name,
                    parent_class = 'Hardware',
                    item_class   = 'CPU'
                )
                for entry in cpu_entries:
                    if not entry.get('count'):
                        self.err.append(self._error(
                            'Hardware', page_name,
                            'Missing CPU Occurrence in Hardware'
                        ))
                    elif not entry.get('count_valid'):
                        self.err.append(self._error(
                            'Hardware', page_name,
                            'Invalid CPU Occurrence (must be an integer)'
                        ))
                    if not entry.get('cores'):
                        self.err.append(self._error(
                            'Hardware', page_name,
                            'Missing Number of Processor Cores'
                        ))
                    elif not entry.get('cores_valid'):
                        self.err.append(self._error(
                            'Hardware', page_name,
                            'Invalid Number of Processor Cores (must be an integer)'
                        ))

    def dataset(self, project, data):
        '''Check Data Set documentation completeness.

        Verifies mandatory fields: binary/text type, proprietary flag, file
        format, data size (option + valid number), publication statement, and
        archival statement (with a valid 4-digit year when Yes is selected).

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.
        '''
        options = get_options()
        values = project.values.filter(
            snapshot  = None,
            attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/dataset')
        )
        for ikey, ivalue in data.get('dataset', {}).items():
            page_name = values.get(set_index=ikey).text

            if not ivalue.get('BinaryText'):
                self.err.append(self._error('Data Set', page_name, 'Missing Data Set Type'))

            if not ivalue.get('Proprietary'):
                self.err.append(self._error(
                    'Data Set', page_name, 'Missing Data Set Proprietary'
                ))

            if not ivalue.get('FileFormat'):
                self.err.append(self._error('Data Set', page_name, 'Missing File Format'))

            size = ivalue.get('Size')
            if not size or not size[0]:
                self.err.append(self._error('Data Set', page_name, 'Missing Data Size'))
            elif not size[1]:
                self.err.append(self._error('Data Set', page_name, 'Missing Data Size Value'))
            else:
                val = str(size[1]).strip()
                if size[0] == options['items']:
                    if not val.isdigit():
                        self.err.append(self._error(
                            'Data Set', page_name,
                            'Invalid Data Size Value (must be an integer for items)'
                        ))
                else:
                    try:
                        float(val)
                    except (ValueError, TypeError):
                        self.err.append(self._error(
                            'Data Set', page_name,
                            'Invalid Data Size Value (must be a number)'
                        ))

            if not ivalue.get('ToPublish') or not ivalue['ToPublish'][0]:
                self.err.append(self._error(
                    'Data Set', page_name, 'Missing Data Set Publication Statement'
                ))
            elif (
                ivalue['ToPublish'][0] == options['YesText']
                and len(ivalue['ToPublish']) > 1
                and ivalue['ToPublish'][1]
                and not str(ivalue['ToPublish'][1]).startswith('10.')
                and not is_valid_url(ivalue['ToPublish'][1])
            ):
                self.err.append(self._error(
                    'Data Set', page_name,
                    'Invalid Publication URL: must start with http:// or https://'
                ))

            to_archive = ivalue.get('ToArchive')
            if not to_archive or not to_archive[0]:
                self.err.append(self._error(
                    'Data Set', page_name, 'Missing Data Set Archival Statement'
                ))
            elif to_archive[0] == options['YesText']:
                if not to_archive[1]:
                    self.err.append(self._error('Data Set', page_name, 'Missing Archival Year'))
                else:
                    val = str(to_archive[1]).strip()
                    if not (len(val) == 4 and val.isdigit()):
                        self.err.append(self._error(
                            'Data Set', page_name,
                            'Invalid Archival Year (must be a 4-digit year)'
                        ))

    # -------------------------------------------------------------------------
    # Run method
    # -------------------------------------------------------------------------

    def run_workflow(self, project, data, catalog=None):
        '''Run all workflow-catalog checks and return the collected error list.

        Executes, in order: ID/Name/Description, workflow, process step,
        algorithm, software, hardware, dataset, and publication checks.

        Args:
            project: RDMO project instance.
            data:    Top-level answers dict.

        Returns:
            List of human-readable error strings, or an empty list when all
            checks pass.
        '''
        self.id_name_description(project, data, CATALOG_WORKFLOW)
        self.workflow(project, data)
        self.step(project, data)
        self.workflow_algorithm(project, data)
        self.workflow_software(project, data)
        self.hardware(project, data)
        self.dataset(project, data)
        self.publication(project, data, CATALOG_WORKFLOW)
        return self._finalise()

dataset(project, data)

Check Data Set documentation completeness.

Verifies mandatory fields: binary/text type, proprietary flag, file format, data size (option + valid number), publication statement, and archival statement (with a valid 4-digit year when Yes is selected).

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def dataset(self, project, data):
    '''Check Data Set documentation completeness.

    Verifies mandatory fields: binary/text type, proprietary flag, file
    format, data size (option + valid number), publication statement, and
    archival statement (with a valid 4-digit year when Yes is selected).

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    options = get_options()
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/dataset')
    )
    for ikey, ivalue in data.get('dataset', {}).items():
        page_name = values.get(set_index=ikey).text

        if not ivalue.get('BinaryText'):
            self.err.append(self._error('Data Set', page_name, 'Missing Data Set Type'))

        if not ivalue.get('Proprietary'):
            self.err.append(self._error(
                'Data Set', page_name, 'Missing Data Set Proprietary'
            ))

        if not ivalue.get('FileFormat'):
            self.err.append(self._error('Data Set', page_name, 'Missing File Format'))

        size = ivalue.get('Size')
        if not size or not size[0]:
            self.err.append(self._error('Data Set', page_name, 'Missing Data Size'))
        elif not size[1]:
            self.err.append(self._error('Data Set', page_name, 'Missing Data Size Value'))
        else:
            val = str(size[1]).strip()
            if size[0] == options['items']:
                if not val.isdigit():
                    self.err.append(self._error(
                        'Data Set', page_name,
                        'Invalid Data Size Value (must be an integer for items)'
                    ))
            else:
                try:
                    float(val)
                except (ValueError, TypeError):
                    self.err.append(self._error(
                        'Data Set', page_name,
                        'Invalid Data Size Value (must be a number)'
                    ))

        if not ivalue.get('ToPublish') or not ivalue['ToPublish'][0]:
            self.err.append(self._error(
                'Data Set', page_name, 'Missing Data Set Publication Statement'
            ))
        elif (
            ivalue['ToPublish'][0] == options['YesText']
            and len(ivalue['ToPublish']) > 1
            and ivalue['ToPublish'][1]
            and not str(ivalue['ToPublish'][1]).startswith('10.')
            and not is_valid_url(ivalue['ToPublish'][1])
        ):
            self.err.append(self._error(
                'Data Set', page_name,
                'Invalid Publication URL: must start with http:// or https://'
            ))

        to_archive = ivalue.get('ToArchive')
        if not to_archive or not to_archive[0]:
            self.err.append(self._error(
                'Data Set', page_name, 'Missing Data Set Archival Statement'
            ))
        elif to_archive[0] == options['YesText']:
            if not to_archive[1]:
                self.err.append(self._error('Data Set', page_name, 'Missing Archival Year'))
            else:
                val = str(to_archive[1]).strip()
                if not (len(val) == 4 and val.isdigit()):
                    self.err.append(self._error(
                        'Data Set', page_name,
                        'Invalid Archival Year (must be a 4-digit year)'
                    ))

hardware(project, data)

Check Hardware documentation completeness.

Verifies that each hardware page has a valid integer compute-node count, at least one CPU, valid name/description for user-defined CPUs, and integer occurrence and core counts per CPU entry.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def hardware(self, project, data):
    '''Check Hardware documentation completeness.

    Verifies that each hardware page has a valid integer compute-node count,
    at least one CPU, valid name/description for user-defined CPUs, and
    integer occurrence and core counts per CPU entry.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/hardware')
    )
    for ikey, ivalue in data.get('hardware', {}).items():
        page_name = values.get(set_index=ikey).text

        nodes = ivalue.get('nodes')
        if not nodes:
            self.err.append(self._error(
                'Hardware', page_name, 'Missing Number of Compute Nodes'
            ))
        elif not str(nodes).strip().isdigit():
            self.err.append(self._error(
                'Hardware', page_name,
                'Invalid Number of Compute Nodes (must be an integer)'
            ))

        cpu_entries = ivalue.get('cpu_entries', [])
        if not cpu_entries:
            self.err.append(self._error('Hardware', page_name, 'Missing CPU'))
        else:
            self._check_without_section_items(
                items        = ivalue.get('cpu', {}),
                parent_page  = page_name,
                parent_class = 'Hardware',
                item_class   = 'CPU'
            )
            for entry in cpu_entries:
                if not entry.get('count'):
                    self.err.append(self._error(
                        'Hardware', page_name,
                        'Missing CPU Occurrence in Hardware'
                    ))
                elif not entry.get('count_valid'):
                    self.err.append(self._error(
                        'Hardware', page_name,
                        'Invalid CPU Occurrence (must be an integer)'
                    ))
                if not entry.get('cores'):
                    self.err.append(self._error(
                        'Hardware', page_name,
                        'Missing Number of Processor Cores'
                    ))
                elif not entry.get('cores_valid'):
                    self.err.append(self._error(
                        'Hardware', page_name,
                        'Invalid Number of Processor Cores (must be an integer)'
                    ))

run_workflow(project, data, catalog=None)

Run all workflow-catalog checks and return the collected error list.

Executes, in order: ID/Name/Description, workflow, process step, algorithm, software, hardware, dataset, and publication checks.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required

Returns:

Type Description

List of human-readable error strings, or an empty list when all

checks pass.

Source code in MaRDMO/checks/workflow.py
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def run_workflow(self, project, data, catalog=None):
    '''Run all workflow-catalog checks and return the collected error list.

    Executes, in order: ID/Name/Description, workflow, process step,
    algorithm, software, hardware, dataset, and publication checks.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.

    Returns:
        List of human-readable error strings, or an empty list when all
        checks pass.
    '''
    self.id_name_description(project, data, CATALOG_WORKFLOW)
    self.workflow(project, data)
    self.step(project, data)
    self.workflow_algorithm(project, data)
    self.workflow_software(project, data)
    self.hardware(project, data)
    self.dataset(project, data)
    self.publication(project, data, CATALOG_WORKFLOW)
    return self._finalise()

step(project, data)

Check Process Step documentation completeness.

Verifies mandatory Input/Output Data Sets, Academic Discipline, and that at least one complete path (Algorithm+Software+Hardware+SoftwareReq or Method+Instrument+MethodProtocol) is documented.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def step(self, project, data):
    '''Check Process Step documentation completeness.

    Verifies mandatory Input/Output Data Sets, Academic Discipline, and
    that at least one complete path (Algorithm+Software+Hardware+SoftwareReq
    or Method+Instrument+MethodProtocol) is documented.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    options = get_options()
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/processstep')
    )
    for ikey, ivalue in data.get('processstep', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationIDS',
            from_class = 'Process Step',
            to_class   = 'Input Data Set'
        )
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationODS',
            from_class = 'Process Step',
            to_class   = 'Output Data Set'
        )
        if not ivalue.get('RFRelatant'):
            self.err.append(self._error(
                'Process Step', page_name, 'Missing Academic Discipline'
            ))
        else:
            self._check_without_section_items(
                items        = ivalue.get('RFRelatant', {}),
                parent_page  = page_name,
                parent_class = 'Process Step',
                item_class   = 'Academic Discipline'
            )

        if ivalue.get('algorithm_software_pairs'):
            for pair in ivalue['algorithm_software_pairs']:
                if not pair['has_primary']:
                    self.err.append(self._error(
                        'Process Step', page_name, 'Missing Algorithm'
                    ))
                elif 'not found' in pair['primary']:
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Selected Algorithm not found in Algorithm Section'
                    ))
                if not pair['has_qualifier']:
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Missing Software (Algorithm Path)'
                    ))
                elif pair['qualifier'] == 'not found':
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Selected Software not found in Software Section'
                    ))
                if not pair.get('hardware'):
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Missing Hardware (Algorithm Path)'
                    ))
                elif pair['hardware'] == 'not found':
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Selected Hardware not found in Hardware Section'
                    ))
                if not pair.get('software_doc'):
                    self.err.append(self._error(
                        'Process Step', page_name,
                        'Missing Software Requirements'
                    ))
                else:
                    self._check_doc_entries(
                        pair['software_doc'], options, page_name,
                        'Software Requirements'
                    )

        if ivalue.get('method_instrument_pairs'):
            self._check_without_section_items(
                items        = ivalue.get('MRelatant', {}),
                parent_page  = page_name,
                parent_class = 'Process Step',
                item_class   = 'Method'
            )
            self._check_without_section_items(
                items        = ivalue.get('IRelatant', {}),
                parent_page  = page_name,
                parent_class = 'Process Step',
                item_class   = 'Instrument'
            )
            for pair in ivalue['method_instrument_pairs']:
                if not pair['has_primary']:
                    self.err.append(self._error(
                        'Process Step', page_name, 'Missing Method'
                    ))
                if not pair['has_qualifier']:
                    self.err.append(self._error(
                        'Process Step', page_name, 'Missing Instrument'
                    ))
                if not pair.get('method_doc'):
                    self.err.append(self._error(
                        'Process Step', page_name, 'Missing Method Protocol'
                    ))
                else:
                    self._check_doc_entries(
                        pair['method_doc'], options, page_name,
                        'Method Protocol'
                    )

workflow(project, data)

Check Interdisciplinary Workflow documentation completeness.

Verifies mandatory Research Objective, Process Step link, Model/Task cross-dependency, and all five reproducibility fields.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def workflow(self, project, data):
    '''Check Interdisciplinary Workflow documentation completeness.

    Verifies mandatory Research Objective, Process Step link,
    Model/Task cross-dependency, and all five reproducibility fields.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/workflow')
    )
    for ikey, ivalue in data.get('workflow', {}).items():
        page_name = values.get(set_index=ikey).text

        if not ivalue.get('objective'):
            self.err.append(self._error(
                'Interdisciplinary Workflow', page_name,
                'Missing Research Objective'
            ))

        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationPS',
            from_class = 'Interdisciplinary Workflow',
            to_class   = 'Process Step'
        )
        self._check_flexible(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationWF',
            from_class = 'Interdisciplinary Workflow'
        )

        for pair in ivalue.get('model_task_pairs', []):
            if pair['has_model'] and not pair['has_tasks']:
                self.err.append(self._error(
                    'Interdisciplinary Workflow', page_name,
                    'Missing Computational Task'
                ))
            elif pair['has_tasks'] and not pair['has_model']:
                self.err.append(self._error(
                    'Interdisciplinary Workflow', page_name,
                    'Missing Mathematical Model'
                ))

        repro_fields = [
            ('mathematical',    'Mathematical Reproducibility'),
            ('runtime',         'Runtime Reproducibility'),
            ('result',          'Reproducibility of Results'),
            ('originalplatform', 'Reproducibility on Original Platform'),
            ('otherplatform',   'Reproducibility on Other Platform'),
        ]
        for key, label in repro_fields:
            if not ivalue.get(key):
                self.err.append(self._error(
                    'Interdisciplinary Workflow', page_name,
                    f'Missing {label}'
                ))

workflow_algorithm(project, data)

Check Algorithm documentation completeness for the workflow catalog.

Verifies that each algorithm page has at least one Algorithmic Task and one Software link. For user-defined tasks (ID == "not found"), also validates name and description.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def workflow_algorithm(self, project, data):
    '''Check Algorithm documentation completeness for the workflow catalog.

    Verifies that each algorithm page has at least one Algorithmic Task
    and one Software link.  For user-defined tasks (ID == "not found"),
    also validates name and description.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/algorithm')
    )
    for ikey, ivalue in data.get('algorithm', {}).items():
        page_name = values.get(set_index=ikey).text
        if not ivalue.get('PRelatant'):
            self.err.append(self._error(
                'Algorithm', page_name, 'Missing Algorithmic Task'
            ))
        else:
            self._check_without_section_items(
                items        = ivalue.get('PRelatant', {}),
                parent_page  = page_name,
                parent_class = 'Algorithm',
                item_class   = 'Algorithmic Task'
            )
        self._check_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationS',
            from_class = 'Algorithm',
            to_class   = 'Software'
        )

workflow_software(project, data)

Check Software documentation completeness for the workflow catalog.

Validates programming language inline items, optional software dependencies, and reference entries.

Parameters:

Name Type Description Default
project

RDMO project instance.

required
data

Top-level answers dict.

required
Source code in MaRDMO/checks/workflow.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def workflow_software(self, project, data):
    '''Check Software documentation completeness for the workflow catalog.

    Validates programming language inline items, optional software
    dependencies, and reference entries.

    Args:
        project: RDMO project instance.
        data:    Top-level answers dict.
    '''
    values = project.values.filter(
        snapshot  = None,
        attribute = Attribute.objects.get(uri=f'{BASE_URI}domain/software')
    )
    for ikey, ivalue in data.get('software', {}).items():
        page_name = values.get(set_index=ikey).text
        self._check_without_section_items(
            items        = ivalue.get('programminglanguage', {}),
            parent_page  = page_name,
            parent_class = 'Software',
            item_class   = 'Programming Language'
        )
        self._check_optional_static(
            data       = ivalue,
            page_name  = page_name,
            relation   = 'RelationS',
            from_class = 'Software',
            to_class   = 'Software'
        )
        if not ivalue.get('reference'):
            self.err.append(self._error('Software', page_name, 'Missing Reference'))
        else:
            ref = ivalue['reference']
            ref_list = [
                (0, 'DOI', 'ID'),
                (1, 'swMath ID', 'ID'),
                (2, 'Description URL', 'URL'),
                (3, 'Repository URL', 'URL')
            ]
            for idx, label, noun in ref_list:
                if ref.get(idx) and not ref[idx][1]:
                    self.err.append(self._error(
                        'Software', page_name,
                        f'{label} selected, but no {noun} provided!'
                    ))
                elif noun == 'URL' and ref.get(idx) and ref[idx][1]:
                    if not is_valid_url(ref[idx][1]):
                        self.err.append(self._error(
                            'Software', page_name,
                            f'Invalid {label}: must start with http:// or https://'
                        ))