test-patient-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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