LT-92: Integrating RECIST evaluator and trial configuration
This commit is contained in:
parent
2cc313a035
commit
5366e1217d
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './Location';
|
||||
export * from './MaxTargetsPerOrgan';
|
||||
export * from './MaxTargets';
|
||||
export * from './MeasurementsLength';
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import * as recistEvaluation from './recist.json';
|
||||
|
||||
export const recist = recistEvaluation;
|
||||
@ -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": {}
|
||||
}
|
||||
@ -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": {}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user