Skip to content

Commit

Permalink
ADD: Add PyRadiomics Model schema validation
Browse files Browse the repository at this point in the history
Update the PyKwalify validation in PyRadiomics to also allow validation of PyRadiomics model files.

Add an example model file to explain the structure.

Update requirements.txt to enforce PyKwalify version != 1.6.0, as this version contains a bug that breaks the usage of partial schemas, which is utilized to allow validation of the two use cases (parameter file and model file)

Because partial schemas are used, load the schemas outside of pykwalify and pass them during validation as a dict.

Update the testParams script to allow testing of both use cases.
  • Loading branch information
JoostJM committed Jan 24, 2018
1 parent ed9e300 commit fb0f769
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 140 deletions.
53 changes: 41 additions & 12 deletions bin/testParams.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,54 @@
# custom parameters specified will be printed. If validation fails, an error message specifying cause of validation
# error will be printed.

import sys
import argparse

import pykwalify.core

from radiomics import getParameterValidationFiles

def main(paramsFile):
schemaFile, schemaFuncs = getParameterValidationFiles()

c = pykwalify.core.Core(source_file=paramsFile, schema_files=[schemaFile], extensions=[schemaFuncs])
def main(paramsFile, is_model=False):
if is_model:
validate_model_file(paramsFile)
else:
validate_customization(paramsFile)


def validate_model_file(model_file,):
schema_data, schemaFuncs = getParameterValidationFiles(is_model_validation=True)
c = pykwalify.core.Core(source_file=model_file, schema_data=schema_data, extensions=[schemaFuncs])

try:
params = c.validate()
print('Model validation successfull!\n\n'
'###Model Type###\n%s\n'
% (params['model']['name']))
except pykwalify.core.SchemaError as e:
print('Parameter validation failed!\n%s' % e.msg)


def validate_customization(parameter_file):
schema_data, schemaFuncs = getParameterValidationFiles()
c = pykwalify.core.Core(source_file=parameter_file, schema_data=schema_data, extensions=[schemaFuncs])

try:
params = c.validate()
print('Parameter validation successfull!\n\n'
'###Enabled Features###\n%s\n'
'###Enabled Image Types###\n%s\n'
'###Settings###\n%s' % (params['featureClass'], params['imageType'], params['setting']))
except Exception as e:
print('Parameter validation failed!\n%s' % e.message)
'###Enabled Features###\n%s\n'
'###Enabled Image Types###\n%s\n'
'###Settings###\n%s' % (params['featureClass'], params['imageType'], params['setting'])
)
except pykwalify.core.SchemaError as e:
print('Parameter validation failed!\n%s' % e.msg)


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('parameter_file', help='File representing the yaml or json structured configuration file to be '
'tested')
parser.add_argument('--model', '-m', action='store_true',
help='If this argument is specified, the configuration file is treated as a PyRadiomics Model, '
'otherwise, it is treated as an extraction parameter file')

if __name__ == '__main__' and len(sys.argv) > 1:
main(sys.argv[1])
args = parser.parse_args()
main(args.parameter_file, args.model)
41 changes: 41 additions & 0 deletions examples/exampleModels/exampleModel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This is an example of how a PyRadiomics Model looks like.
# It consists of 2 main parts: "extraction" and "model"
#
# "extraction": this part defines the settings PyRadiomics needs to extract the required features for the input.
# A model can incorporate features extracted from different images (e.g. multiple time points, or different MR
# sequences). For each image, a customized extraction may be defined by providing the image name as a key, and the
# customization as a value. This customization value must adhere to the same rules as a parameter file.
# In addition, extraction parameters that are common to all input images can be defined under "general". Be aware,
# "general" is therefore not allowed as an image name.
# If an image name is provided in the model, but not included in the extraction settings, no additional customization
# is applied for that image (just the general settings, if present)
#
# "model": this part provides all needed information to build the model. In this case, a simple linear regression model
# is shown. Both the "name" key (identifying the model type) and "parameters" key (providing model specific parameters)
# are required. What kind of parameters are possible/required depends on the model. In this case, only the intercept of
# the model and the betas (slopes) of the included features are required.

extraction:
general:
setting:
binWidth: 25
imageType:
Original: {}

image1:
featureClass:
glcm:
- Dissimilarity

image2:
featureClass:
firstorder:
- Mean

model:
name: linearregression
parameters:
intercept: 0.334
betas:
image1_original_glcm_Dissimilarity: 0.1
image2_original_firstorder_Mean: 0.3
28 changes: 24 additions & 4 deletions radiomics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import tempfile

import numpy # noqa: F401
from pykwalify.compat import yaml
from six.moves import urllib

from . import imageoperations
Expand Down Expand Up @@ -229,17 +230,36 @@ def getTestCase(testCase, repoDirectory=None):
return imageFile, maskFile


def getParameterValidationFiles():
def getParameterValidationFiles(is_model_validation=False):
"""
Returns file locations for the parameter schema and custom validation functions, which are needed when validating
a parameter file using ``PyKwalify.core``.
This functions returns a tuple with the file location of the schema as first and python script with custom validation
functions as second element.
This functions returns a tuple with a dictionary representing the schema and the file location of a python script
containing the custom validation functions.
"""
dataDir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'schemas'))
schemaFile = os.path.join(dataDir, 'paramSchema.yaml')
modelFile = os.path.join(dataDir, 'modelSchema.yaml')
schemaFuncs = os.path.join(dataDir, 'schemaFuncs.py')
return schemaFile, schemaFuncs

if not (os.path.isfile(schemaFile) and os.path.isfile(schemaFuncs)):
raise IOError('Customization Validation Files not Found!')

with open(schemaFile) as schema:
schema_data = yaml.load(schema)

if is_model_validation:
if not os.path.isfile(modelFile):
raise IOError('Model Validation File not Found!')

# Add the additional validation requirements of the model schema
with open(modelFile) as model_schema:
schema_data.update(yaml.load(model_schema))
else:
# Add the include to ensure that the customization_schema is applied
schema_data.update({'include': 'customization_schema'})

return schema_data, schemaFuncs


class _DummyProgressReporter(object):
Expand Down
4 changes: 2 additions & 2 deletions radiomics/featureextractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ def _applyParams(self, paramsFile=None, paramsDict=None):
# No handler available for either pykwalify or root logger, provide first radiomics handler (outputs to stderr)
pykwalify.core.log.addHandler(logging.getLogger('radiomics').handlers[0])

schemaFile, schemaFuncs = getParameterValidationFiles()
schema_data, schemaFuncs = getParameterValidationFiles()
c = pykwalify.core.Core(source_file=paramsFile, source_data=paramsDict,
schema_files=[schemaFile], extensions=[schemaFuncs])
schema_data=schema_data, extensions=[schemaFuncs])
params = c.validate()
self.logger.debug('Parameters parsed, input is valid.')

Expand Down
22 changes: 22 additions & 0 deletions radiomics/schemas/modelSchema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# include: customization_schema
name: model_schema
type: map
mapping:
extraction:
type: map
mapping:
regex;(.+):
include: customization_schema
model:
type: map
required: true
mapping:
name:
type: str
required: true
parameters:
type: map
required: true
mapping:
regex;(.+):
type: any
Loading

0 comments on commit fb0f769

Please sign in to comment.