test-patient-generator 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,15 +1,13 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem 'hqmf-parser', '~> 1.0.6'
4
- gem 'hquery-patient-api', '~> 0.3.0'
5
- gem 'hqmf2js', '~> 1.0.1'
6
- gem 'health-data-standards', '~> 2.1.4'
3
+ gemspec
7
4
 
8
5
  gem 'rake'
9
- gem 'pry'
10
- gem 'pry-nav'
11
- gem 'bson_ext'
12
6
 
13
7
  group :test do
14
8
  gem 'simplecov'
15
- end
9
+ gem 'turn'
10
+ gem 'pry'
11
+ gem 'pry-nav'
12
+ gem 'pry-stack_explorer'
13
+ end
@@ -1,16 +1,11 @@
1
1
  require 'hqmf-parser'
2
2
  require 'health-data-standards'
3
+ require 'qrda_generator'
3
4
 
4
5
  require_relative 'tpg/ext/coded'
5
- require_relative 'tpg/ext/conjunction'
6
6
  require_relative 'tpg/ext/data_criteria'
7
- require_relative 'tpg/ext/derivation_operator'
8
- require_relative 'tpg/ext/population_criteria'
9
- require_relative 'tpg/ext/precondition'
10
7
  require_relative 'tpg/ext/range'
11
8
  require_relative 'tpg/ext/record'
12
- require_relative 'tpg/ext/subset_operator'
13
- require_relative 'tpg/ext/temporal_reference'
14
9
  require_relative 'tpg/ext/value'
15
10
 
16
11
  require_relative 'tpg/generation/generator'
@@ -15,6 +15,11 @@ module HQMF
15
15
  code_sets
16
16
  end
17
17
 
18
+ # Filter through a list of value sets and choose only the ones marked with a given OID.
19
+ #
20
+ # @param [String] oid The OID being used for filtering.
21
+ # @param [Array] value_sets A pool of available value sets
22
+ # @return The value set from the list with the requested OID.
18
23
  def self.select_value_sets(oid, value_sets)
19
24
  # Pick the value set for this DataCriteria. If it can't be found, it is an error from the value set source. We'll add the entry without codes for now.
20
25
  index = value_sets.index{|value_set| value_set["oid"] == oid}
@@ -30,10 +35,11 @@ module HQMF
30
35
  # @return A Hash including a code and code system containing one randomly selected code.
31
36
  def self.select_code(oid, value_sets)
32
37
  codes = select_codes(oid, value_sets)
33
- codeSystem = codes.keys()[0]
38
+ code_system = codes.keys()[0]
39
+ return nil if code_system.nil?
34
40
  {
35
- 'codeSystem' => codeSystem,
36
- 'code' => codes[codeSystem][0]
41
+ 'code_system' => code_system,
42
+ 'code' => codes[code_system][0]
37
43
  }
38
44
  end
39
45
  end
@@ -1,163 +1,171 @@
1
1
  module HQMF
2
2
  class DataCriteria
3
- attr_accessor :generation_range, :values
4
-
5
- # Generate all acceptable ranges of values and times for this data criteria. These ranges will then be updated by the permutate function
6
- # and passed to modify_patient to actually augment the base_patients Records.
7
- #
8
- # @param [Array] base_patients The list of patients who will be augmented by this data criteria.
9
- # @return The updated list of patients. The array will be modified by reference already so this is just for potential convenience.
10
- def generate(base_patients)
11
- acceptable_times = []
12
-
13
- # Evaluate all of the temporal restrictions on this data criteria.
14
- unless temporal_references.nil?
15
- # Generate for patients based on each reference and merge the potential times together.
16
- temporal_references.each do |reference|
17
- acceptable_time = reference.generate(base_patients)
18
- acceptable_times = DerivationOperator.intersection(acceptable_time, acceptable_times)
19
- end
20
- end
21
-
22
- # Apply any subset operators (e.g. FIRST)
23
- # e.g., if the subset operator is THIRD we need to make at least three entries
24
- unless subset_operators.nil?
25
- subset_operators.each do |subset_operator|
26
- subset_operator.generate(base_patients)
27
- end
28
- end
29
-
30
- # Apply any derivation operator (e.g. UNION)
31
- unless derivation_operator.nil?
32
- Range.merge(DerivationOperator.generate(base_patients, children_criteria, derivation_operator), acceptable_times)
3
+ attr_accessor :values
4
+
5
+ # Modify a Record with this data criteria.
6
+ #
7
+ # @param [Record] patient The Record that is being modified.
8
+ # @param [Range] time The period of time during which the data criteria happens.
9
+ # @param [Hash] value_sets The value sets that this data criteria references.
10
+ # @return The modified patient.
11
+ def modify_patient(patient, time, value_sets)
12
+ # Modify the patient with a characteristic if this data criteria defines one
13
+ modify_patient_with_characteristic(patient, time, value_sets)
14
+
15
+ # Otherwise we're dealing with a data criteria that describes a coded entry, so we create it and add it to the patient
16
+ entry = derive_entry(time, value_sets)
17
+ modify_entry_with_values(entry, value_sets)
18
+ modify_entry_with_negation(entry, value_sets)
19
+ modify_entry_with_fields(entry, value_sets)
20
+ modify_patient_with_entry(patient, entry)
21
+ end
22
+
23
+ private
24
+
25
+ # Modify a Record with a data criteria that describes a patient characteristic.
26
+ #
27
+ # @param [Record] patient The Record that is being modified.
28
+ # @param [Range] time The period of time during which the data criteria happens.
29
+ # @param [Hash] value_sets The value sets that this data criteria references.
30
+ # @return The modified patient.
31
+ def modify_patient_with_characteristic(patient, time, value_sets)
32
+ return nil unless characteristic?
33
+
34
+ if property == :birthtime
35
+ patient.birthdate = time.low.to_seconds
36
+ elsif property == :gender
37
+ gender = value.code
38
+ patient.gender = gender
39
+ patient.first = Randomizer.randomize_first_name(gender)
40
+ elsif property == :clinicalTrialParticipant
41
+ patient.clinicalTrialParticipant = true
42
+ elsif property == :expired
43
+ patient.expired = true
44
+ patient.deathdate = time.high.to_seconds
33
45
  end
34
-
35
- # Set the acceptable ranges for this data criteria so any parents can read it
36
- @generation_range = acceptable_times
46
+ end
37
47
 
38
- # Calculate value information
39
- acceptable_values = []
40
- acceptable_values << value
41
-
42
- # Walk through all acceptable time/value combinations and alter out patients
43
- base_patients.each do |patient|
44
- acceptable_times.each do |time|
45
- acceptable_values.each do |value|
46
- modify_patient(patient, time, Generator.value_sets)
47
- end
48
+ # Determine the apporpriate coded entry type from this data criteria and create one to match.
49
+ #
50
+ # @param [Range] time The period of time during which the entry happens.
51
+ # @param [Hash] value_sets The value sets that this data criteria references.
52
+ # @return A coded entry with basic data defined by this data criteria.
53
+ def derive_entry(time, value_sets)
54
+ return nil if characteristic?
55
+
56
+ entry_type = Generator.classify_entry(patient_api_function)
57
+ entry = entry_type.classify.constantize.new
58
+ entry.description = "#{description} (Code List: #{code_list_id})"
59
+ entry.start_time = time.low.to_seconds if time.low
60
+ entry.end_time = time.high.to_seconds if time.high
61
+ entry.status = status
62
+ entry.codes = Coded.select_codes(code_list_id, value_sets)
63
+ entry.oid = HQMF::DataCriteria.template_id_for_definition(definition, status, negation)
64
+ entry
65
+ end
66
+
67
+ # Add any value related data to a coded entry from this data criteria.
68
+ #
69
+ # @param [Entry] entry The coded entry that this data criteria is defining.
70
+ # @param [Hash] value_sets The value sets that this data criteria references.
71
+ # @return The modified coded entry.
72
+ def modify_entry_with_values(entry, value_sets)
73
+ return nil unless entry.present? && values.present?
74
+
75
+ # If the value itself has a code, it will be a Coded type. Otherwise, it's just a regular value with a unit.
76
+ entry.values ||= []
77
+ values.each do |value|
78
+ if value.type == "CD"
79
+ entry.values << CodedResultValue.new({codes: Coded.select_codes(value.code_list_id, value_sets), description: HQMF::Coded.select_value_sets(value.code_list_id, value_sets)["concept"]})
80
+ else
81
+ entry.values << PhysicalQuantityResultValue.new(value.format)
48
82
  end
49
83
  end
50
-
51
- base_patients
52
84
  end
53
-
54
- #
85
+
86
+ # Mark a coded entry as negated if this data criteria describes it as such.
55
87
  #
56
- # @param [Array] acceptable_times
57
- # @param [Array] acceptable_values
58
- def permutate(acceptable_times, acceptable_values)
88
+ # @param [Entry] entry The coded entry that this data criteria is potentially negating.
89
+ # @param [Hash] value_sets The value sets that this data criteria references.
90
+ # @return The modified coded entry.
91
+ def modify_entry_with_negation(entry, value_sets)
92
+ return nil unless entry.present? && negation && negation_code_list_id.present?
59
93
 
94
+ entry.negation_ind = true
95
+ entry.negation_reason = Coded.select_code(negation_code_list_id, value_sets)
60
96
  end
61
-
62
- # Modify a Record with this data criteria. Acceptable times and values are defined prior to this function.
97
+
98
+ # Add this data criteria's field related data to a coded entry.
63
99
  #
64
- # @param [Record] patient The Record that is being modified.
65
- # @param [Range] time An acceptable range of times for the coded entry being put on this patient.
100
+ # @param [Entry] entry The coded entry that this data criteria is modifying.
66
101
  # @param [Hash] value_sets The value sets that this data criteria references.
67
- # @return The modified patient. The passed in patient object will be modified by reference already so this is just for potential convenience.
68
- def modify_patient(patient, time, value_sets)
69
- # Figure out what kind of data criteria we're looking at
70
- if type == :characteristic and property != nil and patient_api_function == nil
71
- # We have a special case on our hands.
72
- if property == :birthtime
73
- patient.birthdate = time.low.to_seconds
74
- elsif value.present? && value.system == "Gender"
75
- patient.gender = value.code
76
- patient.first = Randomizer.randomize_first_name(value.code)
77
- elsif property == :clinicalTrialParticipant
78
- patient.clinicalTrialParticipant = true
79
- end
80
- else
81
- # Otherwise this is a regular coded entry. Start by choosing the correct type and assigning basic metadata.
82
- entry_type = Generator.classify_entry(patient_api_function)
83
-
84
- # HACK -- this is here to deal with types that have not been mapped yet and blow things up
85
- return patient if entry_type.nil? || entry_type == ""
86
- entry = entry_type.classify.constantize.new
87
-
88
- entry.description = "#{description} (Code List: #{code_list_id})"
89
- entry.start_time = time.low.to_seconds if time.low
90
- entry.end_time = time.high.to_seconds if time.high
91
- entry.status = status
92
- entry.codes = Coded.select_codes(code_list_id, value_sets)
93
- entry.oid = HQMF::DataCriteria.template_id_for_definition(definition, status, negation)
94
-
95
- # If the value itself has a code, it will be a Coded type. Otherwise, it's just a regular value with a unit.
96
- if value.present? && !value.is_a?(AnyValue)
97
- entry.values ||= []
98
- if value.type == "CD"
99
- entry.values << CodedResultValue.new({codes: Coded.select_codes(value.code_list_id, value_sets)})
100
- else
101
- entry.values << PhysicalQuantityResultValue.new(value.format)
102
- end
103
- end
104
-
105
- if values.present?
106
- entry.values ||= []
107
- values.each do |value|
108
- if value.type == "CD"
109
- entry.values << CodedResultValue.new({codes: Coded.select_codes(value.code_list_id, value_sets), description: Coded.select_value_sets(value.code_list_id, value_sets)['description']})
110
- else
111
- entry.values << PhysicalQuantityResultValue.new(value.format)
112
- end
113
- end
102
+ # @return The modified coded entry.
103
+ def modify_entry_with_fields(entry, value_sets)
104
+ return nil unless entry.present? && field_values.present?
105
+
106
+ field_values.each do |name, field|
107
+ next if field.nil?
108
+
109
+ # Format the field to be stored in a Record.
110
+ if field.type == "CD"
111
+ field_value = Coded.select_code(field.code_list_id, value_sets)
112
+ field_value["title"] = HQMF::Coded.select_value_sets(field.code_list_id, value_sets)["concept"]
113
+ else
114
+ field_value = field.format
114
115
  end
115
116
 
116
- # Choose a code from each relevant code vocabulary for this entry's negation, if it is negated and referenced.
117
- if negation && negation_code_list_id.present?
118
- entry.negation_ind = true
119
- entry.negation_reason = Coded.select_code(negation_code_list_id, value_sets)
117
+ field_accessor = nil
118
+ # Facilities are a special case where we store a whole object on the entry in Record. Create or augment the existing facility with this piece of data.
119
+ if name.include? "FACILITY"
120
+ facility = entry.facility
121
+ facility ||= Facility.new
122
+ facility_map = {"FACILITY_LOCATION" => :code, "FACILITY_LOCATION_ARRIVAL_DATETIME" => :start_time, "FACILITY_LOCATION_DEPARTURE_DATETIME" => :end_time}
123
+
124
+ facility.name = field.title if type == "CD"
125
+ facility_accessor = facility_map[name]
126
+ facility.send("#{facility_accessor}=", field_value)
127
+
128
+ field_accessor = :facility
129
+ field_value = facility
120
130
  end
121
-
122
- # Additional fields (e.g. ordinality, severity, etc) seem to all be special cases. Capture them here.
123
- if field_values.present?
124
- field_values.each do |name, field|
125
- next if field.nil?
126
-
127
- # These fields are sometimes Coded and sometimes Values.
128
- if field.type == "CD"
129
- codes = Coded.select_codes(field.code_list_id, value_sets)
130
- elsif field.type == "IVL_PQ" || field.type =='PQ'
131
- value = field.format
132
- end
133
-
134
- case name
135
- when "ORDINAL"
136
- entry.ordinality_code = codes
137
- when "FACILITY_LOCATION"
138
- entry.facility = Facility.new("name" => field.title, "codes" => codes)
139
- when "CUMULATIVE_MEDICATION_DURATION"
140
- entry.cumulative_medication_duration = value
141
- when "SEVERITY"
142
- entry.severity = codes
143
- when "REASON"
144
-
145
- when "SOURCE"
146
-
147
- end
131
+
132
+ begin
133
+ field_accessor ||= HQMF::DataCriteria::FIELDS[name][:coded_entry_method]
134
+ entry.send("#{field_accessor}=", field_value)
135
+ rescue
136
+ # Give some feedback if we hit an unexpected error. Some fields have no action expected, so we'll suppress those messages.
137
+ noop_fields = ["LENGTH_OF_STAY", "START_DATETIME", "STOP_DATETIME"]
138
+ unless noop_fields.include? name
139
+ field_accessor = HQMF::DataCriteria::FIELDS[name][:coded_entry_method]
140
+ puts "Unknown field #{name} was unable to be added via #{field_accessor} to the patient"
148
141
  end
149
142
  end
150
-
151
- # Figure out which section this entry will be added to. Some entry names don't map prettily to section names.
152
- section_map = { "lab_results" => "results" }
153
- section_name = section_map[entry_type]
154
- section_name ||= entry_type
155
- # Add the updated section to this patient.
156
- section = patient.send(section_name)
157
- section.push(entry)
158
-
159
- patient
160
143
  end
161
144
  end
145
+
146
+ # Add a coded entry to a patient.
147
+ #
148
+ # @param [Record] patient The coded entry that this data criteria is potentially negating.
149
+ # @param [Entry] entry The value sets that this data criteria references.
150
+ # @return The modified patient.
151
+ def modify_patient_with_entry(patient, entry)
152
+ return patient if entry.nil?
153
+
154
+ # Figure out which section this entry will be added to. Some entry names don't map prettily to section names.
155
+ entry_type = Generator.classify_entry(patient_api_function)
156
+ section_map = { "lab_results" => "results" }
157
+ section_name = section_map[entry_type]
158
+ section_name ||= entry_type
159
+
160
+ # Add the updated section to this patient.
161
+ section = patient.send(section_name)
162
+ section.push(entry)
163
+
164
+ patient
165
+ end
166
+
167
+ def characteristic?
168
+ type == :characteristic && patient_api_function.nil? ? true : false
169
+ end
162
170
  end
163
171
  end
@@ -1,144 +1,14 @@
1
1
  module HQMF
2
2
  class Range
3
- # Perform a deep copy of this Range.
4
- #
5
- # @return A deep copy of this Range.
6
- def clone
7
- Range.new(type.try(:clone), low.try(:clone), high.try(:clone), width.try(:clone))
8
- end
9
-
3
+ # Form an HQMF Range object into a shape that HealthDataStandards understands.
10
4
  #
5
+ # @return A Range formatted for storing a HealthDataStandards Record.
11
6
  def format
12
7
  if low
13
8
  low.format
14
9
  elsif high
15
10
  high.format
16
- else
17
- {}
18
- end
19
- end
20
-
21
- # Perform an intersection between this Range and the passed in Range.
22
- # There are three potential situations that can happen: disjoint, equivalent, or overlapping.
23
- #
24
- # @param [Range] range The other Range intersecting this. If it is nil it implies all times are ok (i.e. no restrictions).
25
- # @return A new Range that represents the shared amount of time between these two Ranges. nil means there is no common time.
26
- def intersection(range)
27
- # Return self if nil (the other range has no restrictions) or if it matches the other range (they are equivalent)
28
- return self.clone if range.nil?
29
- return self.clone if eql?(range)
30
-
31
- # Figure out which range starts later (the more restrictive one)
32
- if low <= range.low
33
- earlier_start = self
34
- later_start = range
35
- else
36
- earlier_start = range
37
- later_start = self
38
- end
39
-
40
- # Return nil if there is no common time (the two ranges are entirely disjoint)
41
- return nil unless later_start.contains?(earlier_start.high)
42
-
43
- # Figure out which ranges ends earlier (the more restrictive one)
44
- if high >= range.high
45
- earlier_end = self
46
- later_end = range
47
- else
48
- earlier_end = range
49
- later_end = self
50
- end
51
-
52
- Range.new("TS", later_start.low.clone, earlier_end.high.clone, nil)
53
- end
54
-
55
- # Perform a union between this Range and the passed in Range.
56
- # There are three potential situations that can happen: disjoint, equivalent, or overlapping.
57
- #
58
- # @param [Range] range The other Range unioning this.
59
- # @return An array of Ranges. One element if the two ranges are overlapping and can be expressed as one new Range
60
- # or two Ranges if the times are disjoint.
61
- def union(range)
62
- # Return self if nil (nothing new to add) or if it matches the other range (they are equivalent)
63
- return self.clone if range.nil?
64
- return self.clone if eql?(range)
65
-
66
- # Figure out which range starts earlier (to capture the most time)
67
- if low <= range.low
68
- earlier_start = self
69
- later_start = range
70
- else
71
- earlier_start = range
72
- later_start = self
73
- end
74
-
75
- # Figure out which ranges ends earlier (the more restrictive one)
76
- if high >= range.high
77
- earlier_end = self
78
- later_end = range
79
- else
80
- earlier_end = range
81
- later_end = self
82
11
  end
83
-
84
- result = []
85
- # We have continuous Ranges so we can return one Range to encapsulate both
86
- if earlier_start.contains?(later_start.low)
87
- result << Range.new("TS", earlier_start.low.clone, later_end.high.clone, nil)
88
- else
89
- # The Ranges are disjoint, so we'll need to return two arrays to capture all of the potential times
90
- result << Range.new("TS", earlier_start.low.clone, earlier_start.high.clone, nil)
91
- result << Range.new("TS", later_start.low.clone, later_start.high.clone, nil)
92
- end
93
- end
94
-
95
- #
96
- #
97
- # @param [Range] ivl_pq
98
- # @return
99
- def apply_pq(ivl_pq)
100
-
101
- end
102
-
103
- #
104
- #
105
- # @param [Range] range1
106
- # @param [Range] range2
107
- # @return
108
- def self.merge_ranges(range1, range2)
109
- return nil if range1.nil? && range2.nil?
110
- return range1 if range2.nil?
111
- return range2 if range1.nil?
112
-
113
- type = range1.type == "PQ" && range2.type == "PQ" ? "IVL_PQ" : "IVL_TS"
114
- low = Value.merge_values(range1.low, range2.low)
115
- high = Value.merge_values(range1.high, range2.high)
116
- width = nil
117
-
118
- Range.new(type, low, high, width)
119
- end
120
-
121
- # Check to see if a given value falls within this Range's high and low.
122
- #
123
- # @param [Value] value The value that may or may not fall within the range.
124
- # @return True if the value is contained. Otherwise, false.
125
- def contains?(value)
126
- start_time = low.to_time_object
127
- end_time = high.to_time_object
128
- time = value.to_time_object
129
-
130
- time.between?(start_time, end_time)
131
- end
132
-
133
- # Check to see if a given Range's low and high matches this' low and high.
134
- #
135
- # @param [Range] range The Range to which we're comparing.
136
- # @return True if the given range starts and ends at the same time as this. Otherwise, false.
137
- def eql?(range)
138
- return false if range.nil? || low.nil? || range.low.nil? || high.nil? || range.high.nil?
139
-
140
- return low.value == range.low.value && low.inclusive? == range.low.inclusive? &&
141
- high.value == range.high.value && high.inclusive? == range.high.inclusive?
142
12
  end
143
13
  end
144
14
  end