LT-92: Integrating RECIST evaluator and trial configuration

This commit is contained in:
Leonardo Campos 2017-01-16 12:33:59 -02:00 committed by Bruno Alves de Faria
parent 2cc313a035
commit 5366e1217d
8 changed files with 136 additions and 43 deletions

View File

@ -109,6 +109,10 @@ class MeasurementApi {
this.changeObserver.changed();
};
const changedHandler = () => {
this.changeObserver.changed();
}
const removedHandler = measurement => {
const measurementNumber = measurement.measurementNumber;
@ -167,6 +171,7 @@ class MeasurementApi {
collection.find().observe({
added: addedHandler,
changed: changedHandler,
removed: removedHandler
});
});

View File

@ -1,31 +1,72 @@
import { OHIF } from 'meteor/ohif:core';
import { _ } from 'meteor/underscore';
import { CriteriaEvaluator } from './CriteriaEvaluator';
import * as recistBaselineEvaluation from './evaluations/recistBaseline.json';
import * as evaluations from './evaluations';
class ConformanceCriteria {
constructor(measurementApi, timepointApi) {
this.measurementApi = measurementApi;
this.timepointApi = timepointApi;
this.results = [];
this.warnings = {};
Tracker.autorun(() => {
const trialCriteriaType = TrialCriteriaTypes.findOne({ selected: true });
this.measurementApi.changeObserver.depend();
this.validate(trialCriteriaType);
});
}
validate() {
const data = this.getData();
this.validateRecist(data);
validate(trialCriteriaType) {
const baselineData = this.getData('baseline');
const followupData = this.getData('followup');
const mergedData = baselineData;
mergedData.targets = mergedData.targets.concat(followupData.targets);
mergedData.nonTargets = mergedData.nonTargets.concat(followupData.nonTargets);
const resultBaseline = this.validateTimepoint('baseline', trialCriteriaType, baselineData);
const resultFollowup = this.validateTimepoint('followup', trialCriteriaType, followupData);
const resultBoth = this.validateTimepoint('both', trialCriteriaType, mergedData);
const results = resultBaseline.concat(resultFollowup).concat(resultBoth);
console.warn('>>>> validate', results);
return results;
}
validateRecist(data) {
const recistBaselineEvaluator = new CriteriaEvaluator(recistBaselineEvaluation);
console.warn('>>>>check', recistBaselineEvaluator.evaluate(data));
validateTimepoint(timepointId, trialCriteriaType, data) {
const evaluators = this.getEvaluators(timepointId, trialCriteriaType);
let results = [];
evaluators.forEach(evaluator => {
const result = evaluator.evaluate(data);
results = results.concat(result);
});
return results;
}
getEvaluators(timepointId, trialCriteriaType) {
const evaluators = [];
const trialCriteriaTypeId = trialCriteriaType.id.toLowerCase();
const evaluation = evaluations[trialCriteriaTypeId]
if(evaluation) {
const evaluationTimepoint = evaluation[timepointId];
if(evaluationTimepoint) {
evaluators.push(new CriteriaEvaluator(evaluationTimepoint));
}
}
return evaluators;
}
/*
* Build the data that will be used to do the conformance criteria checks
*/
getData() {
getData(timepointType) {
const data = {
targets: [],
nonTargets: []
@ -33,12 +74,17 @@ class ConformanceCriteria {
const fillData = measurementType => {
const measurements = this.measurementApi.fetch(measurementType);
measurements.forEach(measurement => {
const { studyInstanceUid, imageId } = measurement;
const metadata = this.getImageMetadata(studyInstanceUid, imageId);
const timepointId = measurement.timepointId;
const timepoint = this.timepointApi.timepoints.findOne({ timepointId });
if((timepointType !== 'both') && (timepoint.timepointType !== timepointType)) {
return;
}
data[measurementType].push({
measurement,
metadata,

View File

@ -25,10 +25,6 @@ export class CriteriaEvaluator {
}
getCriteriaValidator() {
if (CriteriaEvaluator.validator) {
return CriteriaEvaluator.validator;
}
const schema = {
properties: {},
definitions: {}
@ -54,7 +50,7 @@ export class CriteriaEvaluator {
}
});
return CriteriaEvaluator.validator = new Ajv().compile(schema);
return new Ajv().compile(schema);
}
evaluate(data) {

View File

@ -0,0 +1,36 @@
import { BaseCriterion } from './BaseCriterion';
export const LocationSchema = {
type: 'object'
};
/* LocationCriterion
* Check if the there are non-target measurements with response different than "present" on baseline
*/
export class LocationCriterion extends BaseCriterion {
constructor(options) {
super(options);
}
evaluate(data) {
const items = data.targets.concat(data.nonTargets);
const measurements = [];
let message;
items.forEach(item => {
const measurement = item.measurement;
if (!measurement.location) {
measurements.push(measurement);
}
});
if (measurements.length) {
message = 'All measurements should have a location';
}
return this.generateResponse(message, measurements);
}
}

View File

@ -1,3 +1,4 @@
export * from './Location';
export * from './MaxTargetsPerOrgan';
export * from './MaxTargets';
export * from './MeasurementsLength';

View File

@ -0,0 +1,3 @@
import * as recistEvaluation from './recist.json';
export const recist = recistEvaluation;

View File

@ -0,0 +1,35 @@
{
"both": {
"Location": {},
"MaxTargetsPerOrgan": {
"limit": 2
},
"MaxTargets": {
"limit": 5
},
"MeasurementsLength": [{
"longAxis": 10,
"longAxisSliceThicknessMultiplier": 2,
"modalityIn": ["CT", "MR"],
"locationNotIn": ["Lymph Node"],
"message": "Extranodal lesions must be >= 10mm long axis AND >= double the acquisition slice thickness by CT and MR"
}, {
"shortAxis": 20,
"longAxis": 20,
"modalityIn": ["PX", "XA"],
"locationNotIn": ["Lymph Node"],
"message": "Extranodal lesions must be >= 20mm on chest x-ray (although x-rays rarely used for clinical trial assessment)"
}, {
"shortAxis": 15,
"shortAxisSliceThicknessMultiplier": 2,
"modalityIn": ["CT", "MR"],
"locationIn": ["Lymph Node"],
"message": "Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR"
}]
},
"baseline": {
"NonTargetResponse": {},
"TargetType": {}
},
"followup": {}
}

View File

@ -1,29 +0,0 @@
{
"MaxTargetsPerOrgan": {
"limit": 2
},
"MaxTargets": {
"limit": 5
},
"MeasurementsLength": [{
"longAxis": 10,
"longAxisSliceThicknessMultiplier": 2,
"modalityIn": ["CT", "MR"],
"locationNotIn": ["Lymph Node"],
"message": "Extranodal lesions must be >= 10mm long axis AND >= double the acquisition slice thickness by CT and MR"
}, {
"shortAxis": 20,
"longAxis": 20,
"modalityIn": ["PX", "XA"],
"locationNotIn": ["Lymph Node"],
"message": "Extranodal lesions must be >= 20mm on chest x-ray (although x-rays rarely used for clinical trial assessment)"
}, {
"shortAxis": 15,
"shortAxisSliceThicknessMultiplier": 2,
"modalityIn": ["CT", "MR"],
"locationIn": ["Lymph Node"],
"message": "Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR"
}],
"NonTargetResponse": {},
"TargetType": {}
}