Skip to content

Commit e3f1fc2

Browse files
authored
Merge pull request #55 from cdisc-org/sdtm-define-xml
Updates to create_define_json programm
2 parents 367de93 + 7b5c1fb commit e3f1fc2

2 files changed

Lines changed: 166 additions & 9 deletions

File tree

src/define-xml/create_define_json.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,37 @@ def update_datasets_dict(self):
18001800
# Add TS dataset
18011801
if "TS" not in self.datasets_dict:
18021802
self.datasets_dict["TS"] = {}
1803+
1804+
# Add TSPARMCD values extracted from vlm_lookup["TSPARMCD"] WhereClause Values
1805+
tsparmcd_values = set()
1806+
for entry in self.vlm_lookup.get("TSPARMCD", []):
1807+
for where_clause in entry.get("WhereClause", []):
1808+
for clause in where_clause.get("Clause", []):
1809+
for value in clause.get("Values", []):
1810+
if value:
1811+
tsparmcd_values.add(value)
1812+
1813+
if tsparmcd_values:
1814+
self.datasets_dict["TS"]["TSPARMCD"] = {"codelist": {
1815+
"C66738": sorted(tsparmcd_values)
1816+
}}
1817+
1818+
# Add TSPARM values by matching conceptIds from filtered TSPARMCD terms against C67152
1819+
kept_concept_ids = {term.get('conceptId') for term in [
1820+
t for t in self.client.get_api_json(f"/mdr/ct/packages/sdtmct-{self.sdtmct}/codelists/C66738")['terms']
1821+
if not tsparmcd_values or t.get('submissionValue') in tsparmcd_values
1822+
]}
1823+
1824+
tsparm_values = {
1825+
term.get('submissionValue')
1826+
for term in self.client.get_api_json(f"/mdr/ct/packages/sdtmct-{self.sdtmct}/codelists/C67152")['terms']
1827+
if term.get('conceptId') in kept_concept_ids and term.get('submissionValue')
1828+
}
1829+
1830+
if tsparm_values:
1831+
self.datasets_dict["TS"]["TSPARM"] = {"codelist": {
1832+
"C67152": sorted(tsparm_values)
1833+
}}
18031834

18041835
# Add TA dataset from arms
18051836
if self.studyDesignData.get('arms', []):
@@ -2060,7 +2091,7 @@ def update_datasets_dict(self):
20602091
"OID": f"CL.{epoch_codelist_name}",
20612092
"name": "Epoch",
20622093
"dataType": "text",
2063-
"standard": "ST.SDTMCT",
2094+
"standard": "STD.SDTMCT",
20642095
"codeListItems": epoch_terms,
20652096
}
20662097

@@ -2106,8 +2137,13 @@ def _process_standard_dataset(self, dataset, dataset_data):
21062137
"purpose": "Tabulation",
21072138
"structure": dataset_data['datasetStructure'],
21082139
"isReferenceData": (dataset_data.get('_links', {}).get('parentClass', {}).get('title') == "Trial Design") or not any(v.get('name') in ("USUBJID", "POOLID") for v in all_vars),
2109-
# Here are we always sure to use ST.SDTMIG?
2110-
"standard": "ST.SDTMIG",
2140+
"keySequence": ["__PLACEHOLDER__"],
2141+
# Here are we always sure to use STD.SDTMIG?
2142+
"standard": "STD.SDTMIG",
2143+
"observationClass": {
2144+
"name": dataset_data.get("_links", {}).get("parentClass", {}).get("title", "").upper()
2145+
},
2146+
# Here add future code for subClass in ADaM if needed
21112147
"items": []
21122148
}
21132149

@@ -2131,6 +2167,8 @@ def _process_standard_dataset(self, dataset, dataset_data):
21312167

21322168
if 'length' in var_data:
21332169
item_dict['length'] = var_data['length']
2170+
else:
2171+
item_dict['length'] = None
21342172

21352173
if 'format' in var_data:
21362174
item_dict['displayFormat'] = var_data['format']
@@ -2143,6 +2181,11 @@ def _process_standard_dataset(self, dataset, dataset_data):
21432181
"type": var_data['originType'],
21442182
"source": var_data['originSource']
21452183
}
2184+
else:
2185+
item_dict['origin'] = {
2186+
"type": "__PLACEHOLDER__",
2187+
"source": "__PLACEHOLDER__"
2188+
}
21462189

21472190
# Add codelist references for TA variables
21482191
if dataset == "TA":
@@ -2281,6 +2324,7 @@ def _process_standard_dataset(self, dataset, dataset_data):
22812324
"OID": f"VL.{dataset}.{var_name}",
22822325
"name": f"VL_{dataset}_{var_name}",
22832326
"type": "ValueList",
2327+
"wasDerivedFrom": f"IT.{dataset}.{var_name}",
22842328
"items": self.vlm_items_by_variable[vlm_key]
22852329
}
22862330
slices.append(slice_dict)
@@ -2772,6 +2816,7 @@ def process(self):
27722816
self.process_biomedical_concepts()
27732817
self.build_vlm_lookup()
27742818
self.update_datasets_dict()
2819+
self._build_global_codelist_terms()
27752820
self.populate_study_elements()
27762821
self.process_datasets()
27772822
self.add_standards()
@@ -2780,6 +2825,27 @@ def process(self):
27802825
if self.debug:
27812826
self.save_debug_files()
27822827

2828+
def _build_global_codelist_terms(self):
2829+
"""
2830+
Pre-scan datasets_dict to build a union of all explicitly restricted
2831+
terms per codelist C-Code. Used to decide placeholder vs. real terms.
2832+
2833+
For each codelist C-Code encountered across all datasets/variables:
2834+
- Non-empty list: contributes those values to the union
2835+
- Empty list: contributes nothing (means "unrestricted" for that variable)
2836+
2837+
Result stored in self.global_codelist_terms = {codelist_id: set_of_values}.
2838+
A codelist with an empty set means no dataset ever restricted it → placeholder.
2839+
"""
2840+
self.global_codelist_terms = {}
2841+
for variables in self.datasets_dict.values():
2842+
for var_data in variables.values():
2843+
for codelist_id, terms in var_data.get('codelist', {}).items():
2844+
if codelist_id not in self.global_codelist_terms:
2845+
self.global_codelist_terms[codelist_id] = set()
2846+
for term in terms:
2847+
self.global_codelist_terms[codelist_id].add(term)
2848+
27832849
def _get_or_create_condition_from_vlm(self, where_clause_data, dataset, variable_name):
27842850
"""
27852851
Get existing or create new conditions from VLM where clause data.
@@ -2932,7 +2998,15 @@ def create_term_dict(term):
29322998
restriction_list = restriction_codes[codelist_id]
29332999

29343000
if not restriction_list:
2935-
final_terms = [create_term_dict(term) for term in all_terms]
3001+
global_terms = self.global_codelist_terms.get(codelist_id, set())
3002+
if global_terms:
3003+
final_terms = [
3004+
create_term_dict(term)
3005+
for term in all_terms
3006+
if term.get("submissionValue") in global_terms
3007+
]
3008+
else:
3009+
final_terms = [{"NCI Term Code": None, "Term": "__PLACEHOLDER__", "Decoded Value": ["__PLACEHOLDER__"]}]
29363010
else:
29373011
# Find matching terms from standard codelist
29383012
final_terms = [
@@ -2962,7 +3036,15 @@ def create_term_dict(term):
29623036
]
29633037

29643038
else:
2965-
final_terms = [create_term_dict(term) for term in all_terms]
3039+
global_terms = self.global_codelist_terms.get(codelist_id, set())
3040+
if global_terms:
3041+
final_terms = [
3042+
create_term_dict(term)
3043+
for term in all_terms
3044+
if term.get("submissionValue") in global_terms
3045+
]
3046+
else:
3047+
final_terms = [{"NCI Term Code": None, "Term": "__PLACEHOLDER__", "Decoded Value": ["__PLACEHOLDER__"]}]
29663048

29673049
codelist_entry = {
29683050
"NCI Codelist Code": codelist_data.get("conceptId"),
@@ -2990,7 +3072,7 @@ def create_term_dict(term):
29903072
"OID": oid,
29913073
"name": name,
29923074
"dataType": "text",
2993-
"standard": "ST.SDTMCT",
3075+
"standard": "STD.SDTMCT",
29943076
}
29953077

29963078
# Add coding if NCI Codelist Code exists
@@ -3038,14 +3120,14 @@ def add_standards(self):
30383120
"""
30393121
self.template["standards"] = [
30403122
{
3041-
"OID": "ST.SDTMIG",
3123+
"OID": "STD.SDTMIG",
30423124
"name": "SDTMIG",
30433125
"type": "IG",
30443126
"version": self.sdtmig,
30453127
"status": "FINAL"
30463128
},
30473129
{
3048-
"OID": "ST.SDTMCT",
3130+
"OID": "STD.SDTMCT",
30493131
"name": "CDISC/NCI",
30503132
"type": "CT",
30513133
"version": self.sdtmct,

src/define-xml/define.yaml

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,52 @@ classes:
634634
description: >-
635635
Used to indicate that this ItemGroup has no data, e.g. for a manifest.
636636
range: boolean
637+
observationClass:
638+
description: >-
639+
Identifies the predefined CDISC model Class.
640+
range: DefClass
641+
required: false
642+
643+
DefClass:
644+
description: >-
645+
The predefined CDISC model Class that applies to an ItemGroupDef.
646+
attributes:
647+
name:
648+
description: >-
649+
Name of the General Observation Class following CDISC Controlled Terminology.
650+
range: string
651+
required: true
652+
subClasses:
653+
description: >-
654+
One or more SubClasses that further identify the specific SubClass within a Class.
655+
range: SubClass
656+
multivalued: true
657+
required: false
658+
inlined: true
659+
inlined_as_list: true
660+
661+
SubClass:
662+
description: >-
663+
A specific SubClass within a CDISC model Class.
664+
attributes:
665+
name:
666+
description: >-
667+
Name of the SubClass following CDISC Controlled Terminology for SubClass.
668+
range: string
669+
required: true
670+
parentClass:
671+
description: >-
672+
Name of the parent Class or SubClass following CDISC Controlled Terminology.
673+
range: string
674+
required: false
675+
subClasses:
676+
description: >-
677+
Nested SubClass(es) for multi-level SubClass hierarchy.
678+
range: SubClass
679+
multivalued: true
680+
required: false
681+
inlined: true
682+
inlined_as_list: true
637683

638684
Relationship:
639685
description: >-
@@ -1848,7 +1894,8 @@ classes:
18481894
# DPROD to complement Dataset class and link to DCAT ontology
18491895
DataProduct:
18501896
description: >-
1851-
A governed collection that represents a purpose-driven assembly of datasets and services with an owning team and lifecycle
1897+
A governed collection that represents a purpose-driven assembly of datasets and services with an owning team and lifecycle.
1898+
The DataProduct defines the boundary of accountability between data producers and consumers.
18521899
exact_mappings:
18531900
- dprod:DataProduct
18541901
- dcat:DataService
@@ -1881,6 +1928,26 @@ classes:
18811928
multivalued: true
18821929
inlined: true
18831930
inlined_as_list: true
1931+
inputDataflow:
1932+
description: >-
1933+
Description of the input interface before concrete Datasets exist.
1934+
Dataflows referenced here represent the demand side of a ProvisionAgreement.
1935+
range: Dataflow
1936+
multivalued: true
1937+
inlined: true
1938+
inlined_as_list: true
1939+
close_mappings:
1940+
- dcat:distribution
1941+
outputDataflow:
1942+
description: >-
1943+
Description of the output interface before concrete Datasets exist.
1944+
Dataflows referenced here represent the supply side of a ProvisionAgreement.
1945+
range: Dataflow
1946+
multivalued: true
1947+
inlined: true
1948+
inlined_as_list: true
1949+
close_mappings:
1950+
- dcat:distribution
18841951
inputDataset:
18851952
description: Source datasets used by the data product
18861953
range: Dataset
@@ -1891,6 +1958,7 @@ classes:
18911958
description: Output datasets produced by the data product
18921959
range: Dataset
18931960
multivalued: true
1961+
inlined: true
18941962
inlined_as_list: true
18951963
hasPolicy:
18961964
description: Policies governing the use and access of the data product
@@ -1975,6 +2043,13 @@ classes:
19752043
description: >-
19762044
The Data Provider that is part of this agreement
19772045
range: DataProvider
2046+
consumer:
2047+
description: >-
2048+
The Data Consumer that is part of this agreement
2049+
any_of:
2050+
- range: DataProduct
2051+
- range: Organization
2052+
- range: string
19782053
dataFlow:
19792054
description: >-
19802055
The Dataflow that is covered by this agreement

0 commit comments

Comments
 (0)