test-patient-generator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'hqmf-parser', '~> 1.0.2'
4
+ gem 'hquery-patient-api', '~> 0.3.0'
5
+ gem 'hqmf2js', '~> 1.0.0'
6
+ gem 'health-data-standards', '~> 2.1.2'
7
+
8
+ gem 'rake'
9
+ gem 'pry'
10
+ gem 'pry-nav'
11
+ gem 'bson_ext'
12
+
13
+ group :test do
14
+ gem 'simplecov'
15
+ end
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'rake/testtask'
2
+ require "simplecov"
3
+
4
+ Dir['lib/tasks/*.rake'].sort.each do |ext|
5
+ load ext
6
+ end
7
+
8
+ Rake::TestTask.new(:test_unit) do |t|
9
+ t.libs << "test"
10
+ t.test_files = FileList['test/**/*_test.rb']
11
+ t.verbose = true
12
+ end
13
+
14
+ task :test => [:test_unit] do
15
+ system("open coverage/index.html")
16
+ end
17
+
18
+ task :default => [:test]
@@ -0,0 +1,18 @@
1
+ require 'hqmf-parser'
2
+ require 'health-data-standards'
3
+
4
+ require_relative 'tpg/ext/coded'
5
+ require_relative 'tpg/ext/conjunction'
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
+ require_relative 'tpg/ext/range'
11
+ require_relative 'tpg/ext/record'
12
+ require_relative 'tpg/ext/subset_operator'
13
+ require_relative 'tpg/ext/temporal_reference'
14
+ require_relative 'tpg/ext/value'
15
+
16
+ require_relative 'tpg/generation/generator'
17
+ require_relative 'tpg/generation/randomizer'
18
+ require_relative 'tpg/generation/exporter'
@@ -0,0 +1,40 @@
1
+ module HQMF
2
+ class Coded
3
+ # Select the relevant value set that matches the given OID and generate a hash that can be stored on a Record.
4
+ # The hash will be of this format: { "code_set_identified" => [code] }
5
+ #
6
+ # @param [String] oid The target value set.
7
+ # @param [Hash] value_sets Value sets that might contain the OID for which we're searching.
8
+ # @return A Hash of code sets corresponding to the given oid, each containing one randomly selected code.
9
+ def self.select_codes(oid, value_sets)
10
+ value_sets = HQMF::Coded.select_value_sets(oid, value_sets)
11
+ code_sets = {}
12
+ value_sets["code_sets"].each do |value_set|
13
+ code_sets[value_set["code_set"]] = [value_set["codes"].first]
14
+ end
15
+ code_sets
16
+ end
17
+
18
+ def self.select_value_sets(oid, value_sets)
19
+ # 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
+ index = value_sets.index{|value_set| value_set["oid"] == oid}
21
+ value_sets = index.nil? ? { "code_sets" => [] } : value_sets[index]
22
+ value_sets
23
+ end
24
+
25
+ # Select the relevant value set that matches the given OID and generate a hash that can be stored on a Record.
26
+ # The hash will be of this format: { "codeSystem" => "code_set_identified", "code" => code }
27
+ #
28
+ # @param [String] oid The target value set.
29
+ # @param [Hash] value_sets Value sets that might contain the OID for which we're searching.
30
+ # @return A Hash including a code and code system containing one randomly selected code.
31
+ def self.select_code(oid, value_sets)
32
+ codes = select_codes(oid, value_sets)
33
+ codeSystem = codes.keys()[0]
34
+ {
35
+ 'codeSystem' => codeSystem,
36
+ 'code' => codes[codeSystem][0]
37
+ }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module Conjunction
2
+ module AllTrue
3
+ #
4
+ #
5
+ # @param [Array] base_patients
6
+ # @return
7
+ def generate(base_patients)
8
+ preconditions.each do |precondition|
9
+ precondition.generate(base_patients)
10
+ end
11
+ end
12
+ end
13
+
14
+ #
15
+ #
16
+ # @param [Array] base_patients
17
+ # @return
18
+ module AtLeastOneTrue
19
+ def generate(base_patients)
20
+ preconditions.sample.generate(base_patients)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,159 @@
1
+ module HQMF
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)
33
+ end
34
+
35
+ # Set the acceptable ranges for this data criteria so any parents can read it
36
+ @generation_range = acceptable_times
37
+
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
+ end
49
+ end
50
+
51
+ base_patients
52
+ end
53
+
54
+ #
55
+ #
56
+ # @param [Array] acceptable_times
57
+ # @param [Array] acceptable_values
58
+ def permutate(acceptable_times, acceptable_values)
59
+
60
+ end
61
+
62
+ # Modify a Record with this data criteria. Acceptable times and values are defined prior to this function.
63
+ #
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.
66
+ # @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
+ entry = entry_type.classify.constantize.new
84
+ entry.description = "#{description} (Code List: #{code_list_id})"
85
+ entry.start_time = time.low.to_seconds if time.low
86
+ entry.end_time = time.high.to_seconds if time.high
87
+ entry.status = status
88
+ entry.codes = Coded.select_codes(code_list_id, value_sets)
89
+ entry.oid = HQMF::DataCriteria.template_id_for_definition(definition, status, negation)
90
+
91
+ # If the value itself has a code, it will be a Coded type. Otherwise, it's just a regular value with a unit.
92
+ if value.present? && !value.is_a?(AnyValue)
93
+ entry.values ||= []
94
+ if value.type == "CD"
95
+ entry.values << CodedResultValue.new({codes: Coded.select_codes(value.code_list_id, value_sets)})
96
+ else
97
+ entry.values << PhysicalQuantityResultValue.new(value.format)
98
+ end
99
+ end
100
+
101
+ if values.present?
102
+ entry.values ||= []
103
+ values.each do |value|
104
+ if value.type == "CD"
105
+ 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']})
106
+ else
107
+ entry.values << PhysicalQuantityResultValue.new(value.format)
108
+ end
109
+ end
110
+ end
111
+
112
+ # Choose a code from each relevant code vocabulary for this entry's negation, if it is negated and referenced.
113
+ if negation && negation_code_list_id.present?
114
+ entry.negation_ind = true
115
+ entry.negation_reason = Coded.select_code(negation_code_list_id, value_sets)
116
+ end
117
+
118
+ # Additional fields (e.g. ordinality, severity, etc) seem to all be special cases. Capture them here.
119
+ if field_values.present?
120
+ field_values.each do |name, field|
121
+ next if field.nil?
122
+
123
+ # These fields are sometimes Coded and sometimes Values.
124
+ if field.type == "CD"
125
+ codes = Coded.select_codes(field.code_list_id, value_sets)
126
+ elsif field.type == "IVL_PQ"
127
+ value = field.format
128
+ end
129
+
130
+ case name
131
+ when "ORDINAL"
132
+ entry.ordinality_code = codes
133
+ when "FACILITY_LOCATION"
134
+ entry.facility = Facility.new("name" => field.title, "codes" => codes)
135
+ when "CUMULATIVE_MEDICATION_DURATION"
136
+ entry.cumulative_medication_duration = value
137
+ when "SEVERITY"
138
+ entry.severity = codes
139
+ when "REASON"
140
+
141
+ when "SOURCE"
142
+
143
+ end
144
+ end
145
+ end
146
+
147
+ # Figure out which section this entry will be added to. Some entry names don't map prettily to section names.
148
+ section_map = { "lab_results" => "results" }
149
+ section_name = section_map[entry_type]
150
+ section_name ||= entry_type
151
+ # Add the updated section to this patient.
152
+ section = patient.send(section_name)
153
+ section.push(entry)
154
+
155
+ patient
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,49 @@
1
+ module HQMF
2
+ class DerivationOperator
3
+ # Perform an intersection between two sets of Ranges (assuming these are timestamps).
4
+ #
5
+ # @param [Array] set1 One array of Ranges to be intersected.
6
+ # @param [Array] set2 The other array of Ranges to be intersected.
7
+ # @return A new array that contains the shared Ranges between set1 and set2.
8
+ def self.intersection(set1, set2)
9
+ # Special cases to account for emptiness
10
+ return [] if set1.empty? && set2.empty?
11
+ return set1 if set2.empty?
12
+ return set2 if set1.empty?
13
+
14
+ # Merge each element of the two sets together
15
+ result = []
16
+ set1.each do |range1|
17
+ set2.each do |range2|
18
+ intersect = range1.intersection(range2)
19
+ result << intersect unless intersect.nil?
20
+ end
21
+ end
22
+
23
+ result
24
+ end
25
+
26
+ # Perform a union between two sets of Ranges (assuming these are timestamps)
27
+ #
28
+ # @param [Array] set1 One array of Ranges to be unioned.
29
+ # @param [Array] set2 The other array of Ranges to be unioned.
30
+ # @return A new array tha contains the union of Ranges between set1 and set2.
31
+ def self.union(set1, set2)
32
+ # Special cases to account for emptiness
33
+ return [] if set1.empty? && set2.empty?
34
+ return set1 if set2.empty?
35
+ return set2 if set1.empty?
36
+
37
+ # Join each element of the two sets together
38
+ result = []
39
+ set1.each do |range1|
40
+ set2.each do |range2|
41
+ union = range1.union(range2)
42
+ result.concat!(union)
43
+ end
44
+ end
45
+
46
+ result
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ module HQMF
2
+ class PopulationCriteria
3
+ #
4
+ #
5
+ # @param [Array] base_patients
6
+ # @return
7
+ def generate(base_patients)
8
+ # All population criteria begin with a single conjunction precondition
9
+ preconditions.first.generate(base_patients)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ module HQMF
2
+ class Precondition
3
+ #
4
+ #
5
+ # @param [Array] base_patients
6
+ # @return
7
+ def generate(base_patients)
8
+ # d = HQMF::Generator.hqmf.data_criteria(preconditions[2].preconditions[1].preconditions.first.reference.id)
9
+
10
+ if conjunction?
11
+ # Include the matching module to override our generation functions
12
+ conjunction_module = "Conjunction::#{self.conjunction_code.classify}"
13
+ conjunction_module = conjunction_module.split('::').inject(Kernel) {|scope, name| scope.const_get(name)}
14
+
15
+ extend conjunction_module
16
+ generate(base_patients)
17
+ elsif reference
18
+ data_criteria = HQMF::Generator.hqmf.data_criteria(reference.id)
19
+ data_criteria.generate(base_patients)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,144 @@
1
+ module HQMF
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
+
10
+ #
11
+ def format
12
+ if low
13
+ low.format
14
+ elsif high
15
+ 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
+ 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
+ end
143
+ end
144
+ end