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();
|
this.changeObserver.changed();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changedHandler = () => {
|
||||||
|
this.changeObserver.changed();
|
||||||
|
}
|
||||||
|
|
||||||
const removedHandler = measurement => {
|
const removedHandler = measurement => {
|
||||||
const measurementNumber = measurement.measurementNumber;
|
const measurementNumber = measurement.measurementNumber;
|
||||||
|
|
||||||
@ -167,6 +171,7 @@ class MeasurementApi {
|
|||||||
|
|
||||||
collection.find().observe({
|
collection.find().observe({
|
||||||
added: addedHandler,
|
added: addedHandler,
|
||||||
|
changed: changedHandler,
|
||||||
removed: removedHandler
|
removed: removedHandler
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,31 +1,72 @@
|
|||||||
import { OHIF } from 'meteor/ohif:core';
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
import { _ } from 'meteor/underscore';
|
import { _ } from 'meteor/underscore';
|
||||||
import { CriteriaEvaluator } from './CriteriaEvaluator';
|
import { CriteriaEvaluator } from './CriteriaEvaluator';
|
||||||
import * as recistBaselineEvaluation from './evaluations/recistBaseline.json';
|
import * as evaluations from './evaluations';
|
||||||
|
|
||||||
class ConformanceCriteria {
|
class ConformanceCriteria {
|
||||||
|
|
||||||
constructor(measurementApi, timepointApi) {
|
constructor(measurementApi, timepointApi) {
|
||||||
this.measurementApi = measurementApi;
|
this.measurementApi = measurementApi;
|
||||||
this.timepointApi = timepointApi;
|
this.timepointApi = timepointApi;
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
this.warnings = {};
|
Tracker.autorun(() => {
|
||||||
|
const trialCriteriaType = TrialCriteriaTypes.findOne({ selected: true });
|
||||||
|
this.measurementApi.changeObserver.depend();
|
||||||
|
this.validate(trialCriteriaType);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validate() {
|
validate(trialCriteriaType) {
|
||||||
const data = this.getData();
|
const baselineData = this.getData('baseline');
|
||||||
this.validateRecist(data);
|
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) {
|
validateTimepoint(timepointId, trialCriteriaType, data) {
|
||||||
const recistBaselineEvaluator = new CriteriaEvaluator(recistBaselineEvaluation);
|
const evaluators = this.getEvaluators(timepointId, trialCriteriaType);
|
||||||
console.warn('>>>>check', recistBaselineEvaluator.evaluate(data));
|
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
|
* Build the data that will be used to do the conformance criteria checks
|
||||||
*/
|
*/
|
||||||
getData() {
|
getData(timepointType) {
|
||||||
const data = {
|
const data = {
|
||||||
targets: [],
|
targets: [],
|
||||||
nonTargets: []
|
nonTargets: []
|
||||||
@ -33,12 +74,17 @@ class ConformanceCriteria {
|
|||||||
|
|
||||||
const fillData = measurementType => {
|
const fillData = measurementType => {
|
||||||
const measurements = this.measurementApi.fetch(measurementType);
|
const measurements = this.measurementApi.fetch(measurementType);
|
||||||
|
|
||||||
measurements.forEach(measurement => {
|
measurements.forEach(measurement => {
|
||||||
const { studyInstanceUid, imageId } = measurement;
|
const { studyInstanceUid, imageId } = measurement;
|
||||||
const metadata = this.getImageMetadata(studyInstanceUid, imageId);
|
const metadata = this.getImageMetadata(studyInstanceUid, imageId);
|
||||||
const timepointId = measurement.timepointId;
|
const timepointId = measurement.timepointId;
|
||||||
const timepoint = this.timepointApi.timepoints.findOne({ timepointId });
|
const timepoint = this.timepointApi.timepoints.findOne({ timepointId });
|
||||||
|
|
||||||
|
if((timepointType !== 'both') && (timepoint.timepointType !== timepointType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
data[measurementType].push({
|
data[measurementType].push({
|
||||||
measurement,
|
measurement,
|
||||||
metadata,
|
metadata,
|
||||||
|
|||||||
@ -25,10 +25,6 @@ export class CriteriaEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCriteriaValidator() {
|
getCriteriaValidator() {
|
||||||
if (CriteriaEvaluator.validator) {
|
|
||||||
return CriteriaEvaluator.validator;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
properties: {},
|
properties: {},
|
||||||
definitions: {}
|
definitions: {}
|
||||||
@ -54,7 +50,7 @@ export class CriteriaEvaluator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return CriteriaEvaluator.validator = new Ajv().compile(schema);
|
return new Ajv().compile(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate(data) {
|
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 './MaxTargetsPerOrgan';
|
||||||
export * from './MaxTargets';
|
export * from './MaxTargets';
|
||||||
export * from './MeasurementsLength';
|
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