-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathAquarium-High-Throughput-Culturing.aq
1 lines (1 loc) · 377 KB
/
Aquarium-High-Throughput-Culturing.aq
1
{"config":{"title":"Aquarium High Throughput Culturing","description":"This workflow automates the execution of a high throughput microbial culturing experiment. This assesment will phenotypically characterize the genetically modified culture under the user defined experimental conditions. It will generate a virtual 96 well plate with sorted representations of experimental microbial cultures. The sorting algorithm groups replicates by input item and induction conditions, with the consideration of manual pipetting.","copyright":"University of Washington","version":"0.0.1","authors":[{"name":"Eriberto Lopez","affiliation":"University of Washington"}],"maintainer":{"name":"Eriberto Lopez","email":"[email protected]"},"acknowledgements":[{"name":"Cami Cordray","affiliation":"University of Washington"},{"name":"Klavins Lab","affilation":"","affiliation":"UW BIOFAB"}],"github":{"user":"EribertoLopez","repo":"Aquarium-High-Throughput-Culturing","organization":"klavinslab"},"keywords":["Automation","Yeast","E coli","Induction","Flow Cytometry","Plate Reader","DARPA SD2","UW BIOFAB"],"aquadoc_version":"1.0.0","aquarium_version":"\u003c%= Bioturk::Application.config.aquarium_version %\u003e"},"components":[{"sample_types":[],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":855,"name":"24 Unit Disorganized Collection","description":"A collection of timepoint samples with 15 possible slots","min":0,"max":10000000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2020-02-06T10:32:51.000-08:00","updated_at":"2020-02-06T13:32:51.000-08:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":3,"sample_type_name":null}],"operation_type":{"name":"Apply Experimental Condition","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Culture Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"output","name":"Experimental Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Experimental Condition","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Media,Antibiotic(s),Inducer(s),Option(s)"}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 07/09/19\n\nneeds \"Standard Libs/Debug\"\nneeds \"Tissue Culture Libs/CollectionDisplay\"\nneeds \"High Throughput Culturing/CultureComposition\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\n\nclass Protocol\n include Debug\n include CollectionDisplay\n include HighThroughputHelper\n \n # DEF\n INPUT = \"Culture Plate\"\n OUTPUT = \"Experimental Plate\"\n EXPERIMENTAL_CONDITION = \"Experimental Condition\"\n\n def intro\n show do\n title \"Apply Experimental Condition\"\n separator\n note \"This protocol will guide you through applying an experiment condition (ie: Inducer(s), Antibiotic(s), etc...) to a high throughput plate.\"\n note \"\"\n note \"\u003cb\u003e1.\u003c/b\u003e Gather materials for experiment.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Fill a multichannel stripwell with input items.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Using a multichannel pipette, transfer from the multichannel stripwell to the collection.\"\n note \"\u003cb\u003e4.\u003c/b\u003e Label with condition applied and the date \u0026 time.\"\n note \"\u003cb\u003e5.\u003c/b\u003e Incubate.\"\n end\n end\n \n def main\n intro\n clean_up_array = []\n operations.each do |op|\n experimental_condition = op.input(EXPERIMENTAL_CONDITION).val\n op = Operation.find(178796) if debug\n op.input_array(INPUT).collections.each_with_index do |collection, idx|\n collection_associations = AssociationMap.new(collection)\n part_data_matrix = get_part_data_matrix(collection_associations)\n # experimental condition input items taken from attributes associated to collection parts\n item_id_to_rc_list = get_item_id_to_rc_list(part_data_matrix: part_data_matrix, experimental_condition: experimental_condition)\n show {note \"\u003cb\u003eIn the follow steps gather #{experimental_condition} and let thaw at room temperature.\"} unless item_id_to_rc_list.empty?\n input_items = Item.find(item_id_to_rc_list.keys.map {|i| i.to_i}); take input_items, interactive: true; clean_up_array.push(input_items)\n item_id_to_rc_list.each do |item_id, rc_to_volume|\n experimental_component_item = Item.find(item_id)\n if experimental_condition == \"Option(s)\"\n optional_item = Item.find(item_id)\n show do\n title \"Optional Experimental Condition\"\n separator\n note \"Using the following slides to guide yourself in setting up the optional parameters of this experiment.\"\n table highlight_alpha_rc(collection, rc_to_volume.keys){|r,c| \"#{optional_item.sample.name}\\n#{rc_to_volume[[r,c]][:qty]}\\n#{rc_to_volume[[r,c]][:units]}\"}\n end\n else\n total_working_volume = get_total_item_working_volume(rc_to_volume) \n if total_working_volume[:qty] \u003e 0\n show do\n title \"Dispense item #{item_id} into #{collection} #{collection.object_type.name}\"\n separator\n note \"\u003cb\u003eFor the next step you will need:\u003c/b\u003e\"\n bullet \"#{(total_working_volume[:qty]).round(3)}#{total_working_volume[:units]} of #{experimental_component_item.sample.name} #{experimental_component_item.object_type.name}\"\n note \"\u003cb\u003eFollow the table below to dispense the appropriate amount of item #{item_id} into the #{collection.object_type.name}:\u003c/b\u003e\"\n table highlight_alpha_rc(collection, rc_to_volume.keys) {|r,c| \n wk_volume = rc_to_volume[[r,c]]\n \"#{wk_volume.fetch(:qty)}#{wk_volume.fetch(:units)}\"\n }\n end\n end\n end\n end\n associate_applied_condition(collection_associations: collection_associations, experimental_condition: experimental_condition)\n op.output_array(OUTPUT)[idx].set collection: collection\n end\n end\n clean_up(item_arr: clean_up_array.flatten.uniq)\n end # main\n\n def get_part_data_matrix(collection_associations)\n return collection_associations.instance_variable_get(:@map).select {|key| key == 'part_data' }.values.first\n end\n \n def associate_applied_condition(collection_associations:, experimental_condition:)\n applied_conditions = collection_associations.get('applied_conditions') \n if applied_conditions\n applied_conditions.push({experimental_condition: experimental_condition, time: timestamp})\n else\n applied_conditions = [{experimental_condition: experimental_condition, time: timestamp}]\n end\n collection_associations.put(key='applied_conditions', value=applied_conditions)\n collection_associations.save\n end\n \n def display_multichannel_stripwell(sw, sw_vol_mat, rc_list, item_id, total_working_volume)\n show do\n title \"Aliquot #{item_id} into Multichannel Stripwell\"\n separator\n note \"Follow the table below to aliquot #{item_id} into a multichannel format:\"\n table highlight_alpha_rc(sw, rc_list) {|r,c| \"#{sw_vol_mat[r][c]}#{total_working_volume[:units]}\"}\n bullet \"If this buffer has an enzyme keep stripwell on ice (ie: qPCR Master Mix)\"\n end\n end\n \n def timestamp\n timepoint = Time.now()\n day = timepoint.strftime \"%m%d%Y\"\n hour = timepoint.strftime \"%H%M\"\n return \"#{day} - #{hour}\"\n end\n \n def make_multichannel_stripwell(collection:, rc_to_volume:)\n # Create a stripwell to display\n sw = produce new_collection 'Stripwell'\n # Create a matrix the size of the stripwell\n sw_vol_mat = Array.new(sw.object_type.rows) { Array.new(sw.object_type.columns) {0} }\n rc_to_volume.keys.each {|r, c| sw_vol_mat[0][r] += rc_to_volume.fetch([r,c]).fetch(:qty) }\n rc_list = []\n sw_vol_mat.each_with_index {|stripwell, r_i| \n stripwell.each_with_index {|well_vol, w_idx| rc_list.push([0, w_idx]) if (well_vol \u003e 0) }\n }\n return sw, sw_vol_mat, rc_list\n end\n \n # TODO: how to handle changing units and adding - use HighThroughputHelper.unit_conversion_hash\n def get_total_item_working_volume(rc_to_volume)\n total_working_volume = {qty: 0, units: MICROLITERS}\n rc_to_volume.values.each do |wk_volume| \n if wk_volume.fetch(:units) == total_working_volume.fetch(:units)\n total_working_volume[:qty] += wk_volume[:qty]\n else \n raise 'not the same units!!'\n end\n end\n return total_working_volume\n end\n \n def get_item_id_to_rc_list(part_data_matrix:, experimental_condition:)\n item_id_to_rc_list = Hash.new()\n if experimental_condition != \"Option(s)\"\n part_data_matrix.each_with_index do |row, r_i|\n row.each_with_index do |part_data, c_i|\n culture_component = part_data.fetch(experimental_condition, {})\n culture_component.each do |sname, attributes|\n item_id = attributes.fetch(:item_id)\n wk_volume = attributes.fetch(:working_volume)\n (item_id_to_rc_list.keys.include? item_id) ? item_id_to_rc_list[item_id].merge!({[r_i, c_i]=\u003ewk_volume}) : item_id_to_rc_list[item_id] = {[r_i, c_i]=\u003ewk_volume}\n end\n end\n end\n else\n option_items_hash = {}\n available_option_keys = part_data_matrix.map {|row| row.map {|part| part.fetch(experimental_condition, {}) } }.flatten.uniq.reject{|part| part.empty? }.map {|part| part.keys.first}.uniq \n option_key = show do\n title \"Choose the key of what you would like to apply\"\n select available_option_keys, var: 'option', label: \"Select what experimental condition you want to apply in this operation.\", default: 0\n end\n part_data_matrix.each_with_index do |row, r_i|\n row.each_with_index do |part, c_i|\n culture_component = part.fetch(experimental_condition, {}).fetch(option_key[:option], {})\n if !culture_component.empty?\n culture_component.each do |sname, attributes|\n if option_items_hash.keys.include? sname\n option_item = option_items_hash[sname]\n else\n option_sample = Sample.find_by_name(sname)\n option_item = option_sample.items.reject {|i| i.location == 'deleted'}.first\n option_items_hash[sname] = option_item\n end\n fconc = attributes.fetch(:final_concentration, {qty: 0, units: 'NONE'})\n item_id = option_item.id\n (item_id_to_rc_list.keys.include? item_id) ? item_id_to_rc_list[item_id].merge!({[r_i, c_i]=\u003efconc}) : item_id_to_rc_list[item_id] = {[r_i, c_i]=\u003efconc}\n end\n end\n end\n end\n end\n return item_id_to_rc_list\n end\nend # protocol\n","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This operation takes in a culture plate collection applies a reagent, timestamps the collection, and passes it as an output. The operation uses the experimental condition chosen by the user as a key. The key is then used to generate data structures for displaying specific wells in a collection. Furthermore, the key is also used to fetch how much volume of the experimental condition item (ie: inducers or antibiotics) each well requires. Future directions may be to include using the Option(s) key to apply custom experimental conditions (ie: Ethanol, Cellular Stain). The option(s) parameter can be input into the Define Culture Conditions operation as a JSON parsable object.","test":"","timing":null}},{"sample_types":[{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00","field_types":[{"id":18,"parent_id":5,"name":"Parent","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-25T08:43:19.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":8,"field_type_id":18,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]},{"id":19,"parent_id":5,"name":"Integrant","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":9,"field_type_id":19,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":10,"field_type_id":19,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid","Fragment"],"object_types":[null,null]},{"id":20,"parent_id":5,"name":"Plasmid","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":11,"field_type_id":20,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid"],"object_types":[null]},{"id":21,"parent_id":5,"name":"Integrated Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":22,"parent_id":5,"name":"Plasmid Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":23,"parent_id":5,"name":"Mating Type","ftype":"string","choices":"MATa,MATalpha,Diploid","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-10T09:05:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":24,"parent_id":5,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":12,"field_type_id":24,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":25,"parent_id":5,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":13,"field_type_id":25,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":43,"parent_id":5,"name":"QC_length","ftype":"number","choices":"","array":false,"required":false,"created_at":"2016-05-20T14:29:39.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":44,"parent_id":5,"name":"Comp_cell_limit","ftype":"string","choices":"Yes,No","array":false,"required":false,"created_at":"2016-05-20T14:35:37.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":83,"parent_id":5,"name":"Media","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2017-07-12T19:25:25.000-07:00","updated_at":"2017-07-12T19:25:25.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":954,"parent_id":5,"name":"Has this strain passed QC?","ftype":"string","choices":"No,Yes","array":false,"required":false,"created_at":"2017-10-31T11:39:27.000-07:00","updated_at":"2017-10-31T20:14:46.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6455,"parent_id":5,"name":"Haploids","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2669,"field_type_id":6455,"sample_type_id":5,"object_type_id":null,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]}]},{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":4,"parent_id":2,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:30:08.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5,"parent_id":2,"name":"Sequence Verification","ftype":"url","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6,"parent_id":2,"name":"Bacterial Marker","ftype":"string","choices":"Amp,Kan,Amp + Kan,Spec,Kan + Spec,Chlor,Tet,NA,Other","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-12-27T11:26:31.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7,"parent_id":2,"name":"Yeast Marker","ftype":"string","choices":"HIS,TRP,URA,LEU,NatMX,KanMX,HygMX,BleoMX,5FOA,NA,Other","array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-02-13T10:49:58.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":27,"field_type_id":7,"sample_type_id":1,"object_type_id":null,"created_at":"2017-02-13T10:49:58.000-08:00","updated_at":"2017-02-13T10:49:58.000-08:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":8,"parent_id":2,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":42,"parent_id":2,"name":"Sequencing Primers","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":26,"field_type_id":42,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":46,"parent_id":2,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":29,"field_type_id":46,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":47,"parent_id":2,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":30,"field_type_id":47,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":48,"parent_id":2,"name":"QC_length","ftype":"number","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5816,"parent_id":2,"name":"Transformation Temperature","ftype":"number","choices":"37,30","array":false,"required":false,"created_at":"2018-09-17T15:02:22.000-07:00","updated_at":"2018-11-06T11:31:23.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":1,"parent_id":1,"name":"Overhang Sequence","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":2,"parent_id":1,"name":"Anneal Sequence","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3,"parent_id":1,"name":"T Anneal","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":11,"parent_id":4,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":12,"parent_id":4,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":13,"parent_id":4,"name":"Template","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2,"field_type_id":13,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":3,"field_type_id":13,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":4,"field_type_id":13,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":5,"field_type_id":13,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}},{"id":2547,"field_type_id":13,"sample_type_id":70,"object_type_id":null,"created_at":"2018-09-18T16:02:54.000-07:00","updated_at":"2018-09-18T16:02:54.000-07:00","sample_type":{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00"}}],"sample_types":["Plasmid","E coli strain","Fragment","Yeast Strain","DNA Library"],"object_types":[null,null,null,null,null]},{"id":14,"parent_id":4,"name":"Forward Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":6,"field_type_id":14,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2664,"field_type_id":14,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":15,"parent_id":4,"name":"Reverse Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":7,"field_type_id":15,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2665,"field_type_id":15,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":16,"parent_id":4,"name":"Restriction Enzyme(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":17,"parent_id":4,"name":"Yeast Marker","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6423,"parent_id":4,"name":"Fragment Mix Array","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-24T11:05:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2667,"field_type_id":6423,"sample_type_id":1,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2668,"field_type_id":6423,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]}]},{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":10,"parent_id":3,"name":"Parent","ftype":"sample","choices":null,"array":null,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1,"field_type_id":10,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["E coli strain"],"object_types":[null]}]},{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00","field_types":[{"id":3965,"parent_id":70,"name":"Oligo Pool","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1772,"field_type_id":3965,"sample_type_id":75,"object_type_id":null,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","sample_type":{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00"}}],"sample_types":["Oligo Pool"],"object_types":[null]}]},{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00","field_types":[{"id":3947,"parent_id":75,"name":"Manufacturer","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3948,"parent_id":75,"name":"Oligo Library ID","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3949,"parent_id":75,"name":"inner forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1763,"field_type_id":3949,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3950,"parent_id":75,"name":"inner reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1764,"field_type_id":3950,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3951,"parent_id":75,"name":"sublibrary forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1765,"field_type_id":3951,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3952,"parent_id":75,"name":"sublibrary reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1766,"field_type_id":3952,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3953,"parent_id":75,"name":"min length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3954,"parent_id":75,"name":"max length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3955,"parent_id":75,"name":"variants (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3964,"parent_id":75,"name":"sublibrary name (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2018-06-05T14:37:16.000-07:00","updated_at":"2018-06-05T14:37:16.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7477,"parent_id":75,"name":"forward priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7478,"parent_id":75,"name":"reverse priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":15,"name":"Media","description":"Media for yeast and E. coli","created_at":"2015-12-16T14:13:57.000-08:00","updated_at":"2015-12-16T14:35:10.000-08:00","field_types":[]}],"object_types":[{"id":350,"name":"Yeast Plate","description":"Yeast Plate","min":0,"max":100000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:04:32.000-08:00","updated_at":"2015-03-02T10:44:55.000-08:00","unit":"Yeast Strain","cost":1.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"DFP","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":214,"name":"Yeast Glycerol Stock","description":"A 1.8mL culture of half saturated yeast overnight and half 50% glycerol stored in the -80C freezer","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2013-10-16T15:07:47.000-07:00","updated_at":"2014-08-12T18:35:37.000-07:00","unit":"Yeast Strain","cost":5.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"M80","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":365,"name":"E coli Plate of Plasmid","description":"A plate containing E. coli transformed with a plasmid","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-25T17:05:34.000-08:00","updated_at":"2015-03-02T10:45:33.000-08:00","unit":"Plasmid","cost":0.01,"release_method":"return","release_description":"","sample_type_id":2,"image":"","prefix":"DFP","rows":null,"columns":null,"sample_type_name":"Plasmid"},{"id":210,"name":"E coli Glycerol Stock","description":"Long term storage of E. coli in the -80C","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2013-10-15T10:19:08.000-07:00","updated_at":"2014-01-02T09:34:13.000-08:00","unit":"E coli strain","cost":1000.0,"release_method":"return","release_description":"","sample_type_id":3,"image":"","prefix":"M80","rows":null,"columns":null,"sample_type_name":"E coli strain"},{"id":347,"name":"Yeast Overnight Suspension","description":"Yeast Overnight Suspension","min":0,"max":1000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:02:38.000-08:00","updated_at":"2017-10-23T13:35:19.000-07:00","unit":"Yeast Strain","cost":1.0,"release_method":"query","release_description":"","sample_type_id":5,"image":"","prefix":"DFO","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":466,"name":"800 mL Liquid","description":"800 mL Bottle","min":0,"max":10000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":" {\r\n \"samples\": [\r\n { \"name\": \"SC\", \"materials\": 0.0135, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"SDO\", \"materials\": 0.014, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"50% Glycerol\", \"materials\": 0.02, \"labor\": 0.02, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB + Amp\", \"materials\": 0.03, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"LB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }, \r\n { \"name\": \"YPAD\", \"materials\": 0.01, \"labor\": 0.025, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }\r\n ]\r\n }","vendor":"No vendor information","created_at":"2015-12-16T14:35:46.000-08:00","updated_at":"2017-06-13T11:30:03.000-07:00","unit":"Media","cost":0.01,"release_method":"return","release_description":"","sample_type_id":15,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Media"}],"operation_type":{"name":"Define Culture Conditions","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Strain","sample_types":["Yeast Strain","Yeast Strain","Plasmid","E coli strain","Yeast Strain"],"object_types":["Yeast Plate","Yeast Glycerol Stock","E coli Plate of Plasmid","E coli Glycerol Stock","Yeast Overnight Suspension"],"part":false,"array":false,"routing":"CC","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"output","name":"Culture Condition","sample_types":["Yeast Strain","Yeast Strain","Plasmid","E coli strain","Yeast Strain"],"object_types":["Yeast Plate","Yeast Glycerol Stock","E coli Plate of Plasmid","E coli Glycerol Stock","Yeast Overnight Suspension"],"part":false,"array":false,"routing":"CC","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"json","role":"input","name":"Inducer(s)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"number","role":"input","name":"Replicates","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"json","role":"input","name":"Control Tag","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"input","name":"Media","sample_types":["Media"],"object_types":["800 mL Liquid"],"part":false,"array":false,"routing":"M","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"json","role":"input","name":"Antibiotic(s)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"json","role":"input","name":"Option(s)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"class Protocol\n\n def main\n\n operations.each do |op|\n op.error(\"Control block error\", \"Control blocks are not intended to be run by a technician.\") \n end\n \n end\n\nend\n","precondition":"\ndef precondition(_op)\n op = Operation.find(_op.id)\n # Check to see whether the parameter value is JSON parasable before starting\n op.field_values.select {|fv| fv.role == 'input' }.each do |fv|\n if [\"Strain\", \"Media\", \"Replicates\"].include? fv.name\n next # These field values are not JSON parameters\n else\n valid_json?(fv.value)\n end\n end\n op.pass(\"Strain\", \"Culture Condition\")\n # set status to done, so this block will not be evaluated again\n op.status = \"done\"\n op.save\nend\n\ndef valid_json?(json)\n JSON.parse(json) rescue raise \"Not at parsable json: #{json}\" # JSON::ParserError =\u003e e\nend\n","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This operation does NOT get run by a technician. It can be stepped through as long as all FieldValue parameters are valid.\nThis operation abstracts planning microbial culture conditions. These conditions will be wired to `Inoculate Culture Plate` to define parameters for High Throughput Culturing Experiment.\n\n\nFor parameters that will not be used or filled use an empty `{}`\n\n__Strain__\nEnter the name or id of the Strain.\n\n__Media__\nEnter the name or id of the Media.\n\n__Inducer(s)__\nEnter a JSON object to represent \nthe type of inducer by `name`,\nthe desired `final_concentration`, and\nthe `item_id` of the stock that will\nbe used and diluted.\n \n { \n \"beta-estradiol\": {\n \"final_concentration\": [\n \"100_nM\",\"200_nM\"\n ]\n },\n \"IPTG\": {\n \"final_concentration\": [\"50_nM\"]\n }\n }\n \nRepresents the following conditions:\n1. `b-e at 100nM + IPTG at 50nM`\n2. `b-e at 200nM + IPTG at 50nM`\n\n__Antibiotic(s)__\nEnter a JSON object to represent\nadditional antibiotics.\nFollow the example below:\n \n {\n \"Ampicillin Antibiotic\": {\n \"final_concentration\": \"10_ug/mL\"\n }\n }\n\nYou can see the stock concentration\non the sample properties under,\nthe description of the sample.\n \n__Control Tag__\nEnter a JSON object to tag control.\nThe key represents the type of control\nand value represents positive or negative.\nThen, you can add your own additional\ninformation.\n \n For a flow cytometry control,\n use the example below.\n {\n \"flourescence_control\": \"positive\",\n \"channel\": \"tdTomato\"\n }\n \n Example growth control:\n {\n \"growth_control\": \"negative\"\n }\n \nThis tag will allow Aq to place these\ncultures in all of the plates in the\nplanned experiment.\n \n__Replicates__\nEnter an integer of the number of \nreplicates (cultures) desired for \neach of the conditions.\n \n__Option(s)__\nEnter a JSON object for additional \noptions. This could be used for \nnotes or a way to prototype\nnew operation features.\n\n\nThis operation allows a user to define experimental culture conditions for a given microbial sample. The conditions would be very many combinations of inducers, antibiotics, medias, and sample types.\nThe operation will then be wired to `Inoculate Culture Plate` where all conditions will be accounted for, sorted, and organized into a high throughput container.","test":"class ProtocolTest \u003c ProtocolTestBase\n MEDIA = \"Media\"\n CONTROL = \"Control Tag as { tag, type }\"\n INDUCERS = \"Inducer(s) as { name: { final concentration: { qty:, units: } } }\"\n ANTIBIOTICS = \"Antibiotic(s) as { name: { final concentration: { qty:, units: } } }\"\n REPLICATES = \"Replicates\"\n TEMPERATURE = \"Temperature (C)\"\n\n\n\n\n def setup\n\n # add_random_operations(1) # defines three random operations\n add_operation\n .with_input(MEDIA, Sample.find(\t11767))\n .with_input(CONTROL, {})\n .with_input(INDUCERS, { 'IPTG': { final_concentration: { 'qty': 10 , 'units': 'uM' } } })\n .with_input(ANTIBIOTICS, { 'Kanamyacin': { final_concentration: { 'qty': 10 , 'units': 'nM' } } })\n .with_input(REPLICATES, 3)\n .with_input(TEMPERATURE, 30)\n\n \n\n\n\n # add_operation # adds a custom made operation\n # .with_input(\"Primer\", Sample.find(3))\n # .with_property(\"x\", 123)\n # .with_output(\"Primer\", Sample.find(3)) \n\n end\n\n def analyze\n log \"Hello from Nemo LIVE!\"\n @operations.each do |op|\n log \"#{op.id}\"\n end\n\n # log \"#{@backtrace}\"\n assert_equal @backtrace.last[:operation], \"complete\"\n \n end\n\nend","timing":null}},{"sample_types":[{"id":15,"name":"Media","description":"Media for yeast and E. coli","created_at":"2015-12-16T14:13:57.000-08:00","updated_at":"2015-12-16T14:35:10.000-08:00","field_types":[]}],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":730,"name":"96 Well Flat Bottom (black)","description":"plate for plate reader","min":0,"max":1000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"max_well_vol_ul\" : 300,\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2017-11-14T10:54:03.000-08:00","updated_at":"2019-08-14T11:13:34.000-07:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":455,"name":"96 U-bottom Well Plate","description":"96 U bottom well for cytometer readings","min":0,"max":10000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2015-02-04T12:40:07.000-08:00","updated_at":"2019-08-14T11:14:10.000-07:00","unit":"plate","cost":1.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":855,"name":"24 Unit Disorganized Collection","description":"A collection of timepoint samples with 15 possible slots","min":0,"max":10000000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2020-02-06T10:32:51.000-08:00","updated_at":"2020-02-06T13:32:51.000-08:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":3,"sample_type_name":null},{"id":466,"name":"800 mL Liquid","description":"800 mL Bottle","min":0,"max":10000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":" {\r\n \"samples\": [\r\n { \"name\": \"SC\", \"materials\": 0.0135, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"SDO\", \"materials\": 0.014, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"50% Glycerol\", \"materials\": 0.02, \"labor\": 0.02, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB + Amp\", \"materials\": 0.03, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"LB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }, \r\n { \"name\": \"YPAD\", \"materials\": 0.01, \"labor\": 0.025, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }\r\n ]\r\n }","vendor":"No vendor information","created_at":"2015-12-16T14:35:46.000-08:00","updated_at":"2017-06-13T11:30:03.000-07:00","unit":"Media","cost":0.01,"release_method":"return","release_description":"","sample_type_id":15,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Media"}],"operation_type":{"name":"Dilute Collection","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Culture Plate","sample_types":[null,null,null,null],"object_types":["Eppendorf 96 Deepwell Plate","96 Well Flat Bottom (black)","96 U-bottom Well Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"output","name":"Diluted Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Dilution","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"0.5X, 0.2X,0.1X, 0.01X,0.001X"},{"ftype":"sample","role":"input","name":"Media","sample_types":["Media"],"object_types":["800 mL Liquid"],"part":false,"array":false,"routing":"M","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Keep Input Plate?","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Yes,No"}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 08/13/19\n\nneeds \"Standard Libs/Debug\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\n\nclass Protocol\n include HighThroughputHelper\n include Debug\n \n # DEF\n INPUT = \"Culture Plate\"\n OUTPUT = \"Diluted Plate\"\n DILUTION = \"Dilution\"\n MEDIA = \"Media\"\n KEEP_IN_PLT = \"Keep Input Plate?\"\n \n # Constants\n ALLOWABLE_DILUTANT_SAMPLETYPES = ['Yeast Strain', 'Plasmid', 'E coli strain'] # sample types that will be transferred to new plate\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def intro\n show do\n title \"Dilute Collection\"\n separator\n note \"This protocol will guide you on how to dilute the cultures of a culturing plate into another plate.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Gather materials.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Pre-fill new plate with media, if necessary.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Transfer aliquot of culture to new plate.\"\n note \"\u003cb\u003e4.\u003c/b\u003e Incubate.\"\n end\n end\n \n def main\n intro\n operations.retrieve.make\n clean_up_arr = []\n operations.make.each do |op|\n dilution_factor = get_dilution_factor(op: op, fv_str: DILUTION)\n op.input_array(INPUT).collections.zip(op.output_array(OUTPUT).collections) do |from_collection, to_collection|\n raise \"Not enough output plates have been planned please add an output field value to this operation: Plan #{op.plan.id} Operation #{op.id}.\" if to_collection.nil? \n gather_materials(empty_containers: [to_collection], new_materials: ['P1000 Multichannel', 'Permeable Area Seals', 'Multichannel Resivior'], take_items: [from_collection] )\n stamp_transfer(from_collection: from_collection, to_collection: to_collection, process_name: 'dilution')\n transfer_volume = pre_fill_collection(to_collection: to_collection, dilution_factor: dilution_factor, media_item: op.input(MEDIA).item)\n tech_transfer_samples(from_collection: from_collection, to_collection: to_collection, transfer_volume: transfer_volume)\n from_loc = from_collection.location; to_collection.location = from_loc; to_collection.save()\n (op.input(KEEP_IN_PLT).val.to_s.downcase == 'yes') ? (from_collection.mark_as_deleted; from_collection.save()) : nil\n end\n clean_up(item_arr: clean_up_arr)\n operations.store\n end\n end #main\n \n def tech_transfer_samples(from_collection:, to_collection:, transfer_volume:)\n show do\n title \"Transferring From #{from_collection} To #{to_collection}\"\n separator\n warning \"Make sure that both plates are in the same orientation.\".upcase\n note \"Using a Multichannel pipette, follow the table to transfer #{transfer_volume}#{MICROLITERS} of cultures:\"\n bullet \"\u003cb\u003eFrom #{from_collection.object_type.name}\u003c/b\u003e #{from_collection}\"\n bullet \"\u003cb\u003eTo #{to_collection.object_type.name}\u003c/b\u003e #{to_collection}\"\n table highlight_alpha_non_empty(to_collection) {|r,c| \"#{transfer_volume}#{MICROLITERS}\" }\n end\n end\n\n def pre_fill_collection(to_collection:, dilution_factor:, media_item:)\n destination_working_volume = get_object_type_working_volume(to_collection).to_i\n culture_volume = (destination_working_volume*dilution_factor.to_i).round(3)\n media_volume = (destination_working_volume-culture_volume).round(3)\n take [media_item], interactive: true\n if dilution_factor != 'None'\n total_media = ((to_collection.get_non_empty.length*media_volume*1.1)/1000).round(2) #mLs\n show do \n title \"Pre-fill #{to_collection.object_type.name} #{to_collection} with #{media_item.sample.name}\"\n separator\n check \"Gather a \u003cb\u003eMultichannel Pipette\u003cb\u003e\"\n check \"Gather a \u003cb\u003eMultichannel Resivior\u003cb\u003e\"\n check \"Gather #{total_media}#{MILLILITERS}\"\n note \"Pour the media into the resivior in 30mL aliquots\"\n note \"You will need these materials in the next step.\"\n end\n show do\n title \"Pre-fill #{to_collection.object_type.name} #{to_collection} with #{media_item.sample.name}\"\n separator\n note \"Follow the table below to transfer media into #{to_collection}:\"\n table highlight_alpha_non_empty(to_collection){|r,c| \"#{media_volume}#{MICROLITERS}\"}\n end\n end\n return culture_volume\n end\n\n def get_object_type_working_volume(container)\n working_volume = JSON.parse(container.object_type.data).fetch('working_vol', nil)\n raise \"The #{container.id} #{container.object_type.name} ObjectType does not have a 'working_vol' association. \n Please go to the container definitions page and add a JSON parsable association!\".upcase if working_volume.nil?\n return working_volume\n end\n \n def copy_sample_matrix(from_collection:, to_collection:)\n sample_hash = Hash.new()\n from_collection_sample_types = from_collection.matrix.flatten.uniq.reject{|i| i == EMPTY }.map {|sample_id| [sample_id, Sample.find(sample_id)] }\n from_collection_sample_types.each {|sid, sample| (ALLOWABLE_DILUTANT_SAMPLETYPES.include? sample.sample_type.name) ? (sample_hash[sid] = sample) : (sample_hash[sid] = EMPTY) }\n dilution_sample_matrix = from_collection.matrix.map {|row| row.map {|sample_id| sample_hash[sample_id] } }\n to_collection.matrix = dilution_sample_matrix\n to_collection.save()\n end\n \n def transfer_part_associations(from_collection:, to_collection:)\n copy_sample_matrix(from_collection: from_collection, to_collection: to_collection)\n from_collection_associations = AssociationMap.new(from_collection)\n to_collection_associations = AssociationMap.new(to_collection)\n from_associations_map = from_collection_associations.instance_variable_get(:@map)\n # Remove previous source data from each part\n from_associations_map.reject! {|k| k != 'part_data'} # Retain only the part_data, so that global associations do not get copied over\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"source\") ? part.reject! {|k| k == \"source\" } : part } }\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"destination\") ? part.reject! {|k| k == \"destination\" } : part } }\n # Set edited map to the destination collection_associations\n to_collection_associations.instance_variable_set(:@map, from_associations_map)\n to_collection_associations.save()\n return from_associations_map\n end \n \n def part_provenance_transfer(from_collection:, to_collection:, process_name:)\n to_collection_part_matrix = to_collection.part_matrix\n from_collection.part_matrix.each_with_index do |row, r_i|\n row.each_with_index do |from_part, c_i|\n if (from_part) \u0026\u0026 (ALLOWABLE_DILUTANT_SAMPLETYPES.include? from_part.sample.sample_type.name)\n to_part = to_collection_part_matrix[r_i][c_i]\n if !to_part.nil?\n # Create source and destination objs\n source_id = from_part.id; source = [{id: source_id }]\n destination_id = to_part.id; destination = [{id: destination_id }]\n destination.first.merge({additional_relation_data: { process: process_name }}) unless process_name.nil?\n # Association source and destination\n to_part.associate(key=:source, value=source)\n from_part.associate(key=:destination, value=destination)\n end\n end\n end\n end\n end\n \n def stamp_transfer(from_collection:, to_collection:, process_name: nil)\n from_associations_map = transfer_part_associations(from_collection: from_collection, to_collection: to_collection)\n part_provenance_transfer(from_collection: from_collection, to_collection: to_collection, process_name: process_name)\n return from_associations_map.fetch('part_data')\n end\n\nend #Class\n","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will guide you on how to dilute the cultures of a culturing plate into another plate.\n\n 1. Gather materials.\n \n 2. Pre-fill new plate with media, if necessary.\n \n 3. Transfer aliquot of culture to new plate.\n \n 4. Incubate.\n\n\nThis operation takes in a collection and dilutes the microbial cultures found in it by a user specified dilution factor. It requires that the output container have a `Data` association {'working_vol': '1000_uL'} in order for the dilution to be computed.","test":"","timing":null}},{"sample_types":[],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":855,"name":"24 Unit Disorganized Collection","description":"A collection of timepoint samples with 15 possible slots","min":0,"max":10000000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2020-02-06T10:32:51.000-08:00","updated_at":"2020-02-06T13:32:51.000-08:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":3,"sample_type_name":null}],"operation_type":{"name":"Experimental Recovery","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Culture Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":11099,"choices":null},{"ftype":"sample","role":"output","name":"Diluted Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Dilution","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"0.5X, 0.2X,0.1X, 0.01X,0.001X"},{"ftype":"string","role":"input","name":"When to recover? (24hr)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 080619\n\nneeds \"Standard Libs/Debug\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\n\nclass Protocol\n include HighThroughputHelper\n include Debug\n \n #DEF\n INPUT = \"Culture Plate\"\n OUTPUT = \"Diluted Plate\"\n DILUTION = \"Dilution\"\n OUTGROWTH = \"Outgrowth (hr)\"\n OPTIONS = \"Option(s)\"\n\n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def intro\n show do\n title \"High Throughput Culturing Recovery\"\n separator\n note \"This protocol will guide you on how to recover your growing cultures and prepare the next plate for your experiment.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Gather necessary materials.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Pre-fill new plate with media(s).\"\n note \"\u003cb\u003e3.\u003c/b\u003e Stamp transfer plate wells to pre-filled new plate.\"\n note \"\u003cb\u003e4.\u003c/b\u003e Incubate.\"\n end\n end\n \n def main\n intro\n operations.retrieve.make\n clean_up_arr = []\n operations.each do |op|\n dilution_factor = get_dilution_factor(op: op, fv_str: DILUTION)\n op.input_array(INPUT).collections.zip(op.output_array(OUTPUT).collections).each do |from_collection, to_collection|\n raise \"Not enough output plates have been planned please add an output field value to this operation: Plan #{op.plan.id} Operation #{op.id}.\" if to_collection.nil?\n # Tranfer culture information and record PartProvenance\n part_associations_matrix = stamp_transfer(from_collection: from_collection, to_collection: to_collection, process_name: \"Dilution\")\n gather_materials(empty_containers: [to_collection], new_materials: ['Multichannel Pipette', 'Media Reservoir'], take_items: [])\n # Account and gather materials for the output collection\n transfer_vol_matrix = get_transfer_volume_matrix(collection: from_collection, part_associations_matrix: part_associations_matrix, dilution_factor: dilution_factor)\n pre_fill_collection(out_collection: to_collection, part_associations_matrix: part_associations_matrix, transfer_vol_matrix: transfer_vol_matrix)\n transfer_and_dilute_cultures(in_collection: from_collection, out_collection: to_collection, transfer_vol_matrix: transfer_vol_matrix)\n # Delete input collection and move output collection to incubator\n incubator_loc = from_collection.location\n to_collection.location = incubator_loc\n to_collection.save()\n from_collection.mark_as_deleted\n end\n clean_up(item_arr: clean_up_arr)\n operations.store\n end\n end #main\n \n def pre_fill_collection(out_collection:, part_associations_matrix:, transfer_vol_matrix:)\n media_hash = get_component_volume_hash(matrix: part_associations_matrix, component_type: \"Media\")\n show do \n title \"Pre-fill #{out_collection.object_type.name} #{out_collection} with Media\"\n separator\n note \"You will need the following amount of media:\"\n media_hash.each {|media, volume| check \"\u003cb\u003e#{(volume/1000).round(2)}#{MILLILITERS}\u003c/b\u003e of \u003cb\u003e#{media} #{Item.find(media).sample.name}\u003c/b\u003e\"}\n end\n \n media_position_hash = {}\n part_associations_matrix.each_with_index do |row, r_i|\n row.each_with_index do |part, c_i|\n media_attribute = part.fetch(\"Media\", false)\n if media_attribute\n if !media_attribute.nil? || !media_attribute.empty?\n media_name = media_attribute.keys.first\n position = [r_i, c_i]\n (media_position_hash.keys.include? media_name) ? (media_position_hash[media_name].push(position)) : (media_position_hash[media_name] = [position])\n end\n end\n end\n end\n \n media_position_hash.each do |media_name, rc_list|\n show do \n title \"Pre-fill #{out_collection.object_type.name} #{out_collection} with Media\"\n separator\n note \"Follow the table below to prefill #{out_collection.id} with #{media_name}:\"\n table highlight_alpha_rc(out_collection, rc_list) {|r, c|\n media_component = part_associations_matrix[r][c].fetch(\"Media\").values.first\n m_vol = media_component.fetch(:working_volume)\n m_vol[:qty] = m_vol[:qty] - transfer_vol_matrix[r][c]\n \"#{format_collection_display_str(m_vol)}\" \n }\n end\n end\n end\n \n def transfer_and_dilute_cultures(in_collection:, out_collection:, transfer_vol_matrix:)\n show do \n title \"Transfer Cultures from #{in_collection} to #{out_collection}\"\n separator\n note \"Transfer cultures:\"\n bullet \"\u003cb\u003eFrom #{in_collection.object_type.name} #{in_collection}\u003c/b\u003e\"\n bullet \"\u003cb\u003eTo #{out_collection.object_type.name} #{out_collection}\u003c/b\u003e\"\n note \"Follow the table to transfer the appropriate volume:\"\n table highlight_alpha_non_empty(out_collection) {|r, c| \"#{transfer_vol_matrix[r][c]}#{MICROLITERS}\"}\n end\n end\n \n def copy_sample_matrix(from_collection:, to_collection:)\n sample_matrix = from_collection.matrix\n to_collection.matrix = sample_matrix\n to_collection.save()\n end\n \n def transfer_part_associations(from_collection:, to_collection:)\n copy_sample_matrix(from_collection: from_collection, to_collection: to_collection)\n from_collection_associations = AssociationMap.new(from_collection)\n to_collection_associations = AssociationMap.new(to_collection)\n from_associations_map = from_collection_associations.instance_variable_get(:@map)\n # Remove previous source data from each part\n from_associations_map.reject! {|k| k != 'part_data'} # Retain only the part_data, so that global associations do not get copied over\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"source\") ? part.reject! {|k| k == \"source\" } : part } }\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"destination\") ? part.reject! {|k| k == \"destination\" } : part } }\n # log_info 'from_associations_map part_data with out source and destination', from_associations_map\n # Set edited map to the destination collection_associations\n to_collection_associations.instance_variable_set(:@map, from_associations_map)\n to_collection_associations.save()\n return from_associations_map\n end \n \n def part_provenance_transfer(from_collection:, to_collection:, process_name:)\n to_collection_part_matrix = to_collection.part_matrix\n from_collection.part_matrix.each_with_index do |row, r_i|\n row.each_with_index do |from_part, c_i|\n if from_part\n to_part = to_collection_part_matrix[r_i][c_i]\n # Create source and destination objs\n source_id = from_part.id; source = [{id: source_id }]\n destination_id = to_part.id; destination = [{id: destination_id }]\n destination.first.merge({additional_relation_data: { process: process_name }}) unless process_name.nil?\n # Association source and destination\n to_part.associate(key=:source, value=source)\n from_part.associate(key=:destination, value=destination)\n end\n end\n end\n end\n \n def stamp_transfer(from_collection:, to_collection:, process_name: nil)\n from_associations_map = transfer_part_associations(from_collection: from_collection, to_collection: to_collection)\n part_provenance_transfer(from_collection: from_collection, to_collection: to_collection, process_name: process_name)\n return from_associations_map.fetch('part_data')\n end\n\n def get_component_volume_hash(matrix:, component_type:)\n volume_hash = Hash.new(0)\n matrix.each do |culture_array|\n culture_array.each do |culture|\n component = culture.fetch(component_type, nil)\n if component\n attributes = component.values.first\n item_id = attributes.fetch(:item_id, nil)\n if item_id.nil?\n next\n else\n volume_hash[item_id] += attributes.fetch(:working_volume).fetch(:qty)\n end\n end\n end\n end\n return volume_hash\n end\n \n def get_object_type_working_volume(container)\n working_volume = JSON.parse(container.object_type.data).fetch('working_vol', nil)\n raise \"The #{container.id} #{container.object_type.name} ObjectType does not have a 'working_vol' association. \n Please go to the container definitions page and add a JSON parsable association!\".upcase if working_volume.nil?\n return working_volume.to_f\n end\n \n def get_transfer_volume_matrix(collection:, part_associations_matrix:, dilution_factor:)\n transfer_vol_matrix = Array.new(collection.object_type.rows) { Array.new(collection.object_type.columns) { EMPTY } }\n collection = collection_from(collection)\n collection_working_volume = get_object_type_working_volume(container=collection)\n collection.get_non_empty.each do |r, c|\n culture_volume = part_associations_matrix[r][c].fetch(\"Culture_Volume\", false)\n if culture_volume\n transfer_vol_matrix[r][c] = (dilution_factor*collection_working_volume).round(3)\n end\n end\n return transfer_vol_matrix\n end\n \n def format_collection_display_str(value)\n if value.is_a? Hash\n return \"#{(value[:qty]).round(3)}#{value[:units]}\"\n elsif value.is_a? String\n return value\n else\n raise \"This #{value.class} can not be formatted for collection display\"\n end\n end\n\nend #Class","precondition":"# This operation should be wired to only after inoculate culture plate\ndef precondition(_op)\n true # This operation should be wired to only after inoculate culture plate\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will guide you on how to recover your growing cultures and prepare the next plate for your experiment.\n\n 1. Gather necessary materials.\n \n 2. Pre-fill new plate with media(s).\n \n 3. Stamp transfer plate wells to pre-filled new plate.\n \n 4. Incubate.","test":"","timing":null}},{"sample_types":[{"id":65,"name":"Beads","description":"calibration beads for flow cytometry","created_at":"2017-11-14T10:19:08.000-08:00","updated_at":"2017-11-14T10:19:08.000-08:00","field_types":[]}],"object_types":[{"id":724,"name":"Bead droplet dispenser","description":"Stock bottle of beads for cytometer calibration","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2017-11-14T10:20:02.000-08:00","updated_at":"2017-11-14T10:51:16.000-08:00","unit":"Beads","cost":0.01,"release_method":"return","release_description":"","sample_type_id":65,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Beads"},{"id":725,"name":"Diluted beads","description":"Drop of calibration bead stock in 1xPBS","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2017-11-14T10:22:03.000-08:00","updated_at":"2017-11-14T10:22:03.000-08:00","unit":"Beads","cost":0.01,"release_method":"return","release_description":"","sample_type_id":65,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Beads"}],"operation_type":{"name":"Flow Cytometry Calibration","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Optical Particles","sample_types":["Beads"],"object_types":["Bead droplet dispenser"],"part":false,"array":false,"routing":"OP","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"output","name":"Diluted Beads","sample_types":["Beads"],"object_types":["Diluted beads"],"part":false,"array":false,"routing":"OP","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 08/13/19\n\nneeds \"High Throughput Culturing/FlowCytometryHelper\"\nneeds \"High Throughput Culturing/FlowCytometryCalibration\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\nneeds \"Standard Libs/Debug\"\n\nclass Protocol\n include Debug\n include FlowCytometryHelper, FlowCytometryCalibration, HighThroughputHelper \n \n # DEF\n INPUT = \"Optical Particles\"\n OUTPUT = \"Diluted Beads\"\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def intro\n flow_cytometer = FlowCytometer.new()\n get_flow_cytometer_software(flow_cytometer: flow_cytometer)\n show do\n title \"Calibrating the Flow Cytometer\"\n separator\n note \"To compare different experiments against each other we must have a way to compare the fluorescence intensities across different flow cytometers.\"\n note \"To achieve this we will be using small beads that fluoresce multiple colors.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Setup flow cytometer workspace and measure bead sample.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Take chosen beads and dilute, if necessary.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Upload .fcs file to Aquarium.\"\n end\n return flow_cytometer\n end\n \n def main\n fc = intro\n operations.group_by {|op| op.input(INPUT).item }.each do |bead_item, ops|\n setup_calibration_measurement(flow_cytometer: fc, bead_item: bead_item)\n setup_instrument_calibration(instrument=fc)\n empty_containers = []; new_materials = []; take_items = [fc.experimental_item]\n (empty_containers.push(fc.measurement_item); new_materials.push('P1000 Pipette', 'Molecular Grade H2O', '1.5mL Tube')) unless (fc.experimental_item.id == fc.measurement_item.id)\n gather_materials(empty_containers: empty_containers, transfer_required: fc.transfer_required, new_materials: new_materials, take_items: take_items)\n prepare_calibration_beads(flow_cytometer: fc)\n take_calibration_and_upload_data(instrument: fc)\n process_and_associate_calibration(instrument: fc, ops: ops)\n fc.measurement_item.location = 'R4 Fridge'\n end\n release_arr = Protocol.materials_list.flatten.reject {|m| m.is_a? String }.uniq\n cleaning_up(release_arr)\n end # Main\nend # Protocol\n","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"To compare different experiments against each other we must have a way to compare the fluorescence intensities across different flow cytometers.\nTo achieve this we will be using small beads that fluoresce multiple colors.\n\n 1. Setup flow cytometer workspace and measure bead sample.\n \n 2. Take chosen beads and dilute, if necessary.\n \n 3. Upload .fcs file to Aquarium.","test":"","timing":null}},{"sample_types":[],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":455,"name":"96 U-bottom Well Plate","description":"96 U bottom well for cytometer readings","min":0,"max":10000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2015-02-04T12:40:07.000-08:00","updated_at":"2019-08-14T11:14:10.000-07:00","unit":"plate","cost":1.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":730,"name":"96 Well Flat Bottom (black)","description":"plate for plate reader","min":0,"max":1000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"max_well_vol_ul\" : 300,\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2017-11-14T10:54:03.000-08:00","updated_at":"2019-08-14T11:13:34.000-07:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":688,"name":"24 Deep Well Plate","description":"A 4-by-6 cell culturing plate","min":0,"max":1,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2017-09-23T15:22:30.000-07:00","updated_at":"2017-09-23T15:28:02.000-07:00","unit":"Well","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":4,"columns":6,"sample_type_name":null},{"id":855,"name":"24 Unit Disorganized Collection","description":"A collection of timepoint samples with 15 possible slots","min":0,"max":10000000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2020-02-06T10:32:51.000-08:00","updated_at":"2020-02-06T13:32:51.000-08:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":3,"sample_type_name":null}],"operation_type":{"name":"Flow Cytometry Measurement","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Experimental Plate","sample_types":[null,null,null,null,null],"object_types":["Eppendorf 96 Deepwell Plate","96 U-bottom Well Plate","96 Well Flat Bottom (black)","24 Deep Well Plate","24 Unit Disorganized Collection"],"part":false,"array":false,"routing":"P","preferred_operation_type_id":660,"preferred_field_type_id":7918,"choices":null},{"ftype":"sample","role":"output","name":"Measurement Plate","sample_types":[null,null,null],"object_types":["96 U-bottom Well Plate","96 Well Flat Bottom (black)","24 Unit Disorganized Collection"],"part":false,"array":false,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Calibration Required?","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Yes,No"},{"ftype":"string","role":"input","name":"Keep Output Plate?","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Yes,No"},{"ftype":"string","role":"input","name":"When to Measure? (24hr)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 08/07/19\n\nneeds \"High Throughput Culturing/FlowCytometryHelper\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\nneeds \"Standard Libs/Debug\"\n\nclass Protocol\n include Debug\n include FlowCytometryHelper, HighThroughputHelper \n \n # DEF\n INPUT = \"Experimental Plate\"\n OUTPUT = \"Measurement Plate\"\n KEEP_OUT_PLT = \"Keep Output Plate?\"\n WHEN_TO_MEASURE = \"When to Measure? (24hr)\"\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def main\n fc = intro\n operations.each do |op|\n op = Operation.find(219542) if debug\n fc.setup_experimental_measurement(experimental_item: op.input(INPUT).item, output_fv: op.output(OUTPUT))\n setup_instrument_software(instrument=fc)\n empty_containers = []; new_materials = []; take_items = [fc.experimental_item]\n (empty_containers.push(fc.measurement_item); new_materials.push('P1000 Multichannel', 'Area Seal')) unless !fc.transfer_required\n gather_materials(empty_containers: empty_containers, transfer_required: fc.transfer_required, new_materials: new_materials, take_items: take_items)\n (fc.transfer_required) ? tech_transfer_to_valid_container(instrument: fc, output_fieldValue: op.output(OUTPUT)) : op.pass(INPUT, OUTPUT)\n take_measurement_and_upload_data(instrument: fc)\n process_and_associate_data(instrument: fc, op: op)\n # Keep new measurement plate that was created?\n keep_transfer_plate(instrument: fc, user_val: get_parameter(op: op, fv_str: KEEP_OUT_PLT).to_s.upcase)\n end\n release_arr = Protocol.materials_list.flatten.reject {|m| m.is_a? String }.uniq\n cleaning_up(release_arr)\n end # Main\nend # Protocol\n","precondition":"def precondition(_op)\n if _op.input('require calibration?').value.downcase == 'yes'\n calibration_operation_type = OperationType.find_by_name(\"Flow Cytometry Calibration\")\n calibration_op = _op.plan.operations.find { |op| op.operation_type_id == calibration_operation_type.id}\n if calibration_op.nil?\n _op.associate('Waiting for Calibration','In order to use Cytometer, `Cytometer Bead Calibration` must be run in the same plan')\n return false\n elsif calibration_op.status != 'done'\n _op.associate(\"Waiting for Calibration\",\"Flow Cytometry cannot begin until Cytometer Calibration completes.\")\n return false\n else\n _op.get_association('Waiting for Calibration').delete if _op.get_association('Waiting for Calibration')\n return true\n end\n else\n return true\n end\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will instruct you on how to take measurements on the #{flow_cytometer.type} Flow Cytometer.\nA flow cytomter uses lasers to phenotypically characterize a microbial culture.\nThis per cell measurement quantifies a cell's size, shape, and color. Making it a useful tool to analyze \u0026 distiguish cellular populations from each other.\nIn this protocol, you will prepare the instrument workspace and characterize your genetically modified organism.\n\n 1. Setup flow_cytometer.type Flow Cytomter Software workspace.\n \n 2. Check to see if input item is a flow_cytometer.valid_containers if not, transfer samples to a valid container.\n \n 3. Load plate.\n \n 4. Take measurement, export data, \u0026 upload.","test":"","timing":null}},{"sample_types":[{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00","field_types":[{"id":18,"parent_id":5,"name":"Parent","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-25T08:43:19.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":8,"field_type_id":18,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]},{"id":19,"parent_id":5,"name":"Integrant","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":9,"field_type_id":19,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":10,"field_type_id":19,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid","Fragment"],"object_types":[null,null]},{"id":20,"parent_id":5,"name":"Plasmid","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":11,"field_type_id":20,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid"],"object_types":[null]},{"id":21,"parent_id":5,"name":"Integrated Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":22,"parent_id":5,"name":"Plasmid Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":23,"parent_id":5,"name":"Mating Type","ftype":"string","choices":"MATa,MATalpha,Diploid","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-10T09:05:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":24,"parent_id":5,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":12,"field_type_id":24,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":25,"parent_id":5,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":13,"field_type_id":25,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":43,"parent_id":5,"name":"QC_length","ftype":"number","choices":"","array":false,"required":false,"created_at":"2016-05-20T14:29:39.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":44,"parent_id":5,"name":"Comp_cell_limit","ftype":"string","choices":"Yes,No","array":false,"required":false,"created_at":"2016-05-20T14:35:37.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":83,"parent_id":5,"name":"Media","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2017-07-12T19:25:25.000-07:00","updated_at":"2017-07-12T19:25:25.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":954,"parent_id":5,"name":"Has this strain passed QC?","ftype":"string","choices":"No,Yes","array":false,"required":false,"created_at":"2017-10-31T11:39:27.000-07:00","updated_at":"2017-10-31T20:14:46.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6455,"parent_id":5,"name":"Haploids","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2669,"field_type_id":6455,"sample_type_id":5,"object_type_id":null,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]}]},{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":4,"parent_id":2,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:30:08.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5,"parent_id":2,"name":"Sequence Verification","ftype":"url","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6,"parent_id":2,"name":"Bacterial Marker","ftype":"string","choices":"Amp,Kan,Amp + Kan,Spec,Kan + Spec,Chlor,Tet,NA,Other","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-12-27T11:26:31.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7,"parent_id":2,"name":"Yeast Marker","ftype":"string","choices":"HIS,TRP,URA,LEU,NatMX,KanMX,HygMX,BleoMX,5FOA,NA,Other","array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-02-13T10:49:58.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":27,"field_type_id":7,"sample_type_id":1,"object_type_id":null,"created_at":"2017-02-13T10:49:58.000-08:00","updated_at":"2017-02-13T10:49:58.000-08:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":8,"parent_id":2,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":42,"parent_id":2,"name":"Sequencing Primers","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":26,"field_type_id":42,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":46,"parent_id":2,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":29,"field_type_id":46,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":47,"parent_id":2,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":30,"field_type_id":47,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":48,"parent_id":2,"name":"QC_length","ftype":"number","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5816,"parent_id":2,"name":"Transformation Temperature","ftype":"number","choices":"37,30","array":false,"required":false,"created_at":"2018-09-17T15:02:22.000-07:00","updated_at":"2018-11-06T11:31:23.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":1,"parent_id":1,"name":"Overhang Sequence","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":2,"parent_id":1,"name":"Anneal Sequence","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3,"parent_id":1,"name":"T Anneal","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":11,"parent_id":4,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":12,"parent_id":4,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":13,"parent_id":4,"name":"Template","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2,"field_type_id":13,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":3,"field_type_id":13,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":4,"field_type_id":13,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":5,"field_type_id":13,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}},{"id":2547,"field_type_id":13,"sample_type_id":70,"object_type_id":null,"created_at":"2018-09-18T16:02:54.000-07:00","updated_at":"2018-09-18T16:02:54.000-07:00","sample_type":{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00"}}],"sample_types":["Plasmid","E coli strain","Fragment","Yeast Strain","DNA Library"],"object_types":[null,null,null,null,null]},{"id":14,"parent_id":4,"name":"Forward Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":6,"field_type_id":14,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2664,"field_type_id":14,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":15,"parent_id":4,"name":"Reverse Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":7,"field_type_id":15,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2665,"field_type_id":15,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":16,"parent_id":4,"name":"Restriction Enzyme(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":17,"parent_id":4,"name":"Yeast Marker","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6423,"parent_id":4,"name":"Fragment Mix Array","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-24T11:05:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2667,"field_type_id":6423,"sample_type_id":1,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2668,"field_type_id":6423,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]}]},{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":10,"parent_id":3,"name":"Parent","ftype":"sample","choices":null,"array":null,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1,"field_type_id":10,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["E coli strain"],"object_types":[null]}]},{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00","field_types":[{"id":3965,"parent_id":70,"name":"Oligo Pool","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1772,"field_type_id":3965,"sample_type_id":75,"object_type_id":null,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","sample_type":{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00"}}],"sample_types":["Oligo Pool"],"object_types":[null]}]},{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00","field_types":[{"id":3947,"parent_id":75,"name":"Manufacturer","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3948,"parent_id":75,"name":"Oligo Library ID","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3949,"parent_id":75,"name":"inner forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1763,"field_type_id":3949,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3950,"parent_id":75,"name":"inner reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1764,"field_type_id":3950,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3951,"parent_id":75,"name":"sublibrary forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1765,"field_type_id":3951,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3952,"parent_id":75,"name":"sublibrary reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1766,"field_type_id":3952,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3953,"parent_id":75,"name":"min length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3954,"parent_id":75,"name":"max length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3955,"parent_id":75,"name":"variants (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3964,"parent_id":75,"name":"sublibrary name (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2018-06-05T14:37:16.000-07:00","updated_at":"2018-06-05T14:37:16.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7477,"parent_id":75,"name":"forward priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7478,"parent_id":75,"name":"reverse priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]}],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":855,"name":"24 Unit Disorganized Collection","description":"A collection of timepoint samples with 15 possible slots","min":0,"max":10000000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2020-02-06T10:32:51.000-08:00","updated_at":"2020-02-06T13:32:51.000-08:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":3,"sample_type_name":null},{"id":350,"name":"Yeast Plate","description":"Yeast Plate","min":0,"max":100000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:04:32.000-08:00","updated_at":"2015-03-02T10:44:55.000-08:00","unit":"Yeast Strain","cost":1.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"DFP","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":214,"name":"Yeast Glycerol Stock","description":"A 1.8mL culture of half saturated yeast overnight and half 50% glycerol stored in the -80C freezer","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2013-10-16T15:07:47.000-07:00","updated_at":"2014-08-12T18:35:37.000-07:00","unit":"Yeast Strain","cost":5.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"M80","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":365,"name":"E coli Plate of Plasmid","description":"A plate containing E. coli transformed with a plasmid","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-25T17:05:34.000-08:00","updated_at":"2015-03-02T10:45:33.000-08:00","unit":"Plasmid","cost":0.01,"release_method":"return","release_description":"","sample_type_id":2,"image":"","prefix":"DFP","rows":null,"columns":null,"sample_type_name":"Plasmid"},{"id":210,"name":"E coli Glycerol Stock","description":"Long term storage of E. coli in the -80C","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2013-10-15T10:19:08.000-07:00","updated_at":"2014-01-02T09:34:13.000-08:00","unit":"E coli strain","cost":1000.0,"release_method":"return","release_description":"","sample_type_id":3,"image":"","prefix":"M80","rows":null,"columns":null,"sample_type_name":"E coli strain"},{"id":347,"name":"Yeast Overnight Suspension","description":"Yeast Overnight Suspension","min":0,"max":1000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:02:38.000-08:00","updated_at":"2017-10-23T13:35:19.000-07:00","unit":"Yeast Strain","cost":1.0,"release_method":"query","release_description":"","sample_type_id":5,"image":"","prefix":"DFO","rows":null,"columns":null,"sample_type_name":"Yeast Strain"}],"operation_type":{"name":"Inoculate Culture Plate","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"output","name":"Culture Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","24 Unit Disorganized Collection"],"part":false,"array":true,"routing":"P","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"input","name":"Culture Condition","sample_types":["Yeast Strain","Yeast Strain","Plasmid","E coli strain","Yeast Strain"],"object_types":["Yeast Plate","Yeast Glycerol Stock","E coli Plate of Plasmid","E coli Glycerol Stock","Yeast Overnight Suspension"],"part":false,"array":true,"routing":"CC","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"json","role":"input","name":"Option(s)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"number","role":"input","name":"Temperature (°C)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez\n# [email protected]\n# 06/26/19\n\nneeds \"Standard Libs/Debug\"\nneeds \"High Throughput Culturing/CultureComposition\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\n\nclass Protocol\n include Debug\n include HighThroughputHelper\n \n # DEF\n INPUT = \"Culture Condition\"\n OUTPUT = \"Culture Plate\"\n TEMPERATURE = \"Temperature (°C)\"\n \n # Predcessor DEF\n STRAIN = \"Strain\"\n MEDIA = \"Media\"\n INDUCERS = \"Inducer(s)\"\n ANTIBIOTICS = \"Antibiotic(s)\"\n CONTROL = \"Control Tag\"\n REPLICATES = \"Replicates\"\n OPTIONS = \"Option(s)\"\n\n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def intro\n show do\n title \"High Throughput Culturing\"\n separator\n note \"This protocol, organizes a culturing experiment into a high throughput container.\"\n note \"The culturing could be very complex with additional inducers and reagents required to test experimental conditions.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Gather materials for experiment.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Fill and inoculate the container.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Place plate in growth environment.\"\n end\n end\n \n def main\n intro\n clean_up_arr = []\n operations.group_by {|op| get_uninitialized_output_object_type(op) }.map do |out_ot, ops|\n ops.map do |op|\n op = Operation.find(219536) if debug\n experimental_cultures = []; control_cultures = []\n condition_ops = get_define_culuture_condition_ops(op) # Predecessor operations\n condition_ops.map do |condition_op|\n control_tag = get_control_tag(condition_op)\n replicate_num = get_replicate_num(condition_op)\n condition_options = get_condition_options(condition_op)\n culture_component_arr = get_base_culture_components(condition_op)\n # Format inducer components to account for combintorial inducer conditions, prior to initializing CultureComposition\n formatted_inducer_components = format_induction_components(condition_op)\n # Arrange component array by combining the culture component arr with the varying inducer components, unless we do not need inducers\n distribute_inducer_components(culture_component_arr: culture_component_arr, formatted_inducer_components: formatted_inducer_components).each do |component_arr|\n culture = CultureComposition.new(component_arr: component_arr, object_type: out_ot, opts: condition_options)\n culture.composition.merge!(control_tag)\n culture.composition.merge!(get_source_item_input(culture))\n replicates = replicate_culture(culture: culture, replicate_num: replicate_num) \n (control_tag.fetch('Control').empty?) ? experimental_cultures.push(replicates) : control_cultures.push(replicates)\n end\n end\n # Place sorted cultures into new collection \u0026 set the new collections to the ouput array of the operation\n new_output_collections = associate_cultures_to_collection(cultures: experimental_cultures, object_type: out_ot)\n output_array = op.output_array(OUTPUT)\n new_output_collections.each_with_index do |out_collection, idx|\n associate_controls_to_collection(cultures: control_cultures, collection: out_collection)\n if output_array[idx].nil? # If there are no output field values left to fill create a new one and add it to the output array\n new_fv = create_new_fv(args=get_fv_properties(output_array[idx-1]))\n output_array.push(new_fv)\n end\n output_array[idx].set collection: out_collection\n end\n # Depending on the type of input items prepare inoculates using the inoculation_prep_hash\n inoculation_prep_hash = get_inoculation_prep_hash(new_output_collections)\n inoculate_culture_plates(new_output_collections: new_output_collections, inoculation_prep_hash: inoculation_prep_hash)\n incubate_plates(output_collections: new_output_collections, growth_temp: op.input(TEMPERATURE).val)\n end\n end\n clean_up(item_arr: clean_up_arr.flatten.uniq)\n { operations: operations.map {|op| op } }\n end # Main\n \n # Based on the user defined number of replicates, create an array of culture composition hash objects\n #\n # @params culture [class CultureComposition] is an instance of the CultureComposition class that represents a microbial culture with intended experimental conditions\n # @params replicate_num [int] the number of replicates for this culture experimental condition\n # @returns [Array] an array of hash objects representing exprimental culture replicates\n def replicate_culture(culture:, replicate_num:)\n return replicate_num.times.map {|i| culture.composition.merge({'Replicate'=\u003e\"#{i+1}/#{replicate_num}\"}) }\n end \n \n # Find the inoculum item of a culture\n #\n # @params culture [class CultureComposition] is an instance of the CultureComposition class that represents a microbial culture with intended experimental conditions\n # @returns [hash] source array hash object\n def get_source_item_input(culture)\n source_array = []\n source_array.push({id: culture.composition.fetch(STRAIN).values.first[:item_id]})\n return { 'source'=\u003e source_array }\n end\n \n # Create an array of predessor operations that are wired to the input array of this Inoculate Culture Plate operation\n def get_define_culuture_condition_ops(op)\n predecessor_output_fv_ids = op.input_array(INPUT).map {|fv| fv.wires_as_dest.first.from_id } # Finding the pred fv ids by using the wires connecting them to this op.input_array\n define_culture_condition_ops = FieldValue.find(predecessor_output_fv_ids).to_a.map {|fv| fv.operation } # Predecessor operations\n return define_culture_condition_ops\n end\n \n # Get predessor operation input OPTIONS\n def get_condition_options(condition_op)\n condition_op.input(OPTIONS).val\n end\n \n # Get predessor operation input REPLICATES\n def get_replicate_num(condition_op)\n condition_op.input(REPLICATES).val.to_i\n end\n \n # Get predessor operation input CONTROL\n def get_control_tag(condition_op)\n { 'Control' =\u003e condition_op.input(CONTROL).val }\n end\n \n # Get predessor operation input STRAIN, MEDIA, ANTIBIOTICS\n def get_base_culture_components(condition_op)\n base_component_fvs = condition_op.field_values.select {|fv| ([STRAIN, MEDIA, ANTIBIOTICS].include? fv.name) \u0026\u0026 (fv.role == 'input') }\n culture_component_arr = base_component_fvs.map {|fv| FieldValueParser.get_args_from(obj: fv) }.flatten.reject {|component| component.empty? }\n return culture_component_arr\n end\n \n # Uses class methods from module FieldValueParser to parse out inducer combination JSON parameters\n def format_induction_components(condition_op)\n inducer_fv = get_inducer_fieldValue(condition_op)\n formatted_inducer_components = FieldValueParser.get_args_from(obj: inducer_fv)\n end\n \n # Get predessor operation input INDUCERS\n def get_inducer_fieldValue(condition_op)\n condition_op.field_values.select {|fv| fv.name == INDUCERS }.first\n end\n \n # Generate inducer component objects to create the combinatorial induction conditions for multiplexed(2 or more) inducers\n def induction_culture_component_generator(formatted_inducer_components, \u0026block)\n formatted_inducer_components.each {|inducer_component| yield inducer_component } \n end\n \n # Create an array of culture condition objects, to instatiate class CultureComposition. Generate an array, with base culture conditions, for each formatted inducer combination.\n #\n # @params culture_component_arr [Array] array of hash objects each representing a component of a experimental microbial culture\n # @params formatted_inducer_components [Array] array of hash objects each representing an inducer component. An inducer component can be comprised of more than one inducer.\n # @returns culture_condition_arr [2-D Array] an array of arrays. The rows in this matrix represent experimental cultures, these arrays are used to instantiate class CultureComposition\n def distribute_inducer_components(culture_component_arr:, formatted_inducer_components:)\n culture_condition_arr = []\n if formatted_inducer_components\n induction_culture_component_generator(formatted_inducer_components) {|inducer_component| culture_condition_arr.push(culture_component_arr.dup.push(inducer_component).flatten) }\n else\n culture_condition_arr.push(culture_component_arr.dup)\n end\n return culture_condition_arr\n end\n \n # Copy the properties of a given field value. This is used to generate and plan more output FieldValues if not enough output culture plates are planned.\n #\n # @params fv [FieldValue] the input or output of an operation. The green or orange bubbles seen in the designer GUI.\n def get_fv_properties(fv)\n {\n name: fv.name,\n role: fv.role,\n field_type_id: fv.field_type_id,\n allowable_field_type_id: fv.allowable_field_type_id,\n parent_class: fv.parent_class,\n parent_id: fv.parent_id\n }\n end\n \n # Instantiate a new FieldValue\n # \n # @params args [Hash] hash object with FieldValue properties needed to duplicate a FieldValue\n def create_new_fv(args={})\n fv = FieldValue.new()\n (args) ? set_fv_properties(fv, args) : nil\n fv.save()\n return fv\n end\n \n # Set the properties of a FieldValue from a hash object\n def set_fv_properties(fv, args={})\n args.each {|k,v| fv[k] = v }\n fv.save()\n return fv\n end\n \nend # Protocol","precondition":"# use the precondition to determine how many output fv need to be planned in the output array of this operation. \n# This precondition could be used to ensure that the user has planned enough output collections\ndef precondition(_op)\n true\nend\n","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol, organizes a culturing experiment into a high throughput container.\nThe culturing could be very complex with additional inducers and reagents required to test experimental conditions.\n\n 1. Gather materials for experiment.\n \n 2. Fill and inoculate the container.\n \n 3. Place plate in growth environment.","test":"class ProtocolTest \u003c ProtocolTestBase\n\n def setup\n\n add_random_operations(3) # defines three random operations\n\n add_operation # adds a custom made operation\n .with_input(\"Primer\", Sample.find(3))\n .with_property(\"x\", 123)\n .with_output(\"Primer\", Sample.find(3))\n\n end\n\n def analyze\n log \"Hello from Nemo\"\n assert_equal @backtrace.last[:operation], \"complete\"\n end\n\nend","timing":null}},{"sample_types":[],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":797,"name":"96 Well PCR Plate","description":"A 96 Well plate that can fit into a thermocycler.","min":0,"max":1,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2018-08-03T18:10:19.000-07:00","updated_at":"2018-08-03T18:49:13.000-07:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":"Yeast Strain"}],"operation_type":{"name":"Make Glycerol Stock Plates","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Culture Plate","sample_types":[null],"object_types":["Eppendorf 96 Deepwell Plate"],"part":false,"array":false,"routing":"P","preferred_operation_type_id":649,"preferred_field_type_id":7822,"choices":null},{"ftype":"sample","role":"output","name":"Glycerol Stock Plate","sample_types":[null],"object_types":["96 Well PCR Plate"],"part":false,"array":true,"routing":"GP","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"number","role":"input","name":"Delete Input Plate","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Yes, No"}],"protocol":"needs \"Standard Libs/Debug\"\nneeds \"Standard Libs/Units\"\nneeds \"Tissue Culture Libs/CollectionDisplay\"\nneeds \"High Throughput Culturing/HighThroughputHelper\"\nneeds \"High Throughput Culturing/CultureComposition\"\n\nclass Protocol\n include Debug, Units\n include CollectionDisplay\n include HighThroughputHelper\n \n # DEF \n INPUT = \"Culture Plate\"\n OUTPUT = \"Glycerol Stock Plate\"\n INPUT_DELETE = \"Delete Input Plate\"\n \n # Constants\n GLYCEROL_PER_WELL = 20#ul\n CULTURE_PER_WELL = 20#ul\n \n def intro\n show do \n title \"Making Glycerol Stock Plates\"\n separator\n note \"This protocol will show you how to prepare glycerol stock plates in a High Throughput Plate.\"\n note \"This operation also transfers the culture conditions and an experimental materials list for that given collection of glycerol stocks.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Gather materials and label new containers.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Pre-fill collection with glycerol and resuspend culture at a 1:1 ratio.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Store plates at -80#{DEGREES_C}\"\n end\n end\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def main\n intro\n operations.make\n operations.each do |op| \n input_collection = op.input(INPUT).collection\n # stamp transfer part data from input collection to all of the collections in the output array\n output_glycerol_stock_plates = op.output_array(OUTPUT).collections\n output_glycerol_stock_plates.each do |glycerol_stock_plate|\n part_data_matrix = stamp_transfer(from_collection: input_collection, to_collection: glycerol_stock_plate, process_name: 'Aliquot')\n glycerol_stock_plate.associate(key='experimental_materials_list', value=get_experimental_materials_list(part_data_matrix))\n gather_materials(empty_containers: [glycerol_stock_plate], transfer_required: false, new_materials: [\"50% Glycerol\", \"Aluminum Adhesive Seals\"], take_items: [])\n prepare_seal(glycerol_stock_plate)\n prepare_multichannel_stripwell(glycerol_stock_plate)\n pre_fill_glycerol_stock_plate(glycerol_stock_plate)\n transfer_culture_and_inoculate(input_collection: input_collection, glycerol_stock_plate: glycerol_stock_plate)\n glycerol_stock_plate.location = \"-80#{DEGREES_C} Freezer\"\n glycerol_stock_plate.save\n end\n if op.input(INPUT_DELETE) == \"Yes\"\n input_collection.mark_as_deleted # TODO: Add keep plate parameter!!\n end\n end\n cleaning_up\n {operations: operations}\n end # main\n \n def cleaning_up\n show do\n title \"Cleaning Up...\"\n separator\n note \"Make sure trash is placed in the proper disposal.\"\n note \"Make sure that #{operations.map {|op| op.inputs[0].collection.id}} plates are placed in the sink and soaked with bleach.\"\n end\n operations.store\n end\n \n def transfer_culture_and_inoculate(input_collection:, glycerol_stock_plate:)\n show do \n title \"Transfer Cultures from #{input_collection} to #{glycerol_stock_plate}\"\n separator\n warning \"Make sure both plates are in the same orientation.\"\n note \"Transfer and resuspend, #{CULTURE_PER_WELL}#{MICROLITERS} per culture.\"\n note \"\u003cb\u003eFrom #{input_collection} #{input_collection.object_type.name}\"\n note \"\u003cb\u003eTo #{glycerol_stock_plate} #{glycerol_stock_plate.object_type.name}\"\n bullet \"Resuspend 5 times\"\n note \"Take a labeled aluminum adhesive and use it to seal the #{glycerol_stock_plate.object_type.name} #{glycerol_stock_plate.id}.\"\n check \"Finally, set aside until all plates are prepared.\"\n end\n end\n\n def pre_fill_glycerol_stock_plate(glycerol_stock_plate)\n show do\n title \"Fill #{glycerol_stock_plate} #{glycerol_stock_plate.object_type.name}\"\n separator\n note \"Follow the table below to pre-fill the glycerol stock plate with \u003cb\u003e50% glycerol\u003c/b\u003e:\"\n bullet \"Use a multichannel pipette where convenient\"\n table highlight_alpha_non_empty(glycerol_stock_plate) {|r,c| \"#{GLYCEROL_PER_WELL}#{MICROLITERS}\" }\n end\n end\n \n def prepare_seal(glycerol_stock_plate)\n show do\n title \"Prepare Label\"\n separator\n check \"Take an Aluminum Seal\"\n check \"Label the seal with the following:\"\n bullet \"GSP #{glycerol_stock_plate.id}\"\n bullet \"Today's Date\"\n bullet \"Your Initials\"\n check \"Set aside until you are ready to seal the prepared plate.\"\n end\n end\n \n def get_total_glycerol_volume_ul(collection)\n HighThroughputHelper.add_extra_vol(int: collection.get_non_empty.length*GLYCEROL_PER_WELL)\n end\n \n def get_total_glycerol_volume_ml(glycerol_ul)\n (glycerol_ul/1000.0).round(2)\n end\n \n def prepare_multichannel_stripwell(collection)\n sw, aliquot_matrix, rc_list = multichannel_vol_stripwell(collection)\n show do\n title \"Prepare Multichannel Stripwell\"\n separator\n check \"Gather a 12-Well Stripwell \u0026 Holder\"\n check \"Gather a P100 or P200 Pipette\"\n check \"You will need #{get_total_glycerol_volume_ml(get_total_glycerol_volume_ul(collection))}#{MILLILITERS} of \u003cb\u003e50% glycerol\u003c/b\u003e.\"\n note \"Next, follow the table below to fill the appropriate amount of \u003cb\u003e50% glycerol\u003c/b\u003e into each well of the stripwell:\"\n table highlight_alpha_rc(sw, rc_list) {|r, c| \"#{ HighThroughputHelper.add_extra_vol(int: aliquot_matrix[r][c]*GLYCEROL_PER_WELL).round(2) }#{MICROLITERS}\" }\n bullet \"Give a quick spindown to avoid air bubbles!\"\n check \"Gather a P20 Multichannel Pipette\"\n end\n sw.mark_as_deleted\n end\n \n # Used for multichannel pipetting, creates a stripwell to display with the number of aliquots of the desired reagents\n #\n # @params collection [colleciton obj] is the collection that you be aliquoting reagent to\n # @returns sw [collection obj] the stripwell obj that will be used to display\n # @returns aliquot_matrix [2D-Array] is the matrix that contains the information of how many aliquots of reagents go in each well\n # @returns rc_list [Array] is a list of [r,c] tuples that will be used to display which wells are to be used for aliquots\n def multichannel_vol_stripwell(collection)\n # Create a stripwell to display\n sw = make_stripwell\n # Create a matrix the size of the stripwell\n aliquot_matrix = Array.new(sw.object_type.rows) { Array.new(sw.object_type.columns) {0} }\n collection.get_non_empty.each {|r, c| aliquot_matrix[0][c] += 1}\n rc_list = []\n aliquot_matrix.each_with_index {|row, r_i| row.each_with_index {|col, c_i| (col == 0) ? EMPTY : rc_list.push([r_i, c_i]) } }\n return sw, aliquot_matrix, rc_list\n end \n \n def make_stripwell\n sw_obj_type = ObjectType.find_by_name('stripwell')\n sw = Collection.new()\n sw.object_type_id = sw_obj_type.id\n sw.apportion(sw_obj_type.rows, sw_obj_type.columns)\n sw.quantity = 1\n return sw\n end\n \n \n def get_experimental_materials_list(part_data_matrix)\n materials_hash = nested_hash_data_structure\n part_data_matrix.flatten.reject {|composition| composition.nil? }.each do |composition|\n composition.each do |component_type, sample_attr|\n if ['Media', 'Inducer(s)', 'Antibiotic(s)'].include? component_type\n sample_attr.each do |sname, attributes|\n item_id = attributes.fetch(:item_id)\n wk_volume = attributes.fetch(:working_volume)\n ot_name = Item.find(item_id).object_type.name\n if materials_hash[component_type][sname][ot_name].empty?\n materials_hash[component_type][sname][ot_name] = wk_volume\n else\n current_wk_volume = materials_hash[component_type][sname][ot_name]\n if current_wk_volume.fetch(:units) == wk_volume.fetch(:units)\n wk_volume[:qty] = (current_wk_volume.fetch(:qty) + wk_volume.fetch(:qty)).round(3)\n materials_hash[component_type][sname][ot_name] = wk_volume\n else\n # TODO: Consider how to handle different units different units\n # if (1/CultureComponent.unit_conversion_hash[current_wk_vol.fetch(:units)]) \u003c (1/CultureComponent.unit_conversion_hash[wk_volume.fetch(:units)])\n raise \"The current working volume #{current_wk_volume} does not have the same units as the new additional working volume #{wk_volume}\"\n end\n end\n end\n end\n end\n end\n return materials_hash\n end \nend # protocol","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will show you how to prepare glycerol stock plates in a High Throughput Plate.\nThis operation also transfers the culture conditions and an experimental materials list for that given collection of glycerol stocks.\n\n 1. Gather materials and label new containers.\n \n 2. Pre-fill collection with glycerol and resuspend culture at a 1:1 ratio.\n \n 3. Store plates at -80#{DEGREES_C}","test":"","timing":null}},{"sample_types":[{"id":71,"name":"Plate Reader Calibration Solution","description":"Solutions for plate reader calibration","created_at":"2018-03-05T15:23:02.000-08:00","updated_at":"2018-03-05T15:23:02.000-08:00","field_types":[]}],"object_types":[{"id":759,"name":"1X LUDOX Aliquot","description":"An aliquot of LUDOX solution contains silica resuspended in H2O.","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2018-03-05T15:26:56.000-08:00","updated_at":"2018-03-05T15:26:56.000-08:00","unit":"Plate Reader Calibration Solution","cost":0.01,"release_method":"return","release_description":"","sample_type_id":71,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Plate Reader Calibration Solution"},{"id":758,"name":"1mM Fluorescein Stock","description":"Fluorescein sodium diluted in 1XPBS","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2018-03-05T15:26:13.000-08:00","updated_at":"2018-03-05T15:26:13.000-08:00","unit":"Plate Reader Calibration Solution","cost":0.01,"release_method":"return","release_description":"","sample_type_id":71,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Plate Reader Calibration Solution"},{"id":730,"name":"96 Well Flat Bottom (black)","description":"plate for plate reader","min":0,"max":1000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"max_well_vol_ul\" : 300,\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2017-11-14T10:54:03.000-08:00","updated_at":"2019-08-14T11:13:34.000-07:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null}],"operation_type":{"name":"Plate Reader Calibration","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Optical Particles","sample_types":["Plate Reader Calibration Solution"],"object_types":["1X LUDOX Aliquot"],"part":false,"array":false,"routing":"OP","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"input","name":"Fluorescent Salt","sample_types":["Plate Reader Calibration Solution"],"object_types":["1mM Fluorescein Stock"],"part":false,"array":false,"routing":"FS","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"output","name":"Calibration Plate","sample_types":[null],"object_types":["96 Well Flat Bottom (black)"],"part":false,"array":false,"routing":"PR","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez 03/14/19\n# [email protected]\n\nneeds \"High Throughput Culturing/PlateReaderHelper\"\nclass Protocol\n include PlateReaderHelper\n \n # DEF\n OD_CALIBRATION = \"Optical Particles\"\n FLOUR_CALIBRATION = \"Fluorescent Salt\"\n OUTPUT = \"Calibration Plate\"\n \n # Constants\n MEASUREMENT_TYPE = 'Calibration'\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n def main # Typically one calibration op per Plan\n pr = intro\n new_mtype = true\n pr.measurement_type = MEASUREMENT_TYPE\n operations.group_by {|op| op.input(FLOUR_CALIBRATION).item }.each do |flour_item, ops|\n ops.group_by {|op| op.input(OD_CALIBRATION).item }.each do |optical_item, ops|\n # Based on whether an unexpired calibration plate is made, prepare a calibration plate for measurement\n new_mtype = prep_calibration_plate(pr, ops, OUTPUT, flour_item, optical_item)\n take_measurement_and_upload_data(pr: pr)\n process_and_associate_data(pr: pr, ops: ops) \n change_item_location(item: pr.measurement_item, location: \"4#{DEGREES_C} Fridge\")\n end\n end\n cleaning_up(pr: pr)\n end # Main\nend # Protocol\n\n\n\n\n","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will instruct you on how to take measurements to calibrate and compare measurements on the plate_reader.type Plate Reader.\nThis protocol will also guide you through preparing the calibration plate input item or gathering an unexpired one.\n\n 1. Setup plate_reader.type Plate Reader Software workspace.\n \n 2. Check to see if input item is a plate_reader.valid_containers if not, transfer samples to a valid container.\n \n 3. Prepare measurement item with blanks.\n \n 4. Take measurement, export data, \u0026 upload.","test":"","timing":null}},{"sample_types":[{"id":15,"name":"Media","description":"Media for yeast and E. coli","created_at":"2015-12-16T14:13:57.000-08:00","updated_at":"2015-12-16T14:35:10.000-08:00","field_types":[]}],"object_types":[{"id":452,"name":"Eppendorf 96 Deepwell Plate","description":"Eppendorf 96 Deepwell Plate","min":0,"max":100000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"working_vol\": \"1000_uL\", \"max_vol\": \"1.2_mL\" }","vendor":"No vendor information","created_at":"2014-11-09T18:55:06.000-08:00","updated_at":"2019-08-14T20:14:05.000-07:00","unit":"plate","cost":5.0,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":730,"name":"96 Well Flat Bottom (black)","description":"plate for plate reader","min":0,"max":1000,"handler":"collection","safety":"No safety information","cleanup":"No cleanup information","data":"{\"max_well_vol_ul\" : 300,\"working_vol\": \"300_uL\", \"max_vol\": \"0.3_mL\" }","vendor":"No vendor information","created_at":"2017-11-14T10:54:03.000-08:00","updated_at":"2019-08-14T11:13:34.000-07:00","unit":"plate","cost":0.01,"release_method":"return","release_description":"","sample_type_id":null,"image":"","prefix":"","rows":8,"columns":12,"sample_type_name":null},{"id":466,"name":"800 mL Liquid","description":"800 mL Bottle","min":0,"max":10000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":" {\r\n \"samples\": [\r\n { \"name\": \"SC\", \"materials\": 0.0135, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"SDO\", \"materials\": 0.014, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"50% Glycerol\", \"materials\": 0.02, \"labor\": 0.02, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"TB + Amp\", \"materials\": 0.03, \"labor\": 0.028, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 },\r\n { \"name\": \"LB\", \"materials\": 0.01, \"labor\": 0.016, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }, \r\n { \"name\": \"YPAD\", \"materials\": 0.01, \"labor\": 0.025, \"delete\": false, \"unit\": \"mL\", \"total_volume\": 800 }\r\n ]\r\n }","vendor":"No vendor information","created_at":"2015-12-16T14:35:46.000-08:00","updated_at":"2017-06-13T11:30:03.000-07:00","unit":"Media","cost":0.01,"release_method":"return","release_description":"","sample_type_id":15,"image":"","prefix":"","rows":null,"columns":null,"sample_type_name":"Media"}],"operation_type":{"name":"Plate Reader Measurement","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Culturing Plate","sample_types":[null,null],"object_types":["Eppendorf 96 Deepwell Plate","96 Well Flat Bottom (black)"],"part":false,"array":false,"routing":"PR","preferred_operation_type_id":653,"preferred_field_type_id":7878,"choices":null},{"ftype":"sample","role":"output","name":"Plate Reader Plate","sample_types":[null],"object_types":["96 Well Flat Bottom (black)"],"part":false,"array":false,"routing":"PR","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"sample","role":"input","name":"Media","sample_types":["Media"],"object_types":["800 mL Liquid"],"part":false,"array":false,"routing":"M","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Type of Measurement(s)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Optical Density,Green Fluorescence,Optical Density \u0026 Green Fluorescence"},{"ftype":"string","role":"input","name":"Dilution","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"None,0.5X, 0.2X,0.1X, 0.01X,0.001X"},{"ftype":"string","role":"input","name":"Keep Output Plate?","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"Yes,No"},{"ftype":"string","role":"input","name":"When to Measure? (24hr)","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null}],"protocol":"# By: Eriberto Lopez 03/14/19\n# [email protected]\nneeds \"High Throughput Culturing/PlateReaderHelper\"\n\nclass Protocol\n include PlateReaderHelper\n # DEF\n INPUT = \"Culturing Plate\"\n OUTPUT = \"Plate Reader Plate\"\n MEDIA = 'Media'\n DILUTION = \"Dilution\"\n KEEP_OUT_PLT = \"Keep Output Plate?\"\n MEASUREMENT_TYPE = 'Type of Measurement(s)'\n WHEN_TO_MEASURE = \"When to Measure? (24hr)\"\n \n # Access class variables via Protocol.your_class_method\n @materials_list = []\n def self.materials_list; @materials_list; end\n \n # TODO: Get time series measurements online\n def main\n pr = intro\n operations.group_by {|op| op.input(MEASUREMENT_TYPE).val.to_sym }.each do |measurement_type, ops|\n new_mtype = true\n pr.measurement_type = measurement_type\n ops.each do |op|\n op = Operation.find(179078) if debug\n # Use class PlateReader to setup the experimental measurement\n pr.setup_experimental_measurement(experimental_item: op.input(INPUT).item, output_fv: op.output(OUTPUT))\n new_mtype = setup_plate_reader_software_env(pr: pr, new_mtype: new_mtype)\n # Gather materials and items\n media_item = get_media_bottle(op)\n take_items = [pr.experimental_item, media_item].flatten.reject {|i| i.nil?}\n gather_materials(empty_containers: [pr.measurement_item], transfer_required: pr.transfer_required, new_materials: ['P1000 Multichannel'], take_items: take_items )\n # Prepare plate for plate reader\n dilution_factor = get_dilution_factor(op: op, fv_str: DILUTION)\n media_vol_ul, culture_vol_ul = get_culture_and_media_vols(dilution_factor: dilution_factor, measurement_item: pr.measurement_item)\n (pr.transfer_required) ? tech_prefill_and_transfer(pr: pr, media_sample: media_item.sample, media_vol_ul: media_vol_ul, culture_vol_ul: culture_vol_ul) : op.pass(INPUT, OUTPUT)\n tech_add_blanks(pr: pr, blanking_sample: media_item.sample, culture_vol_ul: culture_vol_ul, media_vol_ul: media_vol_ul) # Cannot handle a plate without blanks, esp in processing of upload\n take_measurement_and_upload_data(pr: pr)\n process_and_associate_data(pr: pr, ops: [op], blanking_sample: media_item.sample, dilution_factor: dilution_factor)\n # Keep new measurement plate that was created?\n (pr.transfer_required) ? (keep_transfer_plate(pr: pr, user_val: get_parameter(op: op, fv_str: KEEP_OUT_PLT).to_s.upcase)) : (pr.measurement_item.location = 'Bench')\n end\n end \n cleaning_up(pr: pr)\n end # Main\nend # Protocol\n","precondition":"def precondition(_op)\n true\nend","cost_model":"def cost(_op)\n { labor: 0, materials: 0 }\nend","documentation":"This protocol will instruct you on how to take measurements on the plate_reader.type Plate Reader.\nOptical Density is a quick and easy way to measure the growth rate of your cultures.\nGreen Fluorescence helps researchers assess a response to a biological condition in vivo.\n\n 1. Setup plate_reader.type Plate Reader Software workspace.\n \n 2. Check to see if input item is a plate_reader.valid_containers if not, transfer samples to a valid container.\n \n 3. Prepare measurement item with blanks.\n \n 4. Take measurement, export data, \u0026 upload.","test":"","timing":null}},{"sample_types":[{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00","field_types":[{"id":18,"parent_id":5,"name":"Parent","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-25T08:43:19.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":8,"field_type_id":18,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]},{"id":19,"parent_id":5,"name":"Integrant","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":9,"field_type_id":19,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":10,"field_type_id":19,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid","Fragment"],"object_types":[null,null]},{"id":20,"parent_id":5,"name":"Plasmid","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":11,"field_type_id":20,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Plasmid"],"object_types":[null]},{"id":21,"parent_id":5,"name":"Integrated Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":22,"parent_id":5,"name":"Plasmid Marker(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":23,"parent_id":5,"name":"Mating Type","ftype":"string","choices":"MATa,MATalpha,Diploid","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-10T09:05:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":24,"parent_id":5,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":12,"field_type_id":24,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":25,"parent_id":5,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:23:30.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":13,"field_type_id":25,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":43,"parent_id":5,"name":"QC_length","ftype":"number","choices":"","array":false,"required":false,"created_at":"2016-05-20T14:29:39.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":44,"parent_id":5,"name":"Comp_cell_limit","ftype":"string","choices":"Yes,No","array":false,"required":false,"created_at":"2016-05-20T14:35:37.000-07:00","updated_at":"2016-05-20T14:35:37.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":83,"parent_id":5,"name":"Media","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2017-07-12T19:25:25.000-07:00","updated_at":"2017-07-12T19:25:25.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":954,"parent_id":5,"name":"Has this strain passed QC?","ftype":"string","choices":"No,Yes","array":false,"required":false,"created_at":"2017-10-31T11:39:27.000-07:00","updated_at":"2017-10-31T20:14:46.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6455,"parent_id":5,"name":"Haploids","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2669,"field_type_id":6455,"sample_type_id":5,"object_type_id":null,"created_at":"2018-10-24T19:01:40.000-07:00","updated_at":"2018-10-24T19:01:40.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}}],"sample_types":["Yeast Strain"],"object_types":[null]}]},{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":4,"parent_id":2,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:30:08.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5,"parent_id":2,"name":"Sequence Verification","ftype":"url","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6,"parent_id":2,"name":"Bacterial Marker","ftype":"string","choices":"Amp,Kan,Amp + Kan,Spec,Kan + Spec,Chlor,Tet,NA,Other","array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-12-27T11:26:31.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7,"parent_id":2,"name":"Yeast Marker","ftype":"string","choices":"HIS,TRP,URA,LEU,NatMX,KanMX,HygMX,BleoMX,5FOA,NA,Other","array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2017-02-13T10:49:58.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":27,"field_type_id":7,"sample_type_id":1,"object_type_id":null,"created_at":"2017-02-13T10:49:58.000-08:00","updated_at":"2017-02-13T10:49:58.000-08:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":8,"parent_id":2,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:16:33.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":42,"parent_id":2,"name":"Sequencing Primers","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":26,"field_type_id":42,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T21:30:48.000-07:00","updated_at":"2016-05-09T21:30:48.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":46,"parent_id":2,"name":"QC Primer1","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":29,"field_type_id":46,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":47,"parent_id":2,"name":"QC Primer2","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":30,"field_type_id":47,"sample_type_id":1,"object_type_id":null,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":48,"parent_id":2,"name":"QC_length","ftype":"number","choices":null,"array":false,"required":false,"created_at":"2017-05-24T14:51:40.000-07:00","updated_at":"2017-05-24T14:51:40.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":5816,"parent_id":2,"name":"Transformation Temperature","ftype":"number","choices":"37,30","array":false,"required":false,"created_at":"2018-09-17T15:02:22.000-07:00","updated_at":"2018-11-06T11:31:23.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":1,"parent_id":1,"name":"Overhang Sequence","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":2,"parent_id":1,"name":"Anneal Sequence","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3,"parent_id":1,"name":"T Anneal","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:25:11.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]},{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":11,"parent_id":4,"name":"Sequence","ftype":"url","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":12,"parent_id":4,"name":"Length","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":13,"parent_id":4,"name":"Template","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2,"field_type_id":13,"sample_type_id":2,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":2,"name":"Plasmid","description":"A circular piece of double stranded DNA","created_at":"2013-10-15T09:59:25.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":3,"field_type_id":13,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":4,"field_type_id":13,"sample_type_id":4,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":5,"field_type_id":13,"sample_type_id":5,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":5,"name":"Yeast Strain","description":"A strain of yeast distinguished from others by genomic or plasmid modifications","created_at":"2013-10-16T14:37:29.000-07:00","updated_at":"2015-12-08T17:40:57.000-08:00"}},{"id":2547,"field_type_id":13,"sample_type_id":70,"object_type_id":null,"created_at":"2018-09-18T16:02:54.000-07:00","updated_at":"2018-09-18T16:02:54.000-07:00","sample_type":{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00"}}],"sample_types":["Plasmid","E coli strain","Fragment","Yeast Strain","DNA Library"],"object_types":[null,null,null,null,null]},{"id":14,"parent_id":4,"name":"Forward Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":6,"field_type_id":14,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2664,"field_type_id":14,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":15,"parent_id":4,"name":"Reverse Primer","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-12T19:07:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":7,"field_type_id":15,"sample_type_id":1,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2665,"field_type_id":15,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-22T13:03:32.000-07:00","updated_at":"2018-10-22T13:03:32.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]},{"id":16,"parent_id":4,"name":"Restriction Enzyme(s)","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":17,"parent_id":4,"name":"Yeast Marker","ftype":"string","choices":null,"array":false,"required":false,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T21:17:39.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":6423,"parent_id":4,"name":"Fragment Mix Array","ftype":"sample","choices":null,"array":true,"required":false,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-24T11:05:59.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":2667,"field_type_id":6423,"sample_type_id":1,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}},{"id":2668,"field_type_id":6423,"sample_type_id":4,"object_type_id":null,"created_at":"2018-10-23T13:35:15.000-07:00","updated_at":"2018-10-23T13:35:15.000-07:00","sample_type":{"id":4,"name":"Fragment","description":"A linear double stranded piece of DNA from PCR or Restriction Digest","created_at":"2013-10-16T14:33:41.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer","Fragment"],"object_types":[null,null]}]},{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00","field_types":[{"id":10,"parent_id":3,"name":"Parent","ftype":"sample","choices":null,"array":null,"required":true,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1,"field_type_id":10,"sample_type_id":3,"object_type_id":null,"created_at":"2016-05-09T20:40:31.000-07:00","updated_at":"2016-05-09T20:40:31.000-07:00","sample_type":{"id":3,"name":"E coli strain","description":"A strain of E coli distinguished from others by genomic (not plasmid) modifications.","created_at":"2013-10-15T10:16:52.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["E coli strain"],"object_types":[null]}]},{"id":70,"name":"DNA Library","description":"A sample that contains a pool of DNA molecules with many unique sequences","created_at":"2018-01-02T14:28:12.000-08:00","updated_at":"2018-01-02T14:28:12.000-08:00","field_types":[{"id":3965,"parent_id":70,"name":"Oligo Pool","ftype":"sample","choices":null,"array":false,"required":false,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1772,"field_type_id":3965,"sample_type_id":75,"object_type_id":null,"created_at":"2018-06-05T14:45:01.000-07:00","updated_at":"2018-06-05T14:45:01.000-07:00","sample_type":{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00"}}],"sample_types":["Oligo Pool"],"object_types":[null]}]},{"id":75,"name":"Oligo Pool","description":"Pool or library of ssDNA oligos. May contain one or more sublibraries. In array fields, the n-th position corresponds to n-th sublibrary. \"forward priming site\" and \"reverse priming site\" are read as primer sequences.","created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2019-01-23T16:24:45.000-08:00","field_types":[{"id":3947,"parent_id":75,"name":"Manufacturer","ftype":"string","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3948,"parent_id":75,"name":"Oligo Library ID","ftype":"number","choices":null,"array":false,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-07T09:48:57.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3949,"parent_id":75,"name":"inner forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1763,"field_type_id":3949,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3950,"parent_id":75,"name":"inner reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1764,"field_type_id":3950,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3951,"parent_id":75,"name":"sublibrary forward primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1765,"field_type_id":3951,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3952,"parent_id":75,"name":"sublibrary reverse primer (array)","ftype":"sample","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[{"id":1766,"field_type_id":3952,"sample_type_id":1,"object_type_id":null,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","sample_type":{"id":1,"name":"Primer","description":"A short double stranded piece of DNA for PCR and sequencing","created_at":"2013-10-08T10:18:01.000-07:00","updated_at":"2015-11-29T07:55:20.000-08:00"}}],"sample_types":["Primer"],"object_types":[null]},{"id":3953,"parent_id":75,"name":"min length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3954,"parent_id":75,"name":"max length (nt) (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3955,"parent_id":75,"name":"variants (array)","ftype":"number","choices":null,"array":true,"required":true,"created_at":"2018-06-05T11:30:26.000-07:00","updated_at":"2018-06-05T11:30:26.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":3964,"parent_id":75,"name":"sublibrary name (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2018-06-05T14:37:16.000-07:00","updated_at":"2018-06-05T14:37:16.000-07:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7477,"parent_id":75,"name":"forward priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]},{"id":7478,"parent_id":75,"name":"reverse priming site (array)","ftype":"string","choices":null,"array":true,"required":true,"created_at":"2019-01-23T16:24:45.000-08:00","updated_at":"2019-01-23T16:24:45.000-08:00","parent_class":"SampleType","role":null,"part":null,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"allowable_field_types":[],"sample_types":[],"object_types":[]}]}],"object_types":[{"id":214,"name":"Yeast Glycerol Stock","description":"A 1.8mL culture of half saturated yeast overnight and half 50% glycerol stored in the -80C freezer","min":0,"max":1,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2013-10-16T15:07:47.000-07:00","updated_at":"2014-08-12T18:35:37.000-07:00","unit":"Yeast Strain","cost":5.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"M80","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":347,"name":"Yeast Overnight Suspension","description":"Yeast Overnight Suspension","min":0,"max":1000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:02:38.000-08:00","updated_at":"2017-10-23T13:35:19.000-07:00","unit":"Yeast Strain","cost":1.0,"release_method":"query","release_description":"","sample_type_id":5,"image":"","prefix":"DFO","rows":null,"columns":null,"sample_type_name":"Yeast Strain"},{"id":350,"name":"Yeast Plate","description":"Yeast Plate","min":0,"max":100000,"handler":"sample_container","safety":"No safety information","cleanup":"No cleanup information","data":"No data","vendor":"No vendor information","created_at":"2014-02-13T14:04:32.000-08:00","updated_at":"2015-03-02T10:44:55.000-08:00","unit":"Yeast Strain","cost":1.0,"release_method":"return","release_description":"","sample_type_id":5,"image":"","prefix":"DFP","rows":null,"columns":null,"sample_type_name":"Yeast Strain"}],"operation_type":{"name":"To Single Colonies","category":"High Throughput Culturing","deployed":false,"on_the_fly":false,"field_types":[{"ftype":"sample","role":"input","name":"Yeast Inoculum","sample_types":["Yeast Strain","Yeast Strain"],"object_types":["Yeast Glycerol Stock","Yeast Overnight Suspension"],"part":false,"array":false,"routing":"C","preferred_operation_type_id":266,"preferred_field_type_id":1056,"choices":null},{"ftype":"sample","role":"output","name":"Plate","sample_types":["Yeast Strain"],"object_types":["Yeast Plate"],"part":false,"array":false,"routing":"C","preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":null},{"ftype":"string","role":"input","name":"Media","sample_types":[],"object_types":[],"part":false,"array":false,"routing":null,"preferred_operation_type_id":null,"preferred_field_type_id":null,"choices":"YPAD,SC,SDO,SDO -His,SDO -Leu,SDO -Trp,SDO -Ura,SDO -His -Leu,SDO -His -Trp,SDO -His -Ura,SDO -Leu -Trp,SDO -Leu -Ura,SDO -Trp -Ura,SDO -His -Leu -Trp,SDO -His -Leu -Ura,SDO -His -Trp -Ura,SDO -Leu -Trp -Ura"}],"protocol":"# By: Eriberto Lopez 01/22/18\n# [email protected]\n\nneeds \"Standard Libs/Debug\" \n\nclass Protocol\n include Debug\n \n # DEF\n INPUT = \"Yeast Inoculum\"\n OUTPUT = \"Plate\"\n MEDIA = \"Media\"\n \n \n def main\n intro\n get_plates\n operations.make\n # Grab materials and label plates\n gly_stks, cultures = gather_materials\n streak_plates(gly_stks, cultures)\n # Move new plates to incubator\n plates = operations.running.map { |op| op.output(OUTPUT).item.move \"30C incubator\" }\n release(plates, interactive: true)\n if cultures.empty? == false then discard_cultures(cultures) end\n return {}\n end # Main\n \n # Make a list of all the operations with each unique plate type\n def get_media_types()\n operations.map {|op| op.input(MEDIA).val.to_str }.uniq\n end\n \n def check_media_type_plate_inventory()\n ##The following block will error out any operations for which we don't actually have plates available. \n groupby_media_type = operations.group_by {|op| op.input(MEDIA).val.to_str }\n groupby_media_type.each do |mt, media_ops|\n media_sample = Sample.find_by_name(mt)\n plates_needed = media_ops.length\n batches = Collection.where(object_type_id: ObjectType.find_by_name(\"Agar Plate Batch\").id).select {|b| b.matrix.first.include? media_sample.id } #Find the plate batches\n #Check how many plates of this media type we actually have available, and see how that relates to the number of plates we need. \n plates_available = 0; batches.each {|b| plates_available = plates_available + b.num_samples }\n difference = plates_needed - plates_available\n # If we have a shortfall o plates we need to error out that number of operations. \n if difference \u003e 0\n ops_to_error = media_ops[0..difference]\n ops_to_error.each do |op|\n op.error :plate_needed, \"Not enough agar plates of the required media type available - #{op.input(MEDIA).val.to_str}\"\n end\n #A little note so the technicians understand why there are now fewer operations running\n show do \n title \"More plates required\"\n note \"#{difference} operations have been errored because there are not enough plates of the type #{media_sample.name}\"\n end\n end\n end\n end\n \n def get_plates\n ##The following block will error out any operations for which we don't actually have plates available. \n check_media_type_plate_inventory\n ##Having errored out some operations we need to again check how many plates we're now looking for and send the technician to go get them. \n ##So we're going to update our variables to reflect the fact that we errored out some operations. \n groupby_media_type = operations.running.group_by {|op| Sample.find_by_name(op.input(MEDIA).val.to_s) }\n groupby_media_type.each do |media_sample, media_ops|\n plates_needed = media_ops.length #How many plates do we need of this media type?\n batches = Collection.where(object_type_id: ObjectType.find_by_name(\"Agar Plate Batch\").id).select { |b| b.matrix.first.include? media_sample.id } #Find the plate batches\n # Work out which batches we need to take the plates from and update the inventory to reflect the removed plates. \n # We're also going to keep track of all batches that will be touched to be able to instruct the technicians. \n # This is consdering that we may need to take all plates from one batch and then also a few more from another batch. \n batch_ids = []\n plates_needed.times do \n batch_id = batches.first.id\n batch_ids.push(batch_id.to_s)\n batches.first.remove_one media_sample\n batches = Collection.where(object_type_id: ObjectType.find_by_name(\"Agar Plate Batch\").id).select { |b| b.matrix[0].include? media_sample.id }\n end\n unique_batch_ids = batch_ids.uniq\n #Finally we can instruct the technician to get the plates. \n show do \n title \"Retrieve #{media_sample.name} plates\"\n check \"From the media fridge retrieve #{plates_needed} plates from the tupperware container labeled: #{media_sample.name}, #{unique_batch_ids}\"\n end\n end\n end\n \n def intro()\n container = operations.running.map {|op| op.input(INPUT).object_type.name}.uniq\n img = \"Actions/Yeast_Gates/t_streak_method.png\" #ie: image \"Actions/FlowCytometry/saveFCS_menu_cropped.png\"\n show do \n title \"Introduction\"\n separator\n if container.length == 1\n note \"In this protocol you will isolate single colonies from a #{container.first} using the T-streak technique.\"\n elsif container.length \u003e1\n note \"In this protocol you will isolate single colonies from the object types: #{container} using the T-streak technique.\"\n end\n image img\n end\n end\n \n def gather_materials()\n groupby_input_object_type = operations.running.group_by {|op| op.input(INPUT).item.object_type.name }\n gly_stks = groupby_input_object_type.fetch(\"Yeast Glycerol Stock\", []).map {|op| op.input(INPUT).item }\n cultures = groupby_input_object_type.fetch(\"Yeast Overnight suspension\", []).map {|op| op.input(INPUT).item }\n plate_ids = operations.running.map {|op| op.output(OUTPUT).item.id }\n show do\n title \"Materials for Plate Streaking\"\n separator\n check \"Box of P1000 tips.\"\n check \"Box of P100 tips and P100 pipette\"\n check \"Sharpie Pen\"\n check \"Label plate(s): \u003cb\u003e #{plate_ids}\u003c/b\u003e\"\n end\n return gly_stks, cultures, plate_ids\n end\n \n def streak_plates(gly_stks, cultures)\n img1 = \"Actions/Yeast_Gates/initial_innoculation_new.png\"\n img2 = \"Actions/Yeast_Gates/second_streak_new.png\"\n img3 = \"Actions/Yeast_Gates/third_streak_incubation_new.png\"\n show do\n title \"Inoculation from glycerol stock in M80 area\"\n separator\n check \"Go to M80 area, clean out the pipette tip waste box, clean and sanatize area.\"\n check \"Put on new gloves, and bring a new tip box (blue: 100 - 1000 uL) and an Eppendorf tube rack to the M80 area.\"\n check \"Grab the plates and go to M80 area to perform inoculation steps in the next pages.\"\n image \"Actions/Streak Plates/streak_yeast_plate_setup.JPG\"\n end\n # Sorting glycerol stocks by box and location\n gly_stks.sort! {|x, y| x.location.split('.')[1..3] \u003c=\u003e y.location.split('.')[1..3]} # Sorts by the box number Fridge.X.X.X\n items_grouped_by_box = gly_stks.group_by {|i| i.location.split('.')[1]}\n items_grouped_by_box.each {|box_num, item_arr| # Groups by box then takes by box location\n take item_arr, interactive: true, method: \"boxes\"\n }\n (!cultures.nil?) ? (take cultures, interactive: true) : (nil)\n show do \n title \"Streak Out on Plate\"\n separator\n note \"Streak out the initial innoculation as shown in the example below - a slightly bigger patch is better.\"\n if cultures.empty? == false\n note \"\u003cb\u003eFor liquid cultures\u003c/b\u003e use a pipette to drop 10 µl onto the plate as the initial inoculum\"\n end\n image img1\n note \"Streak out the Yeast Glycerol Stock on the plate(s) according to the following table: \"\n table operations.start_table\n .input_item(INPUT, heading: \"Yeast stock/culture ID\")\n .output_item(OUTPUT, heading: \"Yeast Plate ID\", checkable: true)\n .end_table\n note \"Continue on the the next steps to isolate single colonies.\"\n end\n release(gly_stks,interactive: true)\n show do\n title \"Isolating Single Colonies\"\n separator\n note \"Next, with a new P1000 pipette tip streak out from the initial patch made.\"\n note \"Use the image as an example.\"\n image img2\n note \"Repeat this once more with a clean P1000 tip.\"\n image img3\n end\n end\n \n def discard_cultures(cultures)\n show do\n title \"Discard overnight suspensions\"\n note \"Place the following tubes in the cleaning rack by the sink to be bleached and disposed of\"\n cultures.each do |on|\n check \"#{on.id}\"\n end\n end\n cultures.each do |on|\n on.mark_as_deleted\n on.save\n end\n end\nend # Class\n","precondition":"def precondition(_op)\n true\nend","cost_model":"# def cost(_op)\n# { labor: 0, materials: 0 }\n# end\ndef cost(_op)\n { labor: 1.26, materials: 1.21 }\nend\n\n# 1 whole YPD plate\n# time is 3.78 mins","documentation":"This protocol will guide you in streaking out cells onto an agar plate in order to generated isolated colonies.","test":"","timing":null}},{"library":{"name":"CultureComposition","category":"High Throughput Culturing","code_source":"# By: Eriberto Lopez \n# [email protected]\n# Last Edit Checkpoint: 081419\n\nneeds \"Standard Libs/Units\"\n\n# This class generates a data structure that describes multiple SampleTypes in order to represent an experimental microbial culture. \n# For each instance, it will calculate how much volume of each component is required to generate the desired culture composition.\n# It then merges all components into a single data structure which represents the composition and the experimental intent of the user.\n#\n# @author Eriberto Lopez - [email protected]\n# @since 07/22/19\n# \n# @param component_arr [array] is an array of hashes that comes from formatted 'Define Culture Conditions' FieldValues\n# @param object_type [object] is the ObjectType of the container that will be used for culturing, this is necessary to calculate working volumes for all components in the culture.\n# @param opts [hash] is a hash that is used for prototyping and adding addiitonal functionality without interupting core functions. Generally, not used.\n# \n# @attr [hash] culture_volume is a hash that represents the total culture volume\n# @attr [array of classes] components is an array of instatiated CultureComponent classes. Each with it's default volumes and items.\n# @attr [hash] composition is a hash that represents a culture with multiple samples types or multiple combinations of similar sample types.\n# @attr [hash] options is a hash that is used for prototyping and adding addiitonal functionality without interupting core functions. Generally, not used.\nclass CultureComposition\n \n attr_accessor :culture_volume, :components, :composition, :options\n \n def initialize(component_arr:, object_type:, opts: {})\n @components = component_arr.map {|component| CultureComponent.new(component) }\n @culture_volume = get_container_properties(key: 'working_vol', object_type: object_type)\n @options = opts\n update_culture_components_working_vol # Will calculate how much of each component is needed based on prev calc dilution factor\n @composition = generate_composition_data_structure\n end\n\n def get_component_attr(component)\n {\n component.sample.name =\u003e {\n item_id: component.item.id,\n item_concentration: component.stock_concentration,\n final_concentration: component.final_concentration,\n dilution_factor: component.dilution_factor,\n working_volume: component.working_vol\n }\n }\n end\n \n def generate_composition_data_structure\n data_structure = {}\n components.group_by {|c| c.input_name }.each do |input_name, components|\n components.each do |component|\n if data_structure.keys.include? input_name\n data_structure[input_name].merge!(get_component_attr(component))\n else\n data_structure[input_name] = get_component_attr(component)\n end\n end\n end\n data_structure[\"Option(s)\"] = options\n data_structure[\"Culture_Volume\"] = culture_volume\n return data_structure\n end\n\n def update_culture_components_working_vol\n components.each {|component| get_component_working_vol(component) }\n get_media_working_vol # After all of the additional components have been calculated, take the remainder vol and fill with media\n end\n \n def get_media_working_vol\n total_volume = culture_volume[:qty].to_f\n components.each {|component| total_volume -= component.working_vol.fetch(:qty).to_f }\n media_component = components.select {|c| c.input_name == \"Media\"}.first\n media_component.working_vol[:qty] = total_volume.round(3)\n end\n \n def get_component_working_vol(component)\n df = component.dilution_factor\n if !df.nil?\n component_working_vol_qty = culture_volume[:qty]*df\n component.working_vol = format_measurement(component_working_vol_qty, component.working_vol[:units])\n end\n end\n \n def format_measurement(qty, units)\n {qty: qty.to_f.round(2), units: units}\n end\n \n def get_container_properties(key:, object_type:)\n if object_type.data.include? key\n qty, units = JSON.parse(object_type.data)[key].split('_')\n else\n raise \"The container that you are attempting to grow a culture in does not have a #{key} reference in it's container definition.\n Please update the #{object_type.name} object type definition!\"\n end\n return format_measurement(qty, units)\n end\nend # class CultureComposition\n\n# This class instantiates a formatted culture component for the desired experimental condition. \n# A formatted hash object is used to instantiate this class to represent an experimental culture component. \n# A list of multiple CultureComponent instances is then used by the CultureComposition class, to build up the culture.composition data structure to represent an experimental culture.\n#\n# @author Eriberto Lopez - [email protected]\n# @since 07/22/19\n#\n# @param args [Hash] is a hash object with keys related to the attributes of this class\n#\n# @attr working_vol [Hash] object representing how much volume of a instantiated CultureComponent is required ie: {qty: 100, units: 'microliters'}\n# @attr_reader input_name [string] the name of the Aq sample describing the culture component\n# @attr_reader sample [Sample] the sample of the component\n# @attr_reader item [Item] the item from which the component is take from\n# @attr_reader stock_concentration [Hash] object representing the concentration of the item used in the culture {qty: 100, units: 'mM'}\n# @attr_reader final_concentration [Hash] object representing the desired final concentration of the component used in the culture {qty: 100, units: 'nM'}\n# @attr_reader dilution_factor [int] the computed amount of dilution required to dilute the stock_concentration to reach the desired final_concentration\n# @attr_reader added [boolean] a way to determine whether a component has been added to the culture\nclass CultureComponent\n include Units\n attr_accessor :working_vol\n attr_reader :input_name, :sample, :item, :stock_concentration, :final_concentration, :dilution_factor, :added\n \n def initialize(args={})\n @input_name = args.fetch(:input_name)\n @sample = args.fetch(:sample)\n @item = args.fetch(:item, nil)\n @final_concentration = args.fetch(:final_concentration, nil)\n @working_vol = { qty: 0, units: MICROLITERS }\n get_component_type_attributes#(args) May should add back args but dont think its needed\n @added = false\n end\n \n def get_component_type_attributes#(args) #May should add back args but dont think its needed\n case input_name\n when \"Strain\"\n when \"Media\"\n when \"Control Tag\"\n when \"Inducer(s)\"\n\n #if item.class == Hash then there is no stock. Check here makes debugging easier\n if item.class == Hash \n raise \"Inducer not in stock, check all inducers for existing stock\"\n end\n\n qty, units, name = item.object_type.name.split(' ')\n set_stock_item_concentration(qty: qty.to_f, units: units)\n set_dilution_factor(val: calculate_dilution_factor(stock_conc: stock_concentration, final_conc: final_concentration))\n when \"Antibiotic(s)\"\n @item = (item.nil?) ? (sample.items.select {|i| i.object_type.name == 'Antibiotic Aliquot'}.first) : item\n qty, units = sample.properties['Recommended Working Concentration (ug/mL)'], 'ug/mL' # Aliquots always have the same units\n set_stock_item_concentration(qty: qty.to_f, units: units)\n (final_concentration[:units] == units) ? (set_dilution_factor(val: final_concentration[:qty]/qty)) : (raise \"The antibiotic units are not compatable in CultureComponent class\")\n else \n raise \"#{input_name} is not a valid type of culture component. Please update class CultureComponent!\"\n end\n end\n \n def set_stock_item_concentration(qty:, units:)\n units = (units == 'uM') ? MICROMOLAR : units\n @stock_concentration = { qty: qty.to_f, units: units }\n end\n \n def set_dilution_factor(val:)\n @dilution_factor = val\n end\n \n def calculate_dilution_factor(stock_conc:, final_conc:)\n stock_units = stock_conc[:qty]*CultureComponent.unit_conversion_hash[stock_conc[:units]]\n final_units = final_conc[:qty]*CultureComponent.unit_conversion_hash[final_conc[:units]]\n return (final_units/stock_units).round(4)\n end\n \n def self.unit_conversion_hash\n { NANOMOLAR =\u003e 10e-9, MICROMOLAR =\u003e 10e-6, 'uM' =\u003e 10e-6, MILLIMOLAR =\u003e 10e-3, MOLAR =\u003e 10e0 }\n end\nend # CultureComponent\n\n\n# Parses out JSON parameters \u0026 non-JSON parameters from the Define Culture Conditions operation.\nmodule FieldValueParser\n \n def self.get_args_from(obj:)\n if obj.is_a? FieldValue\n case obj.name\n when \"Strain\", \"Media\"\n args_arr = FieldValueParser.get_non_parameter_args(fv: obj)\n when \"Inducer(s)\"\n args_arr = FieldValueParser.get_inducer_args(fv: obj)\n args_arr = FieldValueParser.generate_component_combinations(args_arr)\n when \"Antibiotic(s)\"\n args_arr = FieldValueParser.get_antibiotic_args(fv: obj)\n when \"Option(s)\"\n # Pass\n else\n raise \"#{obj.name} is not a field value that can be parsed class.\"\n end\n else\n raise \"#{obj.class} is not able to be used to initialize the CultureComposition class.\"\n end\n return args_arr\n end\n \n def self.get_antibiotic_args(fv:)\n args_arr = []\n JSON.parse(fv.value).each do |sname, opts|\n sample = Sample.find_by_name(sname)\n final_concentration = opts.fetch('final_concentration') \n f_qty, f_units = final_concentration.split('_')\n args_arr.push({\n input_name: fv.name, \n sample_name: sname,\n sample: sample,\n item: nil,\n final_concentration: {qty: f_qty.to_f, units: f_units}\n })\n end\n return args_arr\n end\n\n def self.get_non_parameter_args(fv:)\n {\n input_name: fv.name,\n sample_name: fv.sample.name,\n sample: fv.sample,\n item: fv.item\n }\n end\n \n def self.generate_component_combinations(args_arr)\n groupby_types = args_arr.group_by {|args| args.fetch(:sample_name) }\n num_of_components = groupby_types.keys.length\n if num_of_components \u003e 1\n combinations = args_arr.combination(num_of_components).to_a.select {|combo| \n combo.map {|args| args.fetch(:sample_name) }.uniq.length == num_of_components\n }\n return combinations\n else\n return (groupby_types.nil? ? groupby_types : groupby_types.values.first)\n end\n end\n \n def self.get_inducer_args(fv:)\n args_arr = []\n JSON.parse(fv.value).each do |sname, opts|\n sample = Sample.find_by_name(sname)\n final_concentration = opts.fetch('final_concentration')\n if final_concentration.is_a? Array\n sample_inventory = sample.items.reject {|item| item.location == 'deleted' }\n final_concentration.each_with_index do |fconc, idx|\n f_qty = fconc.split('_')[0].to_f; f_units = fconc.split('_')[1]\n item_id = opts.fetch('item_id', nil)\n if item_id.nil?\n item = FieldValueParser.get_inducer_component_item(sample: sample, sample_inventory: sample_inventory, fconc: fconc) \n else\n item = (item_id.is_a? Array) ? Item.find(item_id[idx].to_i) : Item.find(item_id.to_i)\n end\n args_arr.push({\n input_name: fv.name, \n sample_name: sname,\n sample: sample,\n item: item,\n final_concentration: {qty: f_qty, units: f_units}\n })\n end\n end\n end\n return args_arr\n end\n \n # This function helps Aq automatically find an inducer item when given the sample, the sample_inventory (all items that are not deleted), and the desired final_concentration\n # It selects the item by finding the dilution factor of the final_concentration divided by an item in the sample_inventory. If it is greater than a 0.001 fold dilution then it selects that item.\n # It must be greater than a 0.001 dilution in order to pipette accurately.\n def self.get_inducer_component_item(sample:, sample_inventory:, fconc:)\n inventory_by_object_type = sample_inventory.group_by {|item| item.object_type.name }\n f_qty = fconc.split('_')[0].to_f; f_units = fconc.split('_')[1]\n if f_qty \u003c= 0.0\n item = sample_inventory.first\n else\n final_units = f_qty * CultureComponent.unit_conversion_hash[f_units] rescue (raise \"#{sample.id} #{fconc}\")\n inventory_by_object_type.each do |otname, item_arr|\n s_qty = otname.split(' ')[0].to_f; s_units = otname.split(' ')[1]\n stock_units = s_qty * CultureComponent.unit_conversion_hash[s_units]\n dilution_factor = (final_units/stock_units).round(4)\n if dilution_factor \u003c 0.001\n next\n else\n return item_arr.first # return the lowest item_id or the oldest item\n end\n raise MissingInducerComponentItemError.new(sample: sample, final_concentration: fconc)\n end\n end\n end\n\n # Exception class for finding an inducer item that can be diluted accurately to reach the desired final concentration.\n #\n # @attr_reader [String] name the name of the object type where measure has was expected TODO: Documentation\n class MissingInducerComponentItemError \u003c StandardError\n attr_reader :sample_name, :final_concentration\n def initialize(msg: \"A suitible inducer item cannot be found to reach the final concentration desired. Please add an object type to the inducer sample or add an item to its inventory\", sample_name:, final_concentration:)\n @sample_name = sample_name\n @final_concentration = final_concentration\n super(msg)\n end\n end\n \nend # module FieldValueParser\n\n"}},{"library":{"name":"FlowCytometryCalibration","category":"High Throughput Culturing","code_source":"needs \"High Throughput Culturing/FlowCytometryHelper\"\nmodule FlowCytometryCalibration\n include AssociationManagement\n \n BEAD_SAMPLE_KEY = 'BEAD_UPLOAD'.freeze\n \n # Defines class FlowCytometry instance variables experimental_item and the measurement_item when measuring calibration beads on the flow cytometer\n #\n # @param flow_cytometer [class instance] instance of the flow cytometer\n # @param bead_item [Item] is the item from the input FieldValue of the Flow Cytometer Calibration\n def setup_calibration_measurement(flow_cytometer:, bead_item:)\n flow_cytometer.experimental_item = bead_item\n diluted_bead_items = bead_item.sample.items.reject {|item| item.location == 'deleted' || item.object_type.name == bead_item.object_type.name }\n if diluted_bead_items.empty?\n flow_cytometer.measurement_item = create_diluted_bead_item(bead_item)\n else\n flow_cytometer.measurement_item = diluted_bead_items[-1]\n end\n end\n \n # Generates a new virtual diluted bead item\n #\n # @param bead_item [Item] is the item from the input FieldValue of the Flow Cytometer Calibration\n # @return diluted_bead_item [Item] is a new Diluted bead item used for Calibration\n def create_diluted_bead_item(bead_item)\n diluted_bead_item = Item.new()\n diluted_bead_item.object_type = ObjectType.find_by_name('Diluted beads')\n diluted_bead_item.sample = bead_item.sample\n diluted_bead_item.quantity = 1\n diluted_bead_item.location = bead_item.location\n return diluted_bead_item\n end\n \n # Guides technician in preparing diluted beads from bead_item stock\n # \n # @param flow_cytometer [class instance] instance of the flow cytometer\n def dilute_beads(flow_cytometer:)\n bead_item = flow_cytometer.experimental_item\n take [bead_item], interactive: true\n show do \n title \"Prepare #{bead_item.sample.name} #{bead_item} for Calibration\"\n separator\n check \"Grab a new, clean 1.5mL microfuge tube and label: \u003cb\u003e#{flow_cytometer.measurement_item}\u003c/b\u003e\"\n check \"Next, add \u003cb\u003e1mL\u003c/b\u003e of Molecular Grade H2O to the tube\"\n check \"Dispense a drop of each dropper found in the #{bead_item.sample.name} box (ie: Spherotech beads white cap \u0026 brown cap).\"\n check \"Vortex for 10 seconds\"\n check \"Spin down briefly\"\n end\n end\n \n # Prepares calibration beads by determining whether current beads are reusable\n #\n # @param flow_cytometer [class instance] instance of the flow cytometer\n def prepare_calibration_beads(flow_cytometer:)\n if flow_cytometer.experimental_item.id == flow_cytometer.measurement_item.id\n take [flow_cytometer.measurement_item], interactive: true\n reuse_bead_item(flow_cytometer: flow_cytometer)\n else\n dilute_beads(flow_cytometer: flow_cytometer)\n end\n end\n \n # Deterimines if the beads are expired (\u003e4weeks) or if there is not enough volume for the calibration\n # \n # @param flow_cytometer [class instance] instance of the flow cytometer\n def reuse_bead_item(flow_cytometer:)\n if past_expiration?(flow_cytometer: flow_cytometer)\n flow_cytometer.measurement_item.mark_as_deleted\n diluted_bead_item = create_diluted_bead_item(flow_cytometer.experimental_item)\n flow_cytometer.measurement_item = diluted_bead_item\n dilute_beads(flow_cytometer: flow_cytometer)\n else\n respose = show do\n title 'Enough Volume to Continue?'\n separator\n note \"Is there at least 0.5mL in #{flow_cytometer.measurement_item.object_type.name} #{flow_cytometer.measurement_item}?\"\n select ['Yes','No'], var: 'response', label: \"Is there at least 500ul?.\", default: 0\n end\n if respose[:response] == 'Yes'\n flow_cytometer.measurement_item.mark_as_deleted\n diluted_bead_item = create_diluted_bead_item(flow_cytometer.experimental_item)\n flow_cytometer.measurement_item = diluted_bead_item\n dilute_beads(flow_cytometer: flow_cytometer)\n end\n end\n end\n \n # Determine if the beads are older than a month old\n def past_expiration?(flow_cytometer:)\n timepoint = Time.now()\n this_week = timepoint.strftime('%W').to_i\n expiration_week = flow_cytometer.measurement_item.week.to_i+5\n if this_week \u003e= expiration_week\n return true\n else\n return false\n end\n end\n \n # Associate all `uploads` to the `target` DataAssociator. The keys of each upload will be\n # the concatenation of `key_name` and that upload's id.\n # Associating fcs files to the plan and operation makes fcs data of any specific well\n # easily accessible to users\n #\n # @param key_name [String] the name which describes this upload set\n # @param target [Aq Model] can be any Aq model class that can have associations\n # @param uploads [Array\u003cUpload\u003e] An Array containing several Uploads\n # @effects associates all the given uploads to `plan`, each with a\n # unique key generated from the combining `keyname` and upload id\n def associate_uploads(key_name, target, uploads)\n if target\n associations = AssociationMap.new(target)\n uploads.each do |up|\n associations.put(\"U#{up.id}_#{key_name}\", up)\n end\n associations.save\n end\n end\n \n def process_and_associate_calibration(instrument:, ops:)\n calibration_measurement_upload = [instrument.measurement_data.fetch(:uploads).first]\n associate_uploads('BEADS_uploads', instrument.measurement_item, calibration_measurement_upload)\n ops.each do |op|\n associate_uploads(BEAD_SAMPLE_KEY, op, calibration_measurement_upload)\n associate_uploads(BEAD_SAMPLE_KEY, op.plan, calibration_measurement_upload)\n end\n end\n \nend # module FlowCytometryCalibration"}},{"library":{"name":"FlowCytometryConstants","category":"High Throughput Culturing","code_source":"\n# Flow Cytometry Constants \nALLOWABLE_FC_SAMPLETYPES = [\"Plasmid\", \"Yeast Strain\", \"E coli strain\"].freeze # Cultures\nSAMPLE_UPLOAD_KEY = 'SAMPLE_UPLOAD'.freeze\nBEAD_UPLOAD_KEY = 'BEAD_UPLOAD'.freeze\n\n# Flow Cytometry instance constants\nKLAVINS_LAB = 'Klavins Lab'.to_sym\nHAASE_LAB = 'Haase Lab'.to_sym\nFLOW_CYTOMETER_TYPE = {'Klavins Lab':'BD Accuri C6'.to_sym,'Haase Lab':'Attune'.to_sym}\nMY_FLOW_CYTOMETER_PROPERTIES = {\n 'Klavins Lab': {\n 'BD Accuri C6': {\n software_properties: {\n sample_type_settings: {\n 'Yeast Strain': {'Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 400,000'},\n 'Plasmid': {'Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 80,000'},\n 'Beads': {'Figure out': 'Calibration limits','Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 80,000'}\n },\n plate_type_hash: {\n '96 Well Flat Bottom (black)': 'Flat bottom plate',\n 'Diluted beads': '24-Well Tube Rack'\n },\n images: {\n open_software: \"Actions/Yeast_Gates/flowCytometryImages/open_FC_software_icon.png\",\n select_plate_type: \"Actions/Yeast_Gates/flowCytometryImages/select_plate_type.png\",\n apply_settings: \"Actions/Yeast_Gates/flowCytometryImages/flow_cytometry_settings.png\",\n read_plate: \"Actions/Yeast_Gates/flowCytometryImages/measure_plate_autorun.png\",\n export_new_data: \"Actions/FlowCytometry/saveFCS_menu_cropped.png\",\n new_export_directory: \"Actions/FlowCytometry/saveFCS_dirname_cropped.png\"\n },\n measurement_type_templates: {\n \"cleaning\": {dtype: 'could be the three cleaning files to record contamination over time!'},\n \"Yeast\": {dtype: \"paratmers for yeast measurements\"},\n \"Ecoli\": {},\n \"Calibration\": {}\n },\n saving_directory: 'FIND_OUT_THE_PATH/FCS_Exports'\n },\n valid_containers: ['96 Well Flat Bottom (black)'],\n },\n },\n 'YOUR_LAB_HERE':{\n 'YOUR_PLATE_READER_TYPE':{ \n software_steps: 'properties', \n valid_containers: ['96 Well Flat Bottom (black)'], \n },\n saving_directory: 'SAVING_DIRECTORY'\n },\n 'Haase Lab': {\n 'Attune': {\n software_properties: {\n sample_type_settings: {\n 'Yeast Strain': {'Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 400,000'},\n 'Plasmid': {'Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 80,000'},\n 'Beads': {'Figure out': 'Calibration limits','Run Limits': '30,000 events', 'Run Limits (Max Volume)': '250ul', 'Fluidics': 'Fast', 'Set Threshold': 'FSC-H less than 80,000'}\n },\n plate_type_hash: {\n '96 Well Flat Bottom (black)': 'Flat bottom plate',\n '24 Deep Well Plate': '24 Deep Well Plate',\n '24 Unit Disorganized Collection': 'Disorganized Collection',\n 'Diluted beads': '24-Well Tube Rack'\n },\n images: {\n open_software: \"Actions/Yeast_Gates/flowCytometryImages/open_FC_software_icon.png\", #file\n select_plate_type: \"Actions/Yeast_Gates/flowCytometryImages/select_plate_type.png\", # Remove not needed\n apply_settings: \"Actions/Yeast_Gates/flowCytometryImages/flow_cytometry_settings.png\", #Pic of side bar\n read_plate: \"Actions/Yeast_Gates/flowCytometryImages/measure_plate_autorun.png\", # Start Up button\n export_new_data: \"Actions/FlowCytometry/saveFCS_menu_cropped.png\", # Right click? Remove probably\n new_export_directory: \"Actions/FlowCytometry/saveFCS_dirname_cropped.png\" # Remove probably\n },\n measurement_type_templates: {\n \"cleaning\": {dtype: 'could be the three cleaning files to record contamination over time!'},\n \"Yeast\": {dtype: \"paratmers for yeast measurements\"},\n \"Ecoli\": {},\n \"Calibration\": {}\n },\n saving_directory: 'FIND_OUT_THE_PATH/FCS_Exports'\n },\n valid_containers: ['96 Well Flat Bottom (black)', '24 Deep Well Plate', '24 Unit Disorganized Collection'],\n },\n },\n 'YOUR_LAB_HERE':{\n 'YOUR_PLATE_READER_TYPE':{ \n software_steps: 'properties', \n valid_containers: ['96 Well Flat Bottom (black)'], \n },\n saving_directory: 'SAVING_DIRECTORY'\n }\n}\n"}},{"library":{"name":"FlowCytometryHelper","category":"High Throughput Culturing","code_source":"needs \"Standard Libs/Units\"\nneeds \"Standard Libs/AssociationManagement\"\nhtc = \"High Throughput Culturing/\"\nneeds htc + \"FlowCytometryConstants\"\nneeds htc + \"FlowCytometrySoftware\"\nneeds htc + \"InstrumentHelper\"\nneeds htc + \"HighThroughputHelper\"\n\n# This class instantiates a representation of a Flow Cytometer instrument, in order for Aq to interact with this type of instrument\n#\n# @author Eriberto Lopez - [email protected]\n# @since 08/16/19\n# \n# @attr [boolean] software_open describes the state of the software for the flow cytometer\n# @attr_reader [String] instrument_type describes the type of instruement\n# @attr_reader [String] type describes the model of instrument\n# @attr_reader [Array] valid_containers describes the types of containers that can be measured by the flow cytometer\n# @attr_reader [Hash] software is a hash object that comes from FlowCytometryConstants Library\nclass FlowCytometer\n include InstrumentHelper\n attr_accessor :software_open\n attr_reader :instrument_type, :type, :valid_containers, :software, :lab_name\n def initialize()\n @lab_name = get_lab_name\n @instrument_type = 'Flow Cytometer'.freeze\n @type = get_my_flow_cytometer_type\n @valid_containers = valid_containers\n @software = get_my_software_properties\n @software_open = false\n end\n\n #Checks what server AQ is being run on so it can decide what instruments are available to use\n #Assumes that the BioFAB server will only be run in BIOFAB and no where else\n def get_lab_name\n aq_instance = Bioturk::Application.config.instance_name.upcase\n if aq_instance == \"UW BIOFAB\"\n return KLAVINS_LAB.tosym\n elsif aq_instance == \"DARPA_SD2\"\n return HAASE_LAB.to_sym\n else #usually when its dockerized \u0026\u0026 Your Lab\n return HAASE_LAB.to_sym ##This is temporary. Should have a check here saying\n end\n end\n \n def get_my_flow_cytometer_type\n FLOW_CYTOMETER_TYPE[lab_name.to_sym]\n end\n\n def valid_containers\n get_my_flow_cytometer_properties_obj[type][:valid_containers]\n end\n \n def get_my_flow_cytometer_properties_obj\n MY_FLOW_CYTOMETER_PROPERTIES[lab_name.to_sym]\n end\n \n def get_my_software_properties\n get_my_flow_cytometer_properties_obj[type][:software_properties]\n end\n \n def valid_container?\n if experimental_item.instance_of? Collection\n valid_containers.include? experimental_item.object_type.name.to_s\n elsif experimental_item.instance_of? Item\n valid_containers.map {|c| ObjectType.find_by_name(c).id.to_i}.include? experimental_item.object_type_id.to_i\n elsif experimental_item.instance_of? Array\n valid_container = false\n ot_arr = ObjectType.find(experimental_item.map {|item| item.object_type_id}.uniq).map {|ot| ot.name}\n ot_arr.each {|otn| (valid_containers.include? otn) ? (valid_container = true) : (valid_container) }\n valid_container\n elsif experimental_item.class == \"NillClass\"\n raise \"Your input plate is of class #{experimental_item.class} which means it was likley deleted\"\n else\n raise \"This type of #{experimental_item.class} object is not compatible with this instrument\"\n end\n end\n \nend # class FlowCytometer\n\nmodule FlowCytometryHelper\n include Units, AssociationManagement\n \n def intro\n flow_cytometer = FlowCytometer.new\n show do\n title \"Flow Cytometry Measurements\"\n separator\n note \"This protocol will instruct you on how to take measurements on the #{flow_cytometer.type} Flow Cytometer.\"\n note \"A flow cytomter uses lasers to phenotypically characterize a microbial culture.\"\n note \"This per cell measurement quantifies a cell's size, shape, and color. Making it a useful tool to analyze \u0026 distiguish cellular populations from each other.\"\n note \"In this protocol, you will prepare the instrument workspace and characterize your genetically modified organism.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Setup #{flow_cytometer.type} Flow Cytomter Software workspace.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Check to see if input item is a #{flow_cytometer.valid_containers} if not, transfer samples to a valid container.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Load plate.\"\n note \"\u003cb\u003e4.\u003c/b\u003e Take measurement, export data, \u0026 upload.\"\n end\n get_flow_cytometer_software(flow_cytometer: flow_cytometer)\n return flow_cytometer\n end\n \n\n ##TODO This is where you can add new cytometers\n # Using the instance of class FlowCytometer, we can determine which software module to import\n def get_flow_cytometer_software(flow_cytometer:)\n case flow_cytometer.type\n when 'BD Accuri C6'.to_sym\n Protocol.include(BDAccuri)\n when 'Attune'.to_sym\n Protocol.include(Attune)\n else\n raise \"the #{flow_cytometer.type} flow cytometer in the #{lab_name} has no software steps associated to it, create a module with steps to use flow cytomter\".upcase\n end\n end\n \n # Overrides method found in HighThroughputHelper, so that only ALLOWABLE_FC_SAMPLETYPES are transferred to a new collection\n def copy_sample_matrix(from_collection:, to_collection:)\n sample_hash = Hash.new()\n from_collection_sample_types = from_collection.matrix.flatten.uniq.reject{|i| i == EMPTY }.map {|sample_id| [sample_id, Sample.find(sample_id)] }\n from_collection_sample_types.each {|sid, sample| (ALLOWABLE_FC_SAMPLETYPES.include? sample.sample_type.name) ? (sample_hash[sid] = sample) : (sample_hash[sid] = EMPTY) }\n dilution_sample_matrix = from_collection.matrix.map {|row| row.map {|sample_id| sample_hash[sample_id] } }\n to_collection.matrix = dilution_sample_matrix\n to_collection.save()\n end\n \n # Overrides method found in HighThroughputHelper, so that only ALLOWABLE_FC_SAMPLETYPES are transferred to a new collection\n def part_provenance_transfer(from_collection:, to_collection:, process_name:)\n to_collection_part_matrix = to_collection.part_matrix\n from_collection.part_matrix.each_with_index do |row, r_i|\n row.each_with_index do |from_part, c_i|\n if !from_part.nil? || from_part \n if ALLOWABLE_FC_SAMPLETYPES.include? from_part.sample.sample_type.name\n to_part = to_collection_part_matrix[r_i][c_i]\n # Create source and destination objs\n source_id = from_part.id; source = [{id: source_id }]\n destination_id = to_part.id; destination = [{id: destination_id }]\n # raise process_name.inspect unless process_name.nil?\n destination.first.merge!({additional_relation_data: { process: process_name }}) unless process_name.nil?\n # Association source and destination\n to_part.associate(key=:source, value=source)\n from_part.associate(key=:destination, value=destination)\n end\n end\n end\n end\n end\n \n # Guides technician to transfer the ALLOWABLE_FC_SAMPLETYPES wells to a flow cytometer valid container\n #\n # @effect Guides technician through transfer of cultures, then sets the collection to the output FieldValue\n def tech_transfer_to_valid_container(instrument:, output_fieldValue:)\n from_collection = collection_from(instrument.experimental_item); to_collection = collection_from(instrument.measurement_item)\n copy_sample_matrix(from_collection: from_collection, to_collection: to_collection)\n part_provenance_transfer(from_collection: from_collection, to_collection: to_collection, process_name: 'transfer')\n qty, units = get_container_working_volume(container=to_collection)\n display_coordinates = alpha_coordinates_96\n show do\n title \"Transfer to Valid Container\"\n separator\n note \"Gather empty #{to_collection.object_type.name} #{to_collection}\"\n note \"Follow the table below to transfer only the shaded wells:\"\n note \"Transfer 200ul per well\"\n bullet \"\u003cb\u003eFrom\u003c/b\u003e #{from_collection.object_type.name} #{from_collection}\"\n bullet \"\u003cb\u003eTo\u003c/b\u003e #{to_collection.object_type.name} #{to_collection}\"\n table highlight_alpha_non_empty(to_collection) {|r, c| \"#{display_coordinates[r][c]}\" }\n end\n output_fieldValue.set(collection: to_collection)\n end\n \n # Finds a container's JSON parsable data association and grabs the working_vol association\n #\n # @param container [Item/Collection] is an item with a ObjectType.data association\n # @return qty [int] the number of units\n # @return units [string] the type of Units describing the working volume of the container\n def get_container_working_volume(container)\n working_volume = JSON.parse(container.object_type.data)['working_vol']\n raise \"ObjectType #{container.object_type.name} does not have a JSON parsable \u003cb\u003e'working_vol'\u003c/b\u003e association.\n Please go to containers and add an association\" if working_volume.nil?\n qty = working_volume.split('_')[0].to_f\n units = working_volume.split('_')[1].to_s\n return qty, units\n end\n\n # Associate all `uploads` to the `target` DataAssociator. The keys of each upload will be\n # the concatenation of `key_name` and that upload's id.\n # Associating fcs files to the plan and operation makes fcs data of any specific well\n # easily accessible to users\n #\n # @param [String] key_name the name which describes this upload set\n # @param [Plan] plan the plan that the uploads will be associated to\n # @param [Array\u003cUpload\u003e] uploads An Array containing several Uploads\n # @effects associates all the given uploads to `plan`, each with a\n # unique key generated from the combining `keyname` and upload id\n def associate_fcs_uploads(key_name, target, uploads)\n if target\n associations = AssociationMap.new(target)\n uploads.each do |up|\n associations.put(\"U#{up.id}_#{key_name}\", up)\n end\n associations.save\n end\n end\n \n # Associate a matrix containing all `uploads` to `collection`.\n # The upload matrix will map exactly to the sample matrix of\n # `collection`, and it will be associated to `collection` as a value\n # of `key_name`\n #\n # @param [String] key_name the key that the upload matrix will\n # be associated under\n # @param [Collection] collection what the upload matrix will be\n # associated to\n # @param [Array\u003cUpload\u003e] uploads An Array containing several Uploads\n # @effects associates all the given uploads to `collection` as a 2D array inside a singleton hash\n def associate_uploads_to_plate(key_name:, collection:, uploads:)\n ot = collection.object_type\n uploads_well_matrix = Array.new(ot.rows) { Array.new(ot.columns) { EMPTY } }\n uploads.each do |up|\n alpha_coord = up.name[0..2]\n r_i, c_i = get_rc_from_alpha_coords(alpha_coord: alpha_coord)\n uploads_well_matrix[r_i][c_i] = up.id\n end\n collection_associations = AssociationMap.new(collection)\n # ensure we aren't overwriting an existing association\n unless collection_associations.get(key_name).nil?\n i = 0\n i += 1 until collection_associations.get(\"#{key_name}_#{i}\").nil?\n key_name = \"#{key_name}_#{i}\"\n end\n collection_associations.put(key_name, {'upload_matrix' =\u003e uploads_well_matrix })\n collection_associations.save\n end\n \n # Process and associates the array of flow cytometry uploads to operation, plan, and the collection that was measured.\n def process_and_associate_data(instrument:, op:)\n measurement_args = instrument.measurement_data\n uploads = measurement_args[:uploads]\n associate_fcs_uploads(key_name=SAMPLE_UPLOAD_KEY, target=op, uploads=uploads)\n associate_fcs_uploads(key_name=SAMPLE_UPLOAD_KEY, target=op.plan, uploads=uploads)\n associate_uploads_to_plate(key_name: SAMPLE_UPLOAD_KEY.pluralize, collection: measurement_args[:mitem], uploads: uploads)\n end\n \n # Determines whether to keep plate based on user input\n def keep_transfer_plate(instrument:, user_val:)\n instrument.measurement_item.location = ((user_val.upcase == 'YES') ? 'Bench' : 'deleted')\n end\n \n # Cleans up technician workspace by releasing inputs and outputs\n def cleaning_up(release_arr=[])\n clean_up_inputs(release_arr)\n clean_up_outputs\n end\n\n # Cleanup input items\n #\n # @param release_arr [array] is an array of items that are found in Aq\n def clean_up_inputs(release_arr)\n operations.store(opts = { interactive: true, method: 'boxes', errored: false, io: 'input' })\n release release_arr, interactive: true\n end\n\n # Cleanup output items\n def clean_up_outputs()\n show do\n title \"Cleaning Up\"\n separator\n note \"Return any remaining reagents used and clean bench\"\n end\n end\nend # module FlowCytometryHelper"}},{"library":{"name":"FlowCytometrySoftware","category":"High Throughput Culturing","code_source":"# Eriberto Lopez\n# [email protected]\n# 08/15/19\n#\n#Update:\n#Cannon Mallory\n#[email protected]\n#02/04/2020\n\nrequire 'date'\n\nmodule AqUpload\n # Provides a upload button in a showblock in order to upload a single file\n #\n # @param upload_filename [string] can be the name of the file that you want tech to upload\n # @return up_show [hash] is the upload hash created in the upload show block\n # @return up_sym [symbol] is the symbol created in upload show block that will be used to access upload\n def upload_show(saving_dir:, upload_filename:)\n upload_var = \"file\"\n up_show = show do\n title \"Upload Your Measurements\"\n separator\n note \"Select and Upload: #{saving_dir}/\u003cb\u003e#{upload_filename}\u003c/b\u003e\"\n upload var: upload_var.to_sym\n end\n return up_show, upload_var.to_sym\n end\n \n # Retrieves the upload object from upload show block\n #\n # @param up_show [hash] is the hash that is created in the upload show block\n # @param up_sym [symbol] is the symbol created in the upload show block and used to access file uploaded\n # @return upload [upload_object] is the file that was uploaded in the upload show block\n def get_upload_from_show(up_show:, up_sym:)\n (!up_show[up_sym].nil?) ? (upload = up_show[up_sym].map {|up_hash| Upload.find(up_hash[:id])}.shift) : nil #(show {warning \"no upload was found\".upcase})\n end\n \n # Retrieves the upload object from the upload show block and gathers the array of .fcs uploads\n def get_upload_array_from_show(up_show:, up_sym:)\n upload_array = up_show[up_sym].map {|up_hash| Upload.find(up_hash[:id]) }\n return upload_array\n end\n \n # Technician upload show block\n #\n # @param saving_dir [string] name of the directory that is created when all flow cytometery measurements are exported as .fcs files\n def upload_directory_show(saving_dir:)\n upload_var = \"file\"\n up_show = show do\n title \"Upload Your Measurements\"\n separator\n note \"Select and Upload Directory: \u003cb\u003e#{saving_dir}\u003c/b\u003e\"\n upload var: upload_var.to_sym\n end\n return up_show, upload_var.to_sym\n end\nend # module AqUpload\n\nmodule StandardCytometrySoftware\n\n # Groups sample type wells together to create an rc_list of a given sampletype in a collection.\n #\n # @params instrument [class] is a class instance of an instrument\n # @return sample_type_rc_list [Hash] is a hash of sample_type.names with a list of [r,c] tuples describing where that type of sample is in the collection that is being measured\n def get_sample_type_rc_list_hash(instrument)\n measurement_collection = collection_from(instrument.measurement_item)\n strain_sample_hash = Hash.new()\n measurement_collection.matrix.flatten.uniq.reject {|sid| sid == -1 }.each {|sid| strain_sample_hash[sid] = Sample.find(sid.to_i) }\n sample_type_rc_list_hash = Hash.new()\n measurement_collection.matrix.each_with_index do |row, r|\n row.each_with_index do |sid, c|\n if sid == -1\n next\n else\n if sample_type_rc_list_hash[strain_sample_hash[sid].sample_type.name].nil?\n sample_type_rc_list_hash[strain_sample_hash[sid].sample_type.name] = [[r, c]]\n else\n sample_type_rc_list_hash[strain_sample_hash[sid].sample_type.name].push([r, c])\n end\n end\n end\n end\n return sample_type_rc_list_hash\n end\n\n def get_measurement_filename(measurement_item:, timepoint:)\n mt = measurement_type.to_s.gsub(' ', '') \n \"jid_#{jid}_item_#{measurement_item.id}_t#{timepoint.strftime \"%H%M\"}_#{timepoint.strftime \"%m%d%Y\"}\".gsub(' ', '_')\n end\n\n\n # Generates an experimental measurement filename, which is the name of the workspace created on the Accuri Flow Cytometer\n def get_experiment_filename(instrument:)\n timepoint = Time.now\n return \"jid_#{jid}_experiment_#{instrument.measurement_item}_#{timepoint.strftime \"%m%d%Y\"}\".gsub(' ', '')\n end\n\n\n # Guides technician on how to open software if it is not already open\n def open_software(instrument)\n if (!instrument.software_open)\n go_to_computer(instrument)\n show do\n title \"Open #{instrument.type} #{instrument.instrument_type} Software\"\n separator\n note \"Click on the icon shown below to open the #{instrument.instrument_type} software:\"\n image instrument.software[:images][:open_software]\n end\n instrument.software_open = true\n else\n log_info 'the software is already open!'.upcase\n end\n end\n\n # Directs techician to which lab and flow cytometer computer to setup workspace\n def go_to_computer(instrument)\n show do\n title \"Go to the #{LAB_NAME} #{instrument.type} #{instrument.instrument_type}\"\n separator\n warning \"\u003cb\u003eThe next steps should be done on the #{instrument.instrument_type} computer\u003c/b\u003e.\".upcase\n end\n end\n\n\n\n\n # Creates a dummy 24 well tube rack to display on screen (deletes down stream)\n def get_tube_rack\n produce new_collection '24 Deep Well Plate'\n end\n\n # Guides technician to select plate type in the flow cytometry software\n def select_plate_type(instrument)\n show do\n title \"Setup #{instrument.type} Workspace\"\n separator\n note \"Select \u003cb\u003ePlate Type\u003c/b\u003e: #{instrument.software.fetch(:plate_type_hash)[instrument.measurement_item.object_type.name.to_sym]}\"\n image instrument.software.fetch(:images).fetch(:select_plate_type)\n end\n end\nend # module StandarCytometrySoftware\n\nmodule BDAccuri\n include AqUpload\n include StandardCytometrySoftware\n \n # Guides technician through the steps in setting up the BD Acurri software workspace for culture measurements\n def setup_instrument_software(instrument)\n open_software(instrument)\n select_plate_type(instrument)\n get_sample_type_rc_list_hash(instrument).each do |sample_type_name, rc_list|\n if ALLOWABLE_FC_SAMPLETYPES.include? sample_type_name\n show do\n title \"Select Wells\"\n separator\n note \"Select the following wells for #{sample_type_name} cultures found in #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}.\"\n table highlight_alpha_rc(collection_from(instrument.measurement_item), rc_list) {|r,c| \"#\"}\n note 'After checking the wells on the screen continue to the next step.'\n end\n apply_settings(instrument: instrument)\n end\n end\n end\n\n \n # Guides technician through the steps in setting up the BD Acurri software workspace for calibration measurement\n def setup_instrument_calibration(instrument)\n open_software(instrument)\n select_plate_type(instrument)\n tube_rack = get_tube_rack \n show do\n title \"Select Wells\"\n separator\n note \"Select the following location of the optical particals #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}.\"\n table highlight_alpha_rc(tube_rack, [[0,0]]) {|r,c| \"#\"}\n note 'After checking the wells on the screen continue to the next step.'\n end\n tube_rack.mark_as_deleted\n apply_settings(instrument: instrument)\n end\n \n # Guides technician to apply settings to the selected wells. Wells are selected by sampleType (different sampleTypes require different settings)\n def apply_settings(instrument:)\n log_info 'instrument.experimental_item.object_type.name.downcase', instrument.experimental_item.object_type.name.downcase\n if instrument.experimental_item.object_type.name.downcase.include? 'bead'\n sample_type_name = instrument.experimental_item.sample.sample_type.name.to_sym\n else\n sample_type_name = collection_from(instrument.experimental_item).matrix.flatten.uniq.reject {|sid| sid == -1 }.map {|sid| s = Sample.find(sid); s.sample_type.name }.uniq.first.to_sym\n end\n show do\n title \"Apply Settings\"\n separator\n image instrument.software.fetch(:images).fetch(:apply_settings)\n note \"Apply the following settings to the wells you have selected\"\n note \"\u003cb\u003eMake sure that the settings are as follows:\u003c/b\u003e\"\n instrument.software.fetch(:sample_type_settings).fetch(sample_type_name).each {|setting, val| bullet \"Set \u003cb\u003e#{setting}\u003c/b\u003e to \u003cb\u003e#{val}\u003c/b\u003e\"}\n check \"Finally, click \u003cb\u003eApply Settings\u003c/b\u003e\"\n bullet \"Save experimental measurement as \u003cb\u003e#{get_experiment_filename(instrument: instrument)}\u003c/b\u003e\"\n end\n end\n \n # Guides techician to read the culture plate, then export, save, and upload data to Aq\n def take_measurement_and_upload_data(instrument:)\n timepoint = read_plate(instrument: instrument)\n instrument.measurement_data = export_save_and_upload_measurement_data(instrument: instrument, timepoint: timepoint)\n end\n \n # Guides techician to read the calibration sample, then export, save, and upload data to Aq\n def take_calibration_and_upload_data(instrument:)\n timepoint = read_calibration(instrument: instrument)\n instrument.measurement_data = export_save_and_upload_measurement_data(instrument: instrument, timepoint: timepoint)\n end\n \n # Guides technician through software to export, save, and upload data to Aq\n def export_save_and_upload_measurement_data(instrument:, timepoint:)\n sw = instrument.software\n day = timepoint.strftime \"%m%d%Y\"\n hour = timepoint.strftime \"%H%M\"\n # Export\n fcs_exports = show do \n title \"Export .fcs files\"\n separator\n note 'Make sure that flow cytometer run is \u003cb\u003eDONE!\u003c/b\u003e'\n note 'Press \u003cb\u003eCLOSE RUN DISPLAY\u003c/b\u003e'\n note 'Select \u003cb\u003eFile\u003c/b\u003e =\u003e \u003cb\u003eExport ALL Samples as FCS...\u003c/b\u003e (see below)'\n image instrument.software.fetch(:images).fetch(:export_new_data)\n note 'You will see a pop-up like below, record the directory ID #'\n image instrument.software.fetch(:images).fetch(:new_export_directory)\n get 'text', var: 'dirname', label: 'Enter the name of the export directory in Desktop/FCS Exports/'\n end\n # UPLOAD\n if (!debug) \n attempt = 0\n up_show, up_sym = {}, :file \n while (up_show[up_sym].nil?) || (attempt == 3) do\n up_show, up_sym = upload_directory_show(saving_dir: sw[:saving_directory]+\"/#{fcs_exports[:dirname]}\")\n attempt += 1\n end\n upload_array = get_upload_array_from_show(up_show: up_show, up_sym: up_sym)\n else\n log_info 'UPLOADS Array debug'\n upload_array = [Upload.find(26482), Upload.find(26458)]\n end\n measurement_data = {\n mitem: instrument.measurement_item,\n day: day,\n hour: hour,\n uploads: upload_array\n }\n return measurement_data\n end\n\n\n \n # Guides technician through measuring the diluted calibration beads\n def read_calibration(instrument: instrument)\n go_to_computer(instrument)\n tube_rack = get_tube_rack\n show do\n title \"Load #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}\"\n separator\n note \"Click \u003cb\u003eEject Plate\u003c/b\u003e\"\n check \"Open tube #{instrument.measurement_item} then, place the tube into the first well of the #{instrument.software.fetch(:plate_type_hash)[instrument.measurement_item.object_type.name.to_sym]}.\" \n bullet \"Well A1 should be by the red sticker in the top left corner.\"\n table highlight_alpha_rc(tube_rack, [[0,0]]) {|r,c| \"#\"}\n check \"Finally, load the #{instrument.measurement_item.object_type.name} and continue to the next step.\"\n end\n tube_rack.mark_as_deleted # Collection created for display, then deleted\n show do \n title \"Taking Measurements\"\n separator\n note \"Click \u003cb\u003eOPEN RUN DISPLAY\u003c/b\u003e\"\n image instrument.software.fetch(:images).fetch(:read_plate)\n note \"Next, click \u003cb\u003eAUTORUN\u003c/b\u003e\"\n note \"Contiue on to the next step while #{instrument.type} #{instrument.instrument_type} is running...\"\n end\n instrument.measurement_item.location = instrument.type.to_s\n timepoint = Time.now\n return timepoint\n end\n \n # Guides technician through measuring the experimental culture plate\n def read_plate(instrument:)\n go_to_computer(instrument)\n show do\n title \"Load #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}\"\n separator\n note \"Click \u003cb\u003eEject Plate\u003c/b\u003e\"\n note \"Be sure that the plate is in the correct orientation. Well A1 should be by the red sticker in the top left corner.\"\n check \"Finally, load the plate and continue to the next step.\"\n end\n show do \n title \"Taking Measurements\"\n separator\n note \"Click \u003cb\u003eOPEN RUN DISPLAY\u003c/b\u003e\"\n image instrument.software.fetch(:images).fetch(:read_plate)\n note \"Next, click \u003cb\u003eAUTORUN\u003c/b\u003e\"\n note \"Contiue on to the next step while #{instrument.type} #{instrument.instrument_type} is running...\"\n end\n instrument.measurement_item.location = instrument.type.to_s\n timepoint = Time.now\n return timepoint\n end\n \nend # module KlavinsLabFlowCytometrySoftware\n\n\n\n#These are the ATUNE specific pages. Please look at current methods for examples\n#Fill in as needed (I tried my best but I don't have the Atune with me)\nmodule Attune\n include AqUpload\n include StandardCytometrySoftware\n \n # Guides technician through the steps in setting up instrument software workspace for culture measurements\n def setup_instrument_software(instrument)\n open_software(instrument)\n select_plate_type(instrument)\n get_sample_type_rc_list_hash(instrument).each do |sample_type_name, rc_list|\n if ALLOWABLE_FC_SAMPLETYPES.include? sample_type_name\n #This may be the same. On the BD Accurie \n show do\n title \"Select Wells #go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Select the following wells for #{sample_type_name} cultures found in #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}.\"\n table highlight_alpha_rc(collection_from(instrument.measurement_item), rc_list) {|r,c| \"#\"}\n note 'After checking the wells on the screen continue to the next step.'\n end\n apply_settings(instrument: instrument)\n end\n end\n end\n \n # Guides technician through the steps in setting up the instrument software workspace for calibration measurement\n def setup_instrument_calibration(instrument)\n open_software(instrument)\n select_plate_type(instrument)\n tube_rack = get_tube_rack \n show do\n title \"Select Wells #go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Select the following location of the optical particals #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}.\"\n table highlight_alpha_rc(tube_rack, [[0,0]]) {|r,c| \"#\"}\n note 'After checking the wells on the screen continue to the next step.'\n end\n tube_rack.mark_as_deleted\n apply_settings(instrument: instrument)\n end\n \n # Guides technician to apply settings to the selected wells. Wells are selected by sampleType (different sampleTypes require different settings)\n def apply_settings(instrument:)\n log_info 'instrument.experimental_item.object_type.name.downcase', instrument.experimental_item.object_type.name.downcase\n if instrument.experimental_item.object_type.name.downcase.include? 'bead'\n sample_type_name = instrument.experimental_item.sample.sample_type.name.to_sym\n else\n sample_type_name = collection_from(instrument.experimental_item).matrix.flatten.uniq.reject {|sid| sid == -1 }.map {|sid| s = Sample.find(sid); s.sample_type.name }.uniq.first.to_sym\n end\n show do\n title \"Apply Settings #go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n image instrument.software.fetch(:images).fetch(:apply_settings)\n note \"Apply the following settings to the wells you have selected\"\n note \"\u003cb\u003eMake sure that the settings are as follows:\u003c/b\u003e\"\n instrument.software.fetch(:sample_type_settings).fetch(sample_type_name).each {|setting, val| bullet \"Set \u003cb\u003e#{setting}\u003c/b\u003e to \u003cb\u003e#{val}\u003c/b\u003e\"}\n check \"Finally, click \u003cb\u003eApply Settings\u003c/b\u003e\"\n bullet \"Save experimental measurement as \u003cb\u003e#{get_experiment_filename(instrument: instrument)}\u003c/b\u003e\"\n end\n end\n \n #Guides technician through software to export, save, and upload data to Aq\n def export_save_and_upload_measurement_data(instrument:, timepoint:)\n sw = instrument.software\n day = timepoint.strftime \"%m%d%Y\"\n hour = timepoint.strftime \"%H%M\"\n # Export\n fcs_exports = show do \n title \"Export .fcs files #go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note 'Make sure that flow cytometer run is \u003cb\u003eDONE!\u003c/b\u003e'\n note 'Press \u003cb\u003eCLOSE RUN DISPLAY\u003c/b\u003e'\n note 'Select \u003cb\u003eFile\u003c/b\u003e =\u003e \u003cb\u003eExport ALL Samples as FCS...\u003c/b\u003e (see below)'\n image instrument.software.fetch(:images).fetch(:export_new_data)\n note 'You will see a pop-up like below, record the directory ID #'\n image instrument.software.fetch(:images).fetch(:new_export_directory)\n get 'text', var: 'dirname', label: 'Enter the name of the export directory in Desktop/FCS Exports/'\n end\n # UPLOAD\n if (!debug) \n attempt = 0\n up_show, up_sym = {}, :file \n while (up_show[up_sym].nil?) || (attempt == 3) do\n up_show, up_sym = upload_directory_show(saving_dir: sw[:saving_directory]+\"/#{fcs_exports[:dirname]}\")\n attempt += 1\n end\n upload_array = get_upload_array_from_show(up_show: up_show, up_sym: up_sym)\n else\n log_info 'UPLOADS Array debug'\n upload_array = [Upload.find(26482), Upload.find(26458)]\n end\n measurement_data = {\n mitem: instrument.measurement_item,\n day: day,\n hour: hour,\n uploads: upload_array\n }\n return measurement_data\n end\n \n # Guides technician through measuring the diluted calibration beads\n def read_calibration(instrument: instrument)\n go_to_computer(instrument)\n tube_rack = get_tube_rack\n show do\n title \"Load #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}\"\n note \"#go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Click \u003cb\u003eEject Plate\u003c/b\u003e\"\n check \"Open tube #{instrument.measurement_item} then, place the tube into the first well of the #{instrument.software.fetch(:plate_type_hash)[instrument.measurement_item.object_type.name.to_sym]}.\" \n bullet \"Well A1 should be by the red sticker in the top left corner.\"\n table highlight_alpha_rc(tube_rack, [[0,0]]) {|r,c| \"#\"}\n check \"Finally, load the #{instrument.measurement_item.object_type.name} and continue to the next step.\"\n end\n tube_rack.mark_as_deleted # Collection created for display, then deleted\n show do \n title \"Taking Measurements\"\n note \"#go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Click \u003cb\u003eOPEN RUN DISPLAY\u003c/b\u003e\"\n image instrument.software.fetch(:images).fetch(:read_plate)\n note \"Next, click \u003cb\u003eAUTORUN\u003c/b\u003e\"\n note \"Contiue on to the next step while #{instrument.type} #{instrument.instrument_type} is running...\"\n end\n instrument.measurement_item.location = instrument.type.to_s\n timepoint = Time.now\n return timepoint\n end\n \n\n # Guides technician through measuring the experimental culture plate\n def read_plate(instrument:)\n go_to_computer(instrument)\n show do\n title \"Load #{instrument.measurement_item.object_type.name} #{instrument.measurement_item}\"\n note \"#go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Click \u003cb\u003eEject Plate\u003c/b\u003e\"\n note \"Be sure that the plate is in the correct orientation. Well A1 should be by the red sticker in the top left corner.\"\n check \"Finally, load the plate and continue to the next step.\"\n end\n show do \n title \"Taking Measurements\"\n note \"#go to FlowCytometrySoftware library Attune module to customize this step\"\n separator\n note \"Click \u003cb\u003eOPEN RUN DISPLAY\u003c/b\u003e\"\n image instrument.software.fetch(:images).fetch(:read_plate)\n note \"Next, click \u003cb\u003eAUTORUN\u003c/b\u003e\"\n note \"Contiue on to the next step while #{instrument.type} #{instrument.instrument_type} is running...\"\n end\n instrument.measurement_item.location = instrument.type.to_s\n timepoint = Time.now\n return timepoint\n end\n \n # Guides techician to read the calibration sample, then export, save, and upload data to Aq\n def take_calibration_and_upload_data(instrument:)\n timepoint = read_calibration(instrument: instrument)\n instrument.measurement_data = export_save_and_upload_measurement_data(instrument: instrument, timepoint: timepoint)\n end\n\n # Guides techician to read the culture plate, then export, save, and upload data to Aq\n def take_measurement_and_upload_data(instrument:)\n timepoint = read_plate(instrument: instrument)\n instrument.measurement_data = export_save_and_upload_measurement_data(instrument: instrument, timepoint: timepoint)\n end\n\nend # module HaaseLabAttune\n\n\n"}},{"library":{"name":"HighThroughputHelper","category":"High Throughput Culturing","code_source":"# By: Eriberto Lopez\n# [email protected]\n# Updated: 071519\n\nneeds \"Tissue Culture Libs/CollectionDisplay\"\nneeds \"Standard Libs/Units\"\nneeds \"Plate Reader/PlateReaderHelper\"\n\nmodule HTCExperimentalDesign\n include Units\n include AssociationManagement\n \n # Place cultures determined to be controls into the collection specified\n # \n # @param cultures [Array] of instances of class CuitureComposition\n # @param collection [Collection] is an Aq item that is a collection of part items\n def associate_controls_to_collection(cultures:, collection:)\n collection_associations = AssociationMap.new(collection)\n empty_wells = collection.get_empty\n column_samples = cultures.map {|column| Sample.find_by_name(column.first.fetch(\"Strain\").keys.first) }\n cultures.each_with_index do |replicates, idx| \n replicates.each do |culture| \n r, c = empty_wells.shift()\n collection.set(r, c, column_samples[idx])\n collection.save()\n culture.keys.each do |key|\n collection_associations.putrc(row=r, column=c, key=key, data=culture[key])\n end\n end\n end\n collection_associations.save()\n end\n \n # Place sorted cultures into new collection \n #\n # @param cultures [Array] of sorted arrays of culture composition hash objects \n # @param object_type [ObjectType] is a virtual representation of a container\n # @returns new_collections [Array] of Collection containrs filled with sorted and organized experimetnal cultures\n def associate_cultures_to_collection(cultures:, object_type:)\n # Now each one of the matricies is ready to be associated with a collection\n uniq_strains = {}\n new_collections = []\n format_to_collection_type(cultures: cultures, object_type: object_type).each_with_index do |formatted_matrix, idx|\n collection = produce new_collection object_type.name\n sname_matrix = formatted_matrix.map {|culture| (culture == EMPTY) ? culture : culture.fetch(\"Strain\").keys.first }\n sname_matrix.to_a.flatten.uniq.reject {|sname| sname == EMPTY }.each {|sname| uniq_strains[sname] = Sample.find_by_name(sname).id unless uniq_strains.keys.include? sname} \n sid_matrix = sname_matrix.map {|sname| uniq_strains[sname]}\n collection.matrix = sid_matrix.to_a; collection.save()\n collection_associations = AssociationMap.new(collection)\n formatted_matrix.to_a.each_with_index do |row, r_i|\n row.each_with_index do |culture, c_i|\n if culture != EMPTY\n culture.keys.each do |key|\n collection_associations.putrc(row=r_i, column=c_i, key, data=culture[key])\n end\n else\n EMPTY\n end\n end\n end\n collection_associations.save()\n new_collections.push(collection)\n end\n return new_collections\n end\n \n # Format incomplete slices, to avoid class Matrix dimension errors when vertically stacking matricies\n def format_slice(slice, columns)\n slice_width = slice.map {|s| s.length}.uniq.first\n empty_slice = slice_width.times.map {|i| EMPTY }\n while slice.length != columns do\n slice.push(empty_slice)\n end\n return slice\n end\n \n # Sort \u0026 arrange cultures into a given ObjectType's dimensions\n #\n # @param cultures [Array] of sorted arrays of culture composition hash objects \n # @param object_type [ObjectType] is a container in Aq\n # @returns formatted_matrices []\n def format_to_collection_type(cultures:, object_type:, columnwise: false)\n sorted_cultures = sort_culture_composition_objs(cultures: cultures) # sorts CultureComposition replicate arrays by strain and then condition\n formatted_matricies = format_cultures_to_object_type(sorted_cultures: sorted_cultures, object_type: object_type)\n return formatted_matricies\n end \n \n # Sort replicate culture arrays by strain then by total moles of final concentration inducer \n #\n # @param cultures [Array] of arrays of culture composition hash objects \n # @returns sorted_cultures [Array] of sorted arrays of culture composition hash objects \n def sort_culture_composition_objs(cultures:)\n sorted_cultures = []\n groupby_strain_name = cultures.group_by {|rep_arr| rep_arr.first.fetch('Strain').keys.first }\n groupby_strain_name.map {|sname, rep_arrays|\n rep_arrays.sort! {|arr_a, arr_b| get_comparison(arr_a.first) \u003c=\u003e get_comparison(arr_b.first) }.each {|rep_array| sorted_cultures.push(rep_array) }\n }\n return sorted_cultures\n end\n \n # Slice sorted cultures array to fit the dimensions of the given collection\n def format_cultures_to_object_type(sorted_cultures:, object_type:)\n matrix_arr = sorted_cultures.each_slice(object_type.columns).map do |slice|\n slice = format_slice(slice, object_type.columns)\n Matrix.columns(slice) \n end\n # After the columns/reps have been sliced I want to prevent replicates from getting placed on different collections\n max_rows = object_type.rows\n stack_arr_idx = 0\n matricies_to_stack = Array.new(15) {[]} # TODO: 15 is arbitray, essentially max amount of plates per op\n matrix_arr.map do |m| \n max_rows -= m.row_count\n if max_rows \u003e 0\n matricies_to_stack[stack_arr_idx].push(m)\n else\n stack_arr_idx += 1\n matricies_to_stack[stack_arr_idx].push(m)\n max_rows = object_type.rows - m.row_count\n end\n end\n formatted_matricies = matricies_to_stack.select {|i| !i.empty? }.map {|matrix_arr| vstack_matrix_array(matrix_arr: matrix_arr) }\n return formatted_matricies\n end\n\n # Compare total number of moles in each culture, to sort each culture with the consideration of manual pipetting\n def get_comparison(args)\n comparison = []\n if args.fetch('Inducer(s)', nil).nil?\n comparison.push(0)\n else\n args.fetch('Inducer(s)').keys.each do |inducer_name| fconc_obj = args.fetch('Inducer(s)')[inducer_name][:final_concentration]\n comparison.push( (CultureComponent.unit_conversion_hash[fconc_obj[:units]]*fconc_obj[:qty]).to_f )\n end\n end\n return comparison\n end\n \n # Stack each matrix in the array vertically to fill matricies columwise. \n # For example, if you have a list of A, B, C replicates then, A would go into A1, B would go to B1, and C would go to C1.\n # Furthermore, the next set of triplicates D would go to A2, E would go to B2, and F would go to C2. However, replicates will not\n # be broken up, so a new stacked matrix will be created, which means a new collection will be used to avoid breaking up reps.\n def vstack_matrix_array(matrix_arr:)\n stacked_mat = nil\n matrix_arr.each {|m| stacked_mat.nil? ? stacked_mat = m : stacked_mat = stacked_mat.vstack(m) }\n return stacked_mat\n end\n \nend # module HTCExperimentalDesign\n\n\nmodule HighThroughputHelper\n include CollectionDisplay\n include HTCExperimentalDesign\n \n SATURATION_CULT_VOL = 300#ul\n \n # A method serches the culture component part associations and returns a matrix of values, base on the component type and attribute of component\n def search_part_associations(collection:, data_key:, attribute:)\n part_data_matrix = collection.data_matrix_values(data_key)\n part_data_matrix.map! {|row| row.map! {|part| part.nil? ? part : part.values.first.fetch(attribute) } }\n end\n \n # Determines what method to use for collection inoculation. Different methods when using different input object types. ie: Yeast Glycerol Stock vs. Yeast Plate\n def inoculate_culture_plates(new_output_collections:, inoculation_prep_hash:)\n item_ids = []; media_ids = []\n inoculation_prep_hash.each {|collection_id, item_media_hash| item_media_hash.each {|item_id, media_to_rc_list| item_ids.push(item_id); media_ids.push(media_to_rc_list.keys) } }\n uniq_input_items = Item.find(item_ids.flatten.uniq); uniq_media_items = Item.find(media_ids.flatten.uniq)\n input_item_hash = Hash.new(); input_media_hash = Hash.new()\n uniq_input_items.each {|item| input_item_hash[item.id] = item }; uniq_media_items.each {|item| input_media_hash[item.id] = item }\n gather_materials(empty_containers: new_output_collections, transfer_required: false, new_materials: [\"P1000 Multichannel Pipette\", \"Media Reservoir\", \"Aera Breathable Seals\"], take_items: uniq_media_items)\n uniq_input_items.group_by {|item| item.object_type.name }.each do |ot_name, items|\n case ot_name\n when 'Yeast Glycerol Stock', 'E coli Glycerol Stock', 'Yeast Overnight Suspension'\n tubeNum_hash = prep_glycerol_stock_inoculants(inoculation_prep_hash: inoculation_prep_hash, input_item_hash: input_item_hash, input_media_hash: input_media_hash)\n inoculate_glycerol_stock_inoculates(inoculation_prep_hash: inoculation_prep_hash, tubeNum_hash: tubeNum_hash)\n when 'Yeast Plate', 'E coli Plate of Plasmid'\n # Grab plates\n take items, interactive: true\n new_output_collections.each do |collection|\n inoculate_colonies_from_agar_plates(collection: collection, input_media_hash: input_media_hash, inoculation_prep_hash: inoculation_prep_hash)\n seal_plate(collection_id=collection.id)\n end\n when '96 Well PCR Plate' # Yeast Glycerol stock plate\n raise 'There are no steps for inoculating an experiment when coming from a Glycerol Stock Plate'.upcase\n else\n raise \"This object type #{ot_name} is not compatable to inoculate culture plate\"\n end\n end\n end\n \n # What to write on to plate seal\n def seal_plate(collection_id)\n show do \n title \"Seal Plate for Incubation\"\n separator\n check \"Grab a new \u003cb\u003eAera Breathable Seal\u003c/b\u003e\"\n note \"Label the seal with \"\n bullet \"Plate #{collection_id}\"\n bullet \"Today's date\"\n bullet \"Time of inoculation (ie: 3:00pm)\"\n bullet \"Your initials\"\n end\n end\n \n # Guides tech through inoculting collection using Yeast Plates as the inoculum. Each replicate is uses a single colony.\n def inoculate_colonies_from_agar_plates(collection:, input_media_hash:, inoculation_prep_hash:)\n media_part_matrix = search_part_associations(collection: collection, data_key: 'Media', attribute: 'item_id'); media_vol_hash = Hash.new(0)\n media_part_matrix.each {|row| row.reject {|i| i.nil? }.each {|media_id| media_vol_hash[media_id] += 1 } }\n show do\n title \"Fill #{collection.object_type.name} #{collection.id} with Media\"\n separator\n media_vol_hash.each do |media_id, count|\n media_vol_ml = (HighThroughputHelper.add_extra_vol(int: SATURATION_CULT_VOL*count)/1000.0).round(3)\n check \"You will need \u003cb\u003e#{media_vol_ml}#{MILLILITERS}\u003c/b\u003e of \u003cb\u003e#{input_media_hash[media_id].sample.name}\u003c/b\u003e media\" \n end\n note \"In the table, the item id of the media type is followed by the volume to dispense.\"\n note \"Follow the table below to fill with the appropiate media type \u0026 volume:\"\n table highlight_alpha_non_empty(collection) {|r,c| \"#{media_part_matrix[r][c]}\\n#{SATURATION_CULT_VOL}#{MICROLITERS}\"} \n end\n inoculation_prep_hash[collection.id].each do |input_item_id, media_to_rc_list|\n media_to_rc_list.each do |media_id, rc_list|\n show do\n title \"Inoculate #{collection} with Single Colonies\"\n separator\n note \"In the table, the item id of the agar plate is displayed. If the same item is used, try to pick a different biological replicate (single colony) for each well.\"\n note \"Follow the table below to inoculate a well with the appropriate item:\"\n table highlight_alpha_rc(collection, rc_list){|r,c| \"#{input_item_id}\"} \n end\n \n end\n end\n end\n \n # Generate a hash that groups [row, columen] coordinates by media type item and microbial sample item. \n # Uses culture composition components associated to collection part items to generate data structure\n def get_inoculation_prep_hash(new_output_collections)\n inoculation_prep_hash = nested_hash_data_structure\n new_output_collections.each do |collection|\n input_items_matrix = search_part_associations(collection: collection, data_key: 'Strain', attribute: 'item_id') \n media_items_matrix = search_part_associations(collection: collection, data_key: 'Media', attribute: 'item_id')\n input_items_matrix.each_with_index do |row, r_i| \n row.each_with_index do |input_item_id, c_i|\n if input_item_id.nil?\n next\n else\n media_item_id = media_items_matrix[r_i][c_i]\n if inoculation_prep_hash[collection.id].fetch(input_item_id, nil).nil?\n inoculation_prep_hash[collection.id][input_item_id][media_item_id] = [[r_i, c_i]]\n else\n if inoculation_prep_hash[collection.id][input_item_id].fetch(media_item_id, nil).nil?\n inoculation_prep_hash[collection.id][input_item_id][media_item_id] = [[r_i, c_i]]\n else\n inoculation_prep_hash[collection.id][input_item_id][media_item_id].push([r_i, c_i])\n end\n end\n end\n end\n end\n end\n return inoculation_prep_hash\n end\n \n # Guides technician through preparing inoculum from Yeast Glycerol Stocks\n def inoculate_glycerol_stock_inoculates(inoculation_prep_hash:, tubeNum_hash:)\n out_collections_arr = Collection.find(inoculation_prep_hash.keys)\n inoculation_prep_hash.each do |out_collection_id, inoculation_item_hash|\n collection = out_collections_arr.select {|c| c.id == out_collection_id }.first\n inoculation_item_hash.each do |input_item_id, media_rc_list_hash|\n media_rc_list_hash.each do |media_item_id, rc_list|\n show do\n title \"Inoculate #{collection} with Resuspended Inoculants\"\n separator\n note \"Follow the table below to transfer \u003cb\u003e#{SATURATION_CULT_VOL}#{MICROLITERS}\u003c/b\u003e from a resuspended inoculant to the corresponding well.\"\n bullet \"The numbers are the table correspond to the labels on the resuspension tubes.\"\n table highlight_alpha_rc(collection, rc_list) {|r,c| \"#{tubeNum_hash[input_item_id][media_item_id]}\"} \n end\n end\n end\n seal_plate(collection_id=out_collection_id)\n end\n end\n \n # Guides tech through labelling and resuspending glycerol stock sample into specified volume of media for inoculation.\n def prep_glycerol_stock_inoculants(inoculation_prep_hash:, input_item_hash:, input_media_hash:)\n tube_num = 1 \n tubeNum_hash = nested_hash_data_structure\n resuspension_hash = nested_hash_data_structure\n inoculation_prep_hash.each do |out_collection_id, inoculation_item_hash|\n inoculation_item_hash.each do |input_item_id, media_rc_list_hash|\n media_rc_list_hash.each do |media_item_id, rc_list|\n media_vol_ml = ((rc_list.length*SATURATION_CULT_VOL*1.1)/1000.0).round(3) # mL w/ 10%\n if (resuspension_hash[input_item_id][media_item_id]).instance_of? Hash\n resuspension_hash[input_item_id][media_item_id] = media_vol_ml \n else \n resuspension_hash[input_item_id][media_item_id] += media_vol_ml\n resuspension_hash[input_item_id][media_item_id].round(3)\n end\n if tubeNum_hash[input_item_id][media_item_id].instance_of? Hash\n tubeNum_hash[input_item_id][media_item_id] = tube_num\n tube_num += 1\n end\n end\n end\n end\n display_table = [['Tube Label #',\"Media Type\", \"Media Vol (#{MILLILITERS})\", 'Input Item ID','Item Location']] # Headers of table\n resuspension_hash.each do |input_item_id, media_volume_hash|\n media_volume_hash.each do |media_item_id, media_vol_ml|\n dtable_row = make_checkable(value: tubeNum_hash[input_item_id][media_item_id]).concat([input_media_hash[media_item_id].sample.name]).concat(\n make_checkable(value: media_vol_ml)).concat([input_item_id]).concat(make_checkable(value: input_item_hash[input_item_id].location)\n )\n display_table.push(dtable_row)\n end\n end\n show do\n title \"Resuspend Inoculants\"\n separator\n note \"Use the \u003cb\u003eTube Label #\u003c/b\u003e column to determine how many inoculants to prepare.\"\n note \"Use the \u003cb\u003eMedia Vol (#{MILLILITERS})\u003c/b\u003e column to determine the appropriate sized tube.\"\n check \"Gather tubes and label with tube label number\"\n check \"Next, fill tubes with the appropriate amount of media.\"\n check \"Finally, retrieve glycerol stock, take a sample using aseptic technique, then resuspend in pre-filled tube.\"\n note \"Follow the table below to resuspend the input item into the appropriate media.\"\n table display_table\n end\n return tubeNum_hash\n end\n \n def make_checkable(value:)\n if value.is_a? Array\n return value.map {|i| { content: i, check: true } }\n else\n return [value].map {|i| { content: i, check: true } }\n end\n end\n\n def copy_sample_matrix(from_collection:, to_collection:)\n sample_matrix = from_collection.matrix\n to_collection.matrix = sample_matrix\n to_collection.save()\n end\n \n # Only transfer part associations to avoid copying over global associations (things directly associated to the collection)\n def transfer_part_associations(from_collection:, to_collection:)\n copy_sample_matrix(from_collection: from_collection, to_collection: to_collection)\n from_collection_associations = AssociationMap.new(from_collection)\n to_collection_associations = AssociationMap.new(to_collection)\n from_associations_map = from_collection_associations.instance_variable_get(:@map)\n # Remove previous source data from each part, retain only value where the key is part_data\n from_associations_map.select! {|key| key == 'part_data' } # Retain only the part_data, so that global associations do not get copied over\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"source\") ? part.reject! {|k| k == \"source\" } : part } }\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"destination\") ? part.reject! {|k| k == \"destination\" } : part } }\n # Set edited map to the destination collection_associations\n to_collection_associations.instance_variable_set(:@map, from_associations_map) # setting it to the associations @map will push the part_data onto each part item automatically\n to_collection_associations.save()\n return from_associations_map\n end \n \n # Generate source \u0026 destination associations for part provenenance reactor DARPA SD2\n def part_provenance_transfer(from_collection:, to_collection:, process_name:)\n to_collection_part_matrix = to_collection.part_matrix\n from_collection.part_matrix.each_with_index do |row, r_i|\n row.each_with_index do |from_part, c_i|\n if from_part\n to_part = to_collection_part_matrix[r_i][c_i]\n # Create source and destination objs\n source_id = from_part.id; source = [{id: source_id }]\n destination_id = to_part.id; destination = [{id: destination_id }]\n # raise process_name.inspect unless process_name.nil?\n destination.first.merge!({additional_relation_data: { process: process_name }}) unless process_name.nil?\n # Association source and destination\n to_part.associate(key=:source, value=source)\n from_part.associate(key=:destination, value=destination)\n end\n end\n end\n end\n \n # Transfer every non empty well of a collection, keep culture composition association, and create new part provenance associations\n def stamp_transfer(from_collection:, to_collection:, process_name: nil)\n from_associations_map = transfer_part_associations(from_collection: from_collection, to_collection: to_collection)\n part_provenance_transfer(from_collection: from_collection, to_collection: to_collection, process_name: process_name)\n return from_associations_map.fetch('part_data')\n end\n \n # Addup total volume for a given component type found in a part_data matrix (a matrix of culture compostion part associations)\n def get_component_volume_hash(matrix:, component_type:)\n volume_hash = Hash.new(0)\n matrix.each do |culture_array|\n culture_array.each do |culture|\n component = culture.fetch(component_type, nil)\n if component\n attributes = component.values.first\n item_id = attributes.fetch(:item_id, nil)\n if item_id.nil?\n next\n else\n volume_hash[item_id] += attributes.fetch(:working_volume).fetch(:qty)\n end\n end\n end\n end\n return volume_hash\n end\n \n # Compute how much volume to transfer to reach a given dilution factor and also consider the volumes of the other culture components (ie: inducers, antibiotics, etc...)\n def get_transfer_volume_matrix(collection:, part_associations_matrix:, dilution_factor:)\n transfer_vol_matrix = Array.new(collection.object_type.rows) { Array.new(collection.object_type.columns) { -1 } }\n collection = collection_from(collection)\n collection.get_non_empty.each do |r, c|\n culture_volume = part_associations_matrix[r][c].fetch(\"Culture_Volume\", nil)\n if culture_volume.nil?\n culture_volume\n else\n transfer_volume = (dilution_factor*culture_volume[:qty].to_f).round(3)\n transfer_vol_matrix[r][c] = transfer_volume\n end\n end\n return transfer_vol_matrix\n end\n \n def clean_up(item_arr: [uniq_media_items, uniq_input_items])\n item_arr.flatten.group_by {|i| i.object_type.name }.each {|otname, items| release items, interactive: true }\n show do \n title \"Cleaning Up..\"\n separator\n note \"Make sure that all materials and equiptment used is put away and cleaned before finishing.\"\n end\n end\n \n # Set the location of the new output collections to the same incubator that its source plate came from.\n def incubate_plates(output_collections:, growth_temp:)\n incubation_location =\"Incubator at #{growth_temp.to_i}#{DEGREES_C}\"\n if output_collections.is_a? Array\n output_collections.each {|collection| collection.location = incubation_location }\n release output_collections, interactive: true\n else\n output_collections.location == incubation_location\n release [output_collections], interactive: true\n end\n end\n \n # Get and format dilution factor from user specified FieldValue\n def get_dilution_factor(op:, fv_str:)\n param_val = get_parameter(op: op, fv_str: fv_str).to_s\n dilution_factor = (param_val == 'None') ? param_val : param_val.chomp('X').to_f\n end\n \n def get_parameter(op:, fv_str:)\n op.input(fv_str).val\n end\n \n def update_matrix(matrix:, \u0026block)\n matrix.map! {|row| row.map! {|part_data| yield part_data } }\n end\n \n # Generate part data objects to alter or edit \n def update_part_data_matrix(collection:, \u0026block)\n collection_associations = AssociationMap.new(collection_from(collection))\n part_data_matrix = collection_associations.instance_variable_get(:@map).fetch('part_data', Array.new(collection.object_type.rows) { Array.new(collection.object_type.columns) { Hash.new() }})\n update_matrix(matrix: part_data_matrix) {|part_data| yield part_data }\n collection_associations.instance_variable_set(:@map, {'part_data'=\u003epart_data_matrix})\n collection_associations.save()\n end\n\n # Gather materials. label if they are empty containers, display if they are materials are equiptement, and take items that are represented in Aq\n def gather_materials(empty_containers: [], transfer_required: false, new_materials: [], take_items: [])\n new_materials = new_materials.select {|m| !Protocol.materials_list.include? m}\n if !empty_containers.empty? || !new_materials.empty?\n show do\n title \"Gather The Following Materials\"\n separator\n note \"Gather the following and bring them to your bench:\"\n empty_containers.each {|c|\n ((c.is_a? Item) || (c.is_a? Collection)) ? (check \"#{c.object_type.name} and label as \u003cb\u003e#{c.id}\u003c/b\u003e\") : (check \"#{c}\")\n }\n new_materials.each {|m| check \"#{m}\" }\n end \n end\n take take_items, interactive: true unless take_items.length \u003c= 0\n Protocol.materials_list.concat(new_materials).concat(empty_containers)\n end\n \n # Allows for multiple key deeply nested hash[:item][:measurement][:day][:hour] = val\n def nested_hash_data_structure\n Hash.new { |hash, key| hash[key] = Hash.new(\u0026hash.default_proc) }\n end\n \n def get_uninitialized_output_fv_object_type_id(op)\n AllowableFieldType.find(op.outputs[0].allowable_field_type_id).object_type_id\n end\n \n def get_uninitialized_output_object_type(op)\n oti = get_uninitialized_output_fv_object_type_id(op)\n return ObjectType.find(oti)\n end\n \n def self.add_extra_vol(int:, additional_percent: 0.1)\n return (int*(1+additional_percent)).round(3)\n end\n \n # Convert alpha numeric string to a [r,c] tuple\n def get_rc_from_alpha_coords(alpha_coord:)\n abc = ('A'..'H').to_a\n row = abc.find_index(alpha_coord.split('').shift.upcase)\n col = alpha_coord[1..alpha_coord.length].to_i - 1\n return row, col\n end\n \n # Generate a matrix of alpha numeric coordinates for a 96 well plate\n def alpha_coordinates_96\n abc = ('A'..'H').to_a\n empty_matrix = Array.new(8) { Array.new(12) { EMPTY } }\n alpha_numeric_matrix = empty_matrix.each_with_index.map do |row, r_i|\n row.each_with_index.map do |col, c_i|\n \"#{abc[r_i]}#{c_i+1}\"\n end\n end\n return alpha_numeric_matrix\n end\nend # module HighThroughputHelper\n\n"}},{"library":{"name":"InstrumentHelper","category":"High Throughput Culturing","code_source":"# Common instrument class methods. Try to find common methods in instrument classes.\nmodule InstrumentHelper\n attr_accessor :measurement_type, :experimental_item, :measurement_item, :transfer_required, :measurement_data\n def setup_experimental_measurement(experimental_item:, output_fv:)\n @experimental_item = experimental_item\n transfer?(output_fv: output_fv)\n end\n \n def transfer?(output_fv:)\n if transfer_needed\n output_fv.make\n @measurement_item = output_fv.item\n else\n @measurement_item = experimental_item\n end\n end\n \n def transfer_needed\n @transfer_required = !valid_container?\n end\n \n def self.intrument_type?\n self.class.to_s\n end\nend\n"}},{"library":{"name":"PlateReaderCalibration","category":"High Throughput Culturing","code_source":"module PlateReaderCalibration\n include Units, Debug, AssociationManagement\n # Constants\n DILUTANT_VOL = 100 # Final vol that each flourescence cal well will have\n FLOUR_ALIQUOT_CONC = 50#uM # The starting concentration of the flour std curve\n \n # Prepare iGEM plate reader calibration plate\n def prep_calibration_plate(pr, ops, out_fv_str, flour_item, optical_item)\n new_mtype = true \n create_a_new_cal_plt, reusable_plate_item = check_for_reusable_plate(ops.running.map {|op| op.operation_type}.uniq.first)\n create_a_new_cal_plt ? ops.make : reusable_plate_item\n additional_solutions = create_a_new_cal_plt ? [get_pbs_item, get_water_item] : []\n ops.each do |op|\n create_a_new_cal_plt ? op.output(out_fv_str).item : op.output(out_fv_str).set(opts={item: reusable_plate_item})\n pr.setup_experimental_measurement(experimental_item: op.output(out_fv_str).item, output_fv: op.output(out_fv_str))\n new_mtype = setup_plate_reader_software_env(pr: pr, new_mtype: new_mtype)\n # Materials based on whether a new calibration plate will be filled\n take_items = create_a_new_cal_plt ? [flour_item, optical_item].concat(additional_solutions) : [pr.measurement_item]\n new_materials = create_a_new_cal_plt ? ['P1000 Multichannel', 'P200 Pipette'] : ['P20 Multichannel']\n empty_containers = create_a_new_cal_plt ? [pr.measurement_item] : []\n gather_materials(empty_containers: empty_containers, new_materials: new_materials, take_items: take_items)\n if create_a_new_cal_plt # then fill empty container and add inputs to op\n tech_fill_calibration_plate(calibration_plate: pr.measurement_item, water_item: get_water_item, flour_item: flour_item, optical_item: optical_item)\n # additional_solutions.each {|item| add_solution_to_op(op: op, fv_str: \"#{item.sample.name} Aliquot\", item: item) }\n op.operation_type.associate(:calibration_plate, {date_created: todays_date, item_id: pr.measurement_item.id})\n else # reuse the unexpired calibration plate\n equilibrate_calibration_plate(op: op, calibration_solutions: calibration_solutions)\n end\n end\n return new_mtype\n end\n \n # Check to see if there are any calibration plates that are not older than a month\n def check_for_reusable_plate(op_type)\n create_a_new_cal_plt = true\n calibration_plate = nil # if the plate is less than a month old use the cal plate\n op_type.data_associations.select {|da| da[:key] == :calibration_plate }.map do |da|\n key, ot_obj = da[:key], da[:object]\n obj = ot_obj[key]\n date_created, present, plus_month = obj[:date_created], todays_date, [date_created[0..1], date_created[2..3], date_created[4..7]].map {|i| i.to_i}\n plus_month[0] = plus_month[0] + 1\n date_created = [date_created[0..1], date_created[2..3], date_created[4..7]].map {|i| i.to_i}\n if date_created[0] == plus_month[0] # Checking month\n if plus_month[1] \u003e= date_created[1] # Checking day\n create_a_new_cal_plt = true\n expired_plate = Item.find(obj[:item_id])\n show {check \"Before creating a new Calibration plate, throw away the expired plate #{expired_plate} found at #{expired_plate.location}\"}\n expired_plate.mark_as_deleted\n else\n calibration_plate = Item.find(obj[:item_id])\n create_a_new_cal_plt = false\n end\n else\n calibration_plate = Item.find(obj[:item_id])\n create_a_new_cal_plt = false\n end\n end\n return create_a_new_cal_plt, calibration_plate\n end\n \n # Techician retrieves reused calibration plate and equilibrates it to room temperature before measuring\n def equilibrate_calibration_plate(op:, calibration_solutions:)\n plate = op.outputs[0].collection\n cs_wells = get_calibration_solution_wells(calibration_solutions: calibration_solutions, collection: plate)\n show do\n title \"Equilibrating Calibration Plate #{plate}\"\n separator\n check \"Let the #{plate} plate sit at room temperature (25#{DEGREES_C}) for 10 minutes to avoid condensation skewing the calibration.\"\n note \"\u003cb\u003eAfter the timer is up:\u003c/b\u003e\"\n note \"Using a multichannel pipette, resuspend the highlighted wells:\" \n table highlight_alpha_rc(collection_from(plate), cs_wells[op.input(\"Optical Particles\").sample.name])\n check \"Use a kimwipe to remove condensation from the top and bottom of the plate.\"\n end\n end\n \n # Generate a hash describing which [r,c] a given sample is in, used for displaying\n def get_calibration_solution_wells(calibration_solutions:, collection:)\n cs_wells = {}\n calibration_solutions.map {|i| cs_wells[i.sample.name] = get_sample_wells(collection: collection, sample: i.sample) }\n return cs_wells\n end\n \n def get_pbs_item\n get_pbs_sample.items.select {|i| i.location != 'deleted' }[0]\n end\n \n def get_pbs_sample\n Sample.find_by_name('PBS')\n end\n \n def get_water_item\n get_water_sample.items.select {|i| i.location != 'deleted' }[0]\n end\n \n def get_water_sample\n h2o_type = \"Nuclease-free water\" # Change in Production Aq to Mol grade H2O\n h2o_samp = Sample.find_by_name(h2o_type)\n end\n \n def get_stock_solution_concentration(ot:)\n name = ot.name.split(' ')[0]\n units = name[-2..-1]\n stock_conc = name.match /(?\u003cconc\u003e\\d+)/\n return stock_conc[:conc].to_i, units\n end\n \n def dilute_flourescence_item(flour_item:)\n flour_ot = ObjectType.find(flour_item.object_type_id)\n conc, units = get_stock_solution_concentration(ot: flour_ot)\n case units\n when MILLIMOLAR\n dilution_factor = ((conc*1000) / FLOUR_ALIQUOT_CONC)\n when MICROMOLAR\n dilution_factor = (conc*100)/FLOUR_ALIQUOT_CONC\n else\n raise \"The current flourescence #{flour_ot} does not contain information about the concentration. \n please create a new container that will have a name with the concentraiton of the reagent\".upcase\n end\n flour_stk_vol = 1000/dilution_factor\n pbs_vol = 1000 - flour_stk_vol\n show do\n title \"Dilute #{flour_item} #{flour_item.sample.name}\"\n separator\n note \"Vortex item #{flour_item.id} #{flour_ot.name} and make sure there are no precipitates.\"\n check \"In a fresh 1.5mL Eppendorf tube, dilute #{flour_stk_vol}#{MICROLITERS} of #{flour_ot.name} into #{pbs_vol}#{MICROLITERS} of 1X PBS - Final Concentration [#{FLOUR_ALIQUOT_CONC}#{MICROMOLAR}]\"\n note \"Make sure to vortex.\"\n end\n end\n \n # Set iGEM fluorecein salt dilutions, ludox aliquots, and water aliquots to their template location in the calibration plate\n def set_calibration_plate_sample_matrix(calibration_plate:, flour_item:, water_item:, optical_item:)\n calibration_plate = collection_from(calibration_plate)\n rows, cols = calibration_plate.object_type.rows, calibration_plate.object_type.columns\n new_matrix = Array.new(rows) { Array.new(cols) { -1 } }\n rows.times do |r|\n cols.times do |c|\n if r \u003c 4\n new_matrix[r][c] = flour_item.sample.id \n elsif r == 4\n new_matrix[r][c] = optical_item.sample.id\n elsif r == 5\n new_matrix[r][c] = water_item.sample.id\n end\n end\n end\n calibration_plate.matrix = new_matrix\n calibration_plate.save\n return calibration_plate\n end\n \n # Guide tech to fill a new calibration plate with fresh calibration solutions\n def tech_fill_calibration_plate(calibration_plate:, flour_item:, water_item:, optical_item:)\n dilute_flourescence_item(flour_item: flour_item)\n set_calibration_plate_sample_matrix(calibration_plate: calibration_plate, flour_item: flour_item, water_item: water_item, optical_item: optical_item)\n flourescence_serial_dilution(calibration_plate: calibration_plate, flour_item: flour_item)\n [optical_item, water_item].each {|i| fill_calibration_plate_with_optical_solution(calibration_plate: calibration_plate, solution_item: i)}\n # Create associations if a new calibration plate is made and filled\n associate_data(object=calibration_plate, key=\"calibration_plate\", data = {date_created: todays_date}, opts = {})\n end\n \n # Guide tech through creating a fluorecein salt serial dilution\n def flourescence_serial_dilution(calibration_plate:, flour_item:) # iGEM Protocol 2018\n dilutant_wells = collection_from(calibration_plate).select {|well| well == flour_item.sample.id }.select {|r,c| c != 0}\n # direct tech to fill new calibration plate\n show do\n title \"Creating a New #{calibration_plate} Calibration Plate\"\n separator\n note \"You will need \u003cb\u003e#{(dilutant_wells.length * 0.1) + 0.1}mL\u003c/b\u003e of 1X PBS for the next step.\"\n note \"Follow the table below to dispense 1X PBS in the appropriate wells:\"\n table highlight_rc(calibration_plate, dilutant_wells) {|r,c| \"#{DILUTANT_VOL}#{MICROLITERS}\"}\n end\n flour_serial_image = \"Actions/Yeast_Gates/plateReaderImages/flour_serial_dilution.png\"\n show do\n title \"Serial Dilution of #{flour_item} #{flour_item.sample.name}\"\n separator\n note \"From the #{FLOUR_ALIQUOT_CONC}#{MICROMOLAR} #{flour_item.sample.name} solution, dispense \u003cb\u003e#{DILUTANT_VOL+DILUTANT_VOL}#{MICROLITERS}\u003c/b\u003e in wells \u003cb\u003eA1, B1, C1, D1\u003c/b\u003e\"\n note \"Following the image below, transfer \u003cb\u003e#{DILUTANT_VOL}#{MICROLITERS}\u003c/b\u003e of #{FLOUR_ALIQUOT_CONC}#{MICROMOLAR} #{flour_item.sample.name} solution in Column 1 to Column 2\"\n note \"Resuspend by pipetting up and down 3X\"\n note \"Repeat until column 11 and discard the remaining \u003cb\u003e#{DILUTANT_VOL}#{MICROLITERS}\u003c/b\u003e.\"\n image flour_serial_image\n end\n end\n \n # Guide tech through filling calibration plate with ludox optical particle solution\n def fill_calibration_plate_with_optical_solution(calibration_plate:, solution_item:)\n plate = collection_from(calibration_plate)\n rc_list = plate.select {|well| well == solution_item.sample.id}\n show do\n title \"Filling #{calibration_plate} Calibration Plate\"\n separator\n note \"Follow the table below to dispense \u003cb\u003e#{solution_item.sample.name}\u003c/b\u003e into the appropriate wells.\"\n table highlight_rc(plate, rc_list) {|r,c| optical_solution_vol(r, c)}\n end\n end\n \n # Determine how much volume of solution is required for a given column\n def optical_solution_vol(row, col)\n if col \u003c 4\n return \"#{100}#{MICROLITERS}\"\n elsif col.between?(4, 7)\n return \"#{200}#{MICROLITERS}\"\n else col.between?(7, 11)\n return \"#{300}#{MICROLITERS}\"\n end\n end\n \n def todays_date\n DateTime.now.strftime(\"%m%d%Y\")\n end\n \n # The plotted result of this method can be fit to a curve\n # to be used for calibrating the plate reader. This is very specific to the\n # Eriberto's calibration of the biotek plate reader.\n #\n # @param upload [Upload] the object whihc can be resolved to calibration csv\n # @return [Hash] a hash containing averaged measurements for\n # \t\t\t\t\tevery concentration and volume tested\n #\n #\n #\n # New Description needed - some refactoring may be necessary, but works! \n def get_calibration_hash(dm:, measurement_type:)\n result = {}\n data_by_conc = Hash.new { |h, key| h[key] = [0, 0] }\n case measurement_type\n when :Calibration_Green_Fluorescence\n starting_concentration = 50.0#uM\n # first 4 rows are serial dilutions\n for i in 0...4\n 12.times do |j|\n if j == 11\n this_conc = 0\n else\n # each column is a 2x dilution of the previous, starting at 50uM\n this_conc = starting_concentration / (2**j)\n end\n data = data_by_conc[this_conc]\n data[0] += dm[i][j].to_f\n data[1] += 1\n data_by_conc[this_conc] = data\n end\n end\n # add serial dilution averages to result hash\n data_by_conc.each_key do |k|\n data = data_by_conc[k]\n result[k] = data[0] / data[1]\n end\n when :Calibration_Optical_Density\n # row 5, 6 are lud dilutions and pure solution respectively\n for i in 4...6\n for j in 0...4\n data_by_conc[\"100_#{i}\"][0] += dm[i][j].to_f\n data_by_conc[\"100_#{i}\"][1] += 1\n end\n for j in 4...8\n data_by_conc[\"200_#{i}\"][0] += dm[i][j].to_f\n data_by_conc[\"200_#{i}\"][1] += 1\n end\n for j in 8...12\n data_by_conc[\"300_#{i}\"][0] += dm[i][j].to_f\n data_by_conc[\"300_#{i}\"][1] += 1\n end\n end\n # add lud averages to result hash\n for i in 1..3\n lud_avg = data_by_conc[\"#{i}00_4\"][0] / data_by_conc[\"#{i}00_4\"][1]\n sol_avg = data_by_conc[\"#{i}00_5\"][0] / data_by_conc[\"#{i}00_5\"][1]\n result[\"#{i}00\"] = (lud_avg - sol_avg).round(5) # Returns blanked averages\n end\n end\n return result\n end\n \n # This function creates a standard curve from the flourocein calibration plate\n #\n # @param coordinates [hash or 2D-Array] can be a hash or [[x,y],..] where x is known concentration \u0026 y is measurement of flouroscence\n #\n # @returns slope [float] float representing the slope of the regressional line\n # @returns yint [float] float representing where the line intercepts the y-axis\n # @returns x_arr [Array] a 1D array for all x coords\n # @returns y_arr [Array] a 1D arrya for all y coords\n def standard_curve(coordinates:)\n # Calculating Std Curve for GFP\n num_of_pts, a, x_sum, y_sum, x_sq_sum = 0, 0, 0, 0, 0\n x_arr, y_arr = [], []\n coordinates.each do |x, y|\n if x \u003c 25 # Above 25uM is out of linear range of our instrument\n a += (x*y)\n x_sum += x\n x_sq_sum += (x**2)\n y_sum += y\n x_arr.push(x)\n y_arr.push(y)\n num_of_pts += 1\n end\n end\n a *= num_of_pts\n b = x_sum * y_sum\n c = num_of_pts * x_sq_sum\n d = x_sum**2\n slope = (a - b)/(c - d)\n f = slope * (x_sum)\n yint = (y_sum - f)/num_of_pts\n # show{note \"y = #{(slope).round(2)}x + #{(yint).round(2)}\"}\n return (slope).round(3), (yint).round(3), x_arr, y_arr\n end \n \n # This function calculates how much deviation points are from a regressional line - R-squared Value \n # The closer it is to 1 or -1 the less deviation theres is\n #\n # @param slope [float] float representing the slope of the regressional line\n # @param yint [float] float representing where the line intercepts the y-axis\n # @param x_arr [Array] a 1D array for all x coords\n # @param y_arr [Array] a 1D arrya for all y coords\n #\n # @returns rsq_val [float] float representing the R-squared Value\n def r_squared_val(slope, yint, x_arr, y_arr)\n y_mean = y_arr.sum/y_arr.length.to_f\n # Deviation of y coordinate from the y_mean\n y_mean_devs = y_arr.map {|y| (y - y_mean)**2}\n dist_mean = y_mean_devs.sum # the sq distance from the mean\n # Finding y-hat using regression line\n y_estimate_vals = x_arr.map {|x| (slope * x) + yint }\n # Deviation of y-hat values from the y_mean\n y_estimate_dev = y_estimate_vals.map {|y| (y - y_mean)**2}\n dist_regres = y_estimate_dev.sum # the sq distance from regress. line\n rsq_val = (dist_regres/dist_mean).round(4)\n return rsq_val\n end\n \n def get_trendline_equation(coordinates_hash:)\n slope, yint, x_arr, y_arr = standard_curve(coordinates: coordinates_hash)\n r_sq = r_squared_val(slope, yint, x_arr, y_arr)\n trendline = \"y = #{slope}x + #{yint} (R^2 = #{r_sq})\"\n return trendline\n end\n \n # This fuction uses a reference od600 measurement to calculate the correction factor for different vols (100ul, 200, 300)\n # \n # @param hash [hash] is the hash of averaged blanked LUDOX samples at different volumes\n # @returns correction_val_hash [hash] is the hash containing the correction factor for the optical density (600nm) for this experiment\n def optical_correction_factors(hash)\n ref_od600 = 0.0425 #Taken from iGEM protocol - is the ref val of another spectrophotometer\n # ref/corrected vals\n correction_val_hash = Hash.new()\n hash.each do |vol, ave|\n correction_val_hash[vol[3..6]] = (ref_od600/ave).round(4)\n end\n return correction_val_hash\n end\n\n def get_calibration_calculated_values(cal_hash:, measurement_type:)\n case measurement_type\n when :Calibration_Green_Fluorescence\n return get_trendline_equation(coordinates_hash: cal_hash)\n when :Calibration_Optical_Density\n return optical_correction_factors(cal_hash)\n else\n raise \"this #{measurement_type} measurement type is not recognized as a calibration measurement\"\n end\n end\n \nend # module PlateReaderCalibration"}},{"library":{"name":"PlateReaderConstants","category":"High Throughput Culturing","code_source":"# Change LAB_NAME to use different plate reader constants\nLAB_NAME = 'Klavins Lab'.to_sym\nYOUR_LAB_NAME = 'YOUR_LAB_HERE'.to_sym\nPLATE_READER_TYPE = {'Klavins Lab':'Gen 5 BioTek'.to_sym,'YOUR_LAB_HERE':'YOUR_PLATE_READER_TYPE'.to_sym}\n\nMY_PLATE_READER_PROPERTIES = {\n 'Klavins Lab': {\n 'Gen 5 BioTek': {\n software_properties: {\n images: {\n open_software: \"Actions/Yeast_Gates/plateReaderImages/open_biotek.PNG\",\n read_plate: \"Actions/Yeast_Gates/plateReaderImages/begin_plate_reader.PNG\",\n export_new_data: \"Actions/Yeast_Gates/plateReaderImages/exporting_data_new.GIF\",\n export_data_button: \"Actions/Yeast_Gates/plateReaderImages/excel_export_button_new.png\",\n save_export: \"Actions/Yeast_Gates/plateReaderImages/saving_export_csv_new.png\"\n },\n export_mesurement_type: { \n \"Optical Density\": {dtype: 'Read 1:600'}, # Change the template to not blank the measurement data uploaded and exported\n \"Green Fluorescence\": {dtype: 'Read 2:485/20,516/20'}, ## Since we are not sure where the blank samples will be, we can use Aq to determine the OD of the \n 'Calibration Optical Density': {dtype: 'Read 1:600'}, ## blanking sample used and then use that to process the data when associating raw and true part item associations\n 'Calibration Green Fluorescence': {dtype: 'Read 2:485/20,516/20'}\n },\n measurement_type_templates: {\n 'Optical Density':'OD600_GFP_measurement',\n 'Green Fluorescence':'OD600_GFP_measurement',\n 'Optical Density \u0026 Green Fluorescence':'OD600_GFP_measurement',\n 'Calibration':'calibration_template_v1',\n 'Time Series':'create a new timeseries template'.upcase\n },\n saving_directory: 'FIND_OUT_THE_PATH/_UWBIOFAB'\n },\n valid_containers: ['96 Well Flat Bottom (black)', '24-Well TC Dish'],\n },\n },\n 'YOUR_LAB_HERE':{\n 'YOUR_PLATE_READER_TYPE':{ \n software_steps: 'properties', \n valid_containers: ['96 Well Flat Bottom (black)', '24-Well TC Dish'], \n },\n saving_directory: 'SAVING_DIRECTORY'\n }\n}\n"}},{"library":{"name":"PlateReaderHelper","category":"High Throughput Culturing","code_source":"# Eriberto Lopez\n# [email protected]\n# 07/23/19\n\nneeds \"Standard Libs/Debug\"\nneeds \"Standard Libs/Units\"\nneeds \"Standard Libs/AssociationManagement\"\nneeds \"High Throughput Culturing/InstrumentHelper\"\nneeds \"High Throughput Culturing/PlateReaderSoftware\"\nneeds \"High Throughput Culturing/PlateReaderConstants\"\nneeds \"High Throughput Culturing/PlateReaderCalibration\"\nneeds \"Tissue Culture Libs/CollectionDisplay\"\n\nclass PlateReader \n include InstrumentHelper\n attr_accessor :software_open\n attr_reader :type, :valid_containers, :software\n def initialize()\n @type = get_my_plate_reader_type\n @valid_containers = valid_containers\n @software = get_my_software_properties\n @software_open = false\n end\n \n def get_my_plate_reader_type\n PLATE_READER_TYPE[LAB_NAME]\n end\n\n def valid_containers\n get_my_plate_reader_properties_obj[type][:valid_containers]\n end\n \n def get_my_plate_reader_properties_obj\n MY_PLATE_READER_PROPERTIES[LAB_NAME]\n end\n \n def get_my_software_properties\n get_my_plate_reader_properties_obj[type][:software_properties]\n end\n \n def valid_container?\n if experimental_item.instance_of? Collection\n valid_containers.include? experimental_item.object_type.name.to_s\n elsif experimental_item.instance_of? Item\n valid_containers.map {|c| ObjectType.find_by_name(c).id.to_i}.include? experimental_item.object_type_id.to_i\n elsif experimental_item.instance_of? Array\n valid_container = false\n ot_arr = ObjectType.find(experimental_item.map {|item| item.object_type_id}.uniq).map {|ot| ot.name}\n ot_arr.each {|otn| (valid_containers.include? otn) ? (valid_container = true) : (valid_container) }\n valid_container\n else\n raise \"This type of #{experimental_item.class} object is not compatible with this instrument\"\n end\n end\nend # Class PlateReader \n\nmodule PlateReaderHelper\n include Units, Debug, AssociationManagement\n include CollectionDisplay\n include PlateReaderCalibration\n \n def intro\n plate_reader = PlateReader.new\n show do\n title \"Plate Reader Measurements\"\n separator\n note \"This protocol will instruct you on how to take measurements on the #{plate_reader.type} Plate Reader.\"\n note \"Optical Density is a quick and easy way to measure the growth rate of your cultures.\"\n note \"Green Fluorescence helps researchers assess a response to a biological condition \u003ci\u003ein vivo\u003c/i\u003e.\"\n note \"\u003cb\u003e1.\u003c/b\u003e Setup #{plate_reader.type} Plate Reader Software workspace.\"\n note \"\u003cb\u003e2.\u003c/b\u003e Check to see if input item is a #{plate_reader.valid_containers} if not, transfer samples to a valid container.\"\n note \"\u003cb\u003e3.\u003c/b\u003e Prepare measurement item with blanks.\"\n note \"\u003cb\u003e4.\u003c/b\u003e Take measurement, export data, \u0026 upload.\"\n end\n get_plate_reader_software(plate_reader: plate_reader)\n return plate_reader\n end\n \n # when PlateReaderHelper is included into class Protocol we can use the class method .include() \n # to include the required software module dynamically to the Protocol. \n # Which allows us to use methods found in the software module.\n def get_plate_reader_software(plate_reader:)\n case plate_reader.type\n when 'Gen 5 BioTek'.to_sym\n Protocol.include(KlavinsLabPlateReaderSoftware)\n when 'YOUR_LABS_PLATE_READER_MODULE'.to_sym\n Protocol.include(YourSoftwareSteps)\n else\n raise \"the #{plate_reader.type} plate reader in the #{LAB_NAME} has no software steps associated to it, create a module with steps to use plate reader\".upcase\n end\n end\n \n # If a dilution is required then find how much media an culture are required given the working volume of the container.\n def get_culture_and_media_vols(dilution_factor:, measurement_item:)\n working_vol = get_object_type_data(collection: measurement_item).fetch('working_vol').split('_').first.to_f\n cult_vol_ul = (dilution_factor == 'None') ? working_vol : dilution(dilution_factor: dilution_factor, vol: working_vol)\n media_vol_ul = working_vol - cult_vol_ul\n return media_vol_ul, cult_vol_ul\n end\n \n def copy_sample_matrix(from_collection:, to_collection:)\n sample_matrix = from_collection.matrix\n to_collection.matrix = sample_matrix\n to_collection.save()\n end\n \n def transfer_part_associations(from_collection:, to_collection:)\n copy_sample_matrix(from_collection: from_collection, to_collection: to_collection)\n from_collection_associations = AssociationMap.new(from_collection)\n to_collection_associations = AssociationMap.new(to_collection)\n from_associations_map = from_collection_associations.instance_variable_get(:@map)\n # Remove previous source data from each part, retain only value where the key is part_data\n from_associations_map.select! {|key| key == 'part_data' } # Retain only the part_data, so that global associations do not get copied over\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"source\") ? part.reject! {|k| k == \"source\" } : part } }\n from_associations_map.fetch('part_data').map! {|row| row.map! {|part| part.key?(\"destination\") ? part.reject! {|k| k == \"destination\" } : part } }\n # Set edited map to the destination collection_associations\n to_collection_associations.instance_variable_set(:@map, from_associations_map) # setting it to the associations @map will push the part_data onto each part item automatically\n to_collection_associations.save()\n return from_associations_map\n end \n \n def part_provenance_transfer(from_collection:, to_collection:, process_name:)\n to_collection_part_matrix = to_collection.part_matrix\n from_collection.part_matrix.each_with_index do |row, r_i|\n row.each_with_index do |from_part, c_i|\n if from_part\n to_part = to_collection_part_matrix[r_i][c_i]\n # Create source and destination objs\n source_id = from_part.id; source = [{id: source_id }]\n destination_id = to_part.id; destination = [{id: destination_id }]\n # raise process_name.inspect unless process_name.nil?\n destination.first.merge!({additional_relation_data: { process: process_name }}) unless process_name.nil?\n # Association source and destination\n to_part.associate(key=:source, value=source)\n from_part.associate(key=:destination, value=destination)\n end\n end\n end\n end\n \n def stamp_transfer(from_collection:, to_collection:, process_name: nil)\n from_associations_map = transfer_part_associations(from_collection: from_collection, to_collection: to_collection)\n part_provenance_transfer(from_collection: from_collection, to_collection: to_collection, process_name: process_name)\n return from_associations_map.fetch('part_data')\n end\n \n def update_matrix(matrix:, \u0026block)\n matrix.map! {|row| row.map! {|part_data| yield part_data } }\n end\n \n def update_part_data_matrix(collection:, \u0026block)\n collection_associations = AssociationMap.new(collection_from(collection))\n part_data_matrix = collection_associations.instance_variable_get(:@map).fetch('part_data', Array.new(collection.object_type.rows) { Array.new(collection.object_type.columns) { Hash.new() }})\n update_matrix(matrix: part_data_matrix) {|part_data| yield part_data }\n collection_associations.instance_variable_set(:@map, {'part_data'=\u003epart_data_matrix})\n collection_associations.save()\n end\n \n def transfer_culture_volume(pr:, culture_vol_ul:, media_vol_ul:)\n total_transfer_volume = culture_vol_ul + media_vol_ul\n from_collection = collection_from(pr.experimental_item)\n to_collection = collection_from(pr.measurement_item)\n part_data_matrix = stamp_transfer(from_collection: from_collection, to_collection: to_collection, process_name: 'dilution')\n \n # Remove volume from Culture_Volume attribute\n update_part_data_matrix(collection: from_collection) do |part_data| culture_volume = part_data.fetch('Culture_Volume', false)\n if culture_volume\n culture_volume[:qty] = culture_volume[:qty] - culture_vol_ul\n part_data['Culture_Volume'] = culture_volume\n end\n part_data\n end\n # Add transfer volume to new collection\n update_part_data_matrix(collection: to_collection) do |part_data| culture_volume = part_data.fetch('Culture_Volume', false)\n if culture_volume\n culture_volume[:qty] = total_transfer_volume\n else\n culture_volume = {qty: total_transfer_volume, units: \"#{MICROLITERS}\"}\n end\n part_data['Culture_Volume'] = culture_volume\n part_data\n end\n end\n\n def get_part_culture_volumes(collection:)\n collection.data_matrix_values('Culture_Volume')\n end\n \n def tech_prefill_and_transfer(pr:, media_sample:, media_vol_ul:, culture_vol_ul:)\n pr.measurement_item = transfer_sample_matrix(source_collection: pr.experimental_item, destination_collection: pr.measurement_item)\n prefill_plate_w_media(collection: pr.measurement_item, media_sample: media_sample, media_vol_ul: media_vol_ul)\n tech_transfer_samples(culture_vol_ul: culture_vol_ul, sources: pr.experimental_item, destinations: pr.measurement_item)\n # update \"Culture_Volume\" for the parts that were transfered based on the \"working_vol\"\n transfer_culture_volume(pr: pr, culture_vol_ul: culture_vol_ul, media_vol_ul: media_vol_ul)\n end\n \n # TODO: Make opts hash allow usner to use to transfer sample matrix by some offset index \n def transfer_sample_matrix(source_collection:, destination_collection:) #, opt: {starting_idx: [0,0]})\n destination_collection = collection_from(destination_collection)\n source_collection = collection_from(source_collection)\n if (source_collection.dimensions == destination_collection.dimensions)\n source_matrix = source_collection.matrix\n destination_collection.matrix = source_matrix\n else\n destination_collection.matrix = source_collection.matrix.flatten.each_slice(destination_collection.object_type.columns).map {|row| row }\n end\n destination_collection.save()\n return destination_collection\n end\n \n def prefill_plate_w_media(collection:, media_sample:, media_vol_ul:, display_hash: {})\n if !display_hash.empty? || media_vol_ul \u003e 0.0 \n collection = collection_from(collection) \n show do\n title \"Pre-fill #{collection.object_type.name} #{collection}\"\n separator\n note \"Follow the table below to pre-fill the collection with \u003cb\u003e#{media_sample.name}\u003c/b\u003e prior to transferring and resuspending cultures:\"\n table highlight_alpha_non_empty(collection) {|r,c| ((display_hash.empty?) ? (\"#{media_vol_ul}#{MICROLITERS}\") : (\"#{display_hash[r][c][:media_vol_ul]}#{MICROLITERS}\")) }\n end\n end\n end\n\n def tech_transfer_samples(culture_vol_ul:, sources:, destinations:) \n alpha_numeric = coordinates_96\n destination_coll = collection_from(destinations)\n source_coll = collection_from(sources)\n source_alpha = source_coll.get_non_empty.map {|r, c| alpha_numeric[r][c] }\n source_alpha_hashmap = nested_hash_data_structure\n destination_coll.get_non_empty.each_with_index do |rc, alpha_idx|\n r, c = rc\n source_alpha_hashmap[r][c] = source_alpha[alpha_idx]\n end\n show do \n title \"Transferring Samples \u003cb\u003eFrom\u003c/b\u003e #{source_coll} \u003cb\u003eTo\u003c/b\u003e #{destination_coll}\"\n separator\n note \"Follow the table below to transfer cultures \u003cb\u003eTo #{destination_coll}\u003c/b\u003e:\"\n bullet \"The alpha numeric coordinates coorespond to wells in #{source_coll}\"\n bullet \"Transfer #{culture_vol_ul}#{MICROLITERS} of culture \u003cb\u003eFrom\u003c/b\u003e each well of #{sources}\"\n table highlight_alpha_non_empty(destination_coll) {|r,c| \"#{source_alpha_hashmap[r][c]}\"}\n end\n end\n\n def add_blanking_samples_to_measurement_item(measurement_item:, blanking_sample:, num_blanks: 3)\n measurement_collection = (measurement_item.instance_of? Collection) ? (measurement_item) : (collection_from(measurement_item))\n count = 0\n blank_wells = []\n while ((measurement_collection.get_empty.length != 0) \u0026\u0026 (count != num_blanks)) do\n r, c, x = measurement_collection.add_one(blanking_sample, reverse: true)\n create_part_association(collection: measurement_collection, key: \"Plate Reader Blanks\", row: r, col: c, val: \"jid_#{jid}\")\n blank_wells.push([r,c])\n count+=1\n end\n measurement_collection.save()\n return measurement_collection, blank_wells \n end\n \n def create_part_association(collection:, key:, row:, col:, val:)\n part = collection_from(collection).set_part_data(key, row, col, val)\n part.save()\n end\n\n def tech_add_blanks(pr:, blanking_sample:, culture_vol_ul:, media_vol_ul:)\n max_well_vol_ul = get_max_well_vol(culture_vol_ul: culture_vol_ul, media_vol_ul: media_vol_ul)\n measurement_collection, blank_wells = add_blanking_samples_to_measurement_item(measurement_item: pr.measurement_item, blanking_sample: blanking_sample)\n blank_wells.each {|row, col| create_part_association(collection: measurement_collection, key: 'Culture_Volume', row: row, col: col, val: {qty: culture_vol_ul+media_vol_ul, units: \"uL\"}) }\n show do\n title \"Adding #{blanking_sample.name} Blanks to #{measurement_collection}\"\n separator\n note \"Follow the table below to add #{max_well_vol_ul}#{MICROLITERS} of #{blanking_sample.name} to the appropriate wells:\"\n table highlight_alpha_rc(measurement_collection, blank_wells) {|r,c| \"#{max_well_vol_ul}#{MICROLITERS}\"}\n end\n end\n \n def data_attributes(data:)\n return data[:mt].to_sym, data[:day].to_sym, data[:hour].to_sym, data[:mitem]\n end\n \n # TODO: Error handling when processing and associating upload data\n # Associate to the valid container and correct the data. \n def process_and_associate_data(pr:, ops:, dilution_factor: 'None', blanking_sample: nil)\n pr.measurement_data.each do |data|\n mt, day, hour, mitem = data_attributes(data: data)\n key = \"#{mt}_#{day}_#{hour}\"\n upload = (debug) ? Upload.find(11379) : data[:upload]\n associate_data(object=mitem, key=key, data=upload, opts = {}) # Associate upload to object #=\u003e Standard Libs/AssociationManagement\n ops.each {|op| associate_data(object=op.plan, key=key, data=upload, opts = {})} # Associates upload to plans\n if pr.measurement_type == 'Calibration'\n # Process data and create standard curve and optical item volume correction factor\n create_prm_calibration_associations(ops: ops, upload: upload, mitem: mitem, mt: mt)\n else\n # Process upload into corrected_data_matrix\n raw_data_matrix = extract_measurement_matrix_from_csv(upload: upload) \n blanked_data_matrix, corrected_data_matrix = correct_plate_reader_data(\n pr: pr,\n blanking_sample: blanking_sample,\n data_matrix: raw_data_matrix,\n dilution_factor: dilution_factor\n )\n # Associate to collection\n create_prm_part_associations_for_collection(\n collection: pr.measurement_item,\n data_matrix: corrected_data_matrix,\n measurement_type: mt,\n day: day,\n hour: hour\n ) \n create_prm_part_associations_for_collection(\n collection: pr.experimental_item,\n data_matrix: corrected_data_matrix,\n measurement_type: mt,\n day: day,\n hour: hour\n ) unless !pr.transfer_required\n end\n # Filters operations that have a culture as input\n create_prm_item_association_for_cultures(ops: ops, mt: mt, day: day, hour: hour, corrected_data_matrix: corrected_data_matrix)\n ops.each {|op| associate_data(object=op, key=key, data={upload_id: upload.id}, opts = {})} # Associates upload_id to operation\n end\n end\n \n def associate_calibration_data(ops, mitem, measurement_type, cal_hash, calc_calibration_data)\n key = measurement_type\n associations = AssociationMap.new(mitem)\n case measurement_type\n when :Calibration_Green_Fluorescence\n trendline_points = {uM_to_val: cal_hash}\n associations.put(key, trendline_points) # ie: 'Calibration_Green_Fluorescence' : {'uM_to_val'=\u003e{50=\u003e2400,25=\u003e1234...}}\n calibration_obj = {standard_curve: calc_calibration_data}\n when :Calibration_Optical_Density\n optical_pts = {vol_to_factor: cal_hash}\n calibration_obj = {vol_to_factor: calc_calibration_data}\n associations.put(key, optical_pts)\n else\n raise \"this is not a recognized measurement_type: #{measumrenent_type}\"\n end\n ops.each {|op| \n associate_data(object=op, key=key, data=calibration_obj, opts = {})\n associate_data(object=op.plan, key=key, data=calibration_obj, opts = {})\n }\n associations.put(key, calibration_obj)\n associations.save\n end\n \n def create_prm_calibration_associations(ops:, upload:, mitem:, mt:)\n data_matrix = extract_measurement_matrix_from_csv(upload: upload) unless debug\n data_matrix = Array.new(8){Array.new(12) { rand(10) } }\n cal_hash = get_calibration_hash(dm: data_matrix, measurement_type: mt)\n calcd_calibration_data = get_calibration_calculated_values(cal_hash: cal_hash, measurement_type: mt)\n associate_calibration_data(ops, mitem, mt, cal_hash, calcd_calibration_data)\n end \n\n def create_prm_item_association_for_cultures(ops:, mt:, day:, hour:, corrected_data_matrix:) \n alpha_coords = coordinates_96\n ops.select {|op| !op.input('Cultures').nil? }.each do |op|\n out_fv, r, c = op.outputs[0], op.outputs[0].row, op.outputs[0].column\n destination_part, m_val = \"#{out_fv.collection.id}/#{alpha_coords[r][c]}\", corrected_data_matrix[r][c]\n input_culture = (debug) ? Item.find(234809) : op.input('Cultures').item\n input_cult_associations = AssociationMap.new(input_culture)\n current_da = input_cult_associations.get(mt.to_s) \n if current_da.nil?\n # Create a new entry under the mt key\n current_da = nested_hash_data_structure\n current_da[day][destination_part][hour] = m_val\n else\n if current_da.has_key? day.to_s\n current_da[day][destination_part] = { hour =\u003e m_val }\n else\n current_da[day] = {destination_part =\u003e { hour =\u003e m_val } }\n end\n end\n input_cult_associations.put(mt.to_s, current_da)\n input_cult_associations.save\n end\n end \n \n def keep_transfer_plate(pr:, user_val:)\n pr.measurement_item.location = (user_val == 'YES') ? 'Bench' : 'deleted'\n end\n \n def cleaning_up(pr:)\n clean_up_inputs([])\n clean_up_outputs\n end\n\n # Cleanup input items\n #\n # @param release_arr [array] is an array of items that are found in Aq\n def clean_up_inputs(release_arr)\n operations.store(opts = { interactive: true, method: 'boxes', errored: false, io: 'input' })\n release release_arr, interactive: true\n # operations.store\n end\n\n # Cleanup output items\n def clean_up_outputs()\n operations.store(interactive: true, method: 'boxes', errored: false, io: 'output')\n show do\n title \"Cleaning Up\"\n separator\n note \"Return any remaining reagents used and clean bench\"\n end\n end\n \n def create_prm_part_associations_for_collection(collection:, data_matrix:, measurement_type:, day:, hour:)\n collection_from(collection).get_non_empty.each do |r, c|\n pa = collection_from(collection).get_part_data(key=measurement_type, row=r, col=c)\n if pa.nil? || pa == -1\n pa = {day =\u003e {hour =\u003e data_matrix[r][c]} }\n else\n (pa.keys.include? day.to_s) ? pa[day].merge!({hour =\u003e data_matrix[r][c]}) : pa[day] = {hour =\u003e data_matrix[r][c]}\n end\n create_part_association(collection: collection, key: measurement_type, row: r, col: c, val: pa)\n end\n end\n\n def get_part_association_data_matrix(collection:, key:)\n matrix = Array.new(collection.object_type.rows) {Array.new(collection.object_type.columns) {-1} }\n part_data_arr = get_collection_part_data(collection: collection, key: key)\n collection.get_non_empty.zip(part_data_arr).each do |rc, data|\n r, c = rc\n matrix[r][c] = data\n end\n return matrix\n end\n \n def average_arr_values(arr)\n arr.reduce(:+) / arr.size.to_f\n end\n \n def get_sample_wells(collection:, sample:)\n collection_from(collection).find(sample)\n end\n \n def apply_value_to_matrix(matrix:, value:, operator:, \u0026block)\n if block_given?\n matrix.map {|row| row.map {|part| (!part.nil? || part != -1) ? yield(part, value) : -1 } }\n else\n matrix.map {|row| row.map {|part| (!part.nil? || part != -1) ? (part.send(operator, value)) : (-1) } } \n end\n end\n \n def correct_plate_reader_data(pr:, blanking_sample:, data_matrix:, dilution_factor:)\n # Get blank wells\n blanking_wells = get_sample_wells(collection: pr.measurement_item, sample: blanking_sample)\n blanking_value = average_arr_values( blanking_wells.map {|r, c| data_matrix[r][c]} )\n blanked_data_matrix = apply_value_to_matrix(matrix: data_matrix, operator: '-', value: blanking_value)\n # Apply dilution factor to the measurement to calc the experimental_item/source measurement value\n if dilution_factor.is_a? Array \n # Create a dilution_factor_matrix that has the same dimensions as the collection default value is 'None' dilution\n r, c = collection_from(pr.measurement_item).object_type.rows, collection_from(pr.measurement_item).object_type.columns\n dilution_matrix = Array.new(r) {Array.new(c) { 'None'} }\n # Take dilution_factor array and slice by the dimensions of the collection, next place all of the df into dilution_matrix\n dilution_factor.each_slice(c).map {|s| s}.each_with_index {|s, ridx| \n s.each_with_index {|dil, cidx| dilution_matrix[ridx][cidx] = dil } }\n # Finally, either apply the dilution factor (df) or take the value from the blanked data matrix\n corrected_data_matrix = dilution_matrix.each_with_index.map {|row, r_idx| row.each_with_index.map {|df, c_idx|\n (df == 'None') ? (blanked_data_matrix[r_idx][c_idx]) : (blanked_data_matrix[r_idx][c_idx] * 1/df)\n }\n }\n else\n corrected_data_matrix = (dilution_factor == 'None') ? (blanked_data_matrix) : (apply_value_to_matrix(matrix: blanked_data_matrix, operator: '*', value: 1/dilution_factor))\n end\n return blanked_data_matrix, corrected_data_matrix\n end\n \n def change_item_location(item:, location:) \n item.location = location\n item.save()\n end\n \n def get_parameter(op:, fv_str:)\n op.input(fv_str).val\n end\n \n def get_dilution_factor(op:, fv_str:)\n param_val = get_parameter(op: op, fv_str: fv_str).to_s\n dilution_factor = (param_val == 'None') ? param_val : param_val.chomp('X').to_f\n end\n \n def get_object_type_data(collection:)\n JSON.parse(Collection.find(collection.id).object_type.data)\n end\n \n def dilution(dilution_factor:, vol:)\n dilution_factor.to_f * vol.to_f\n end\n \n def get_media_bottle(op)\n op.input('Media').item\n end\n \n # A way to filter Struct objects (depriciated)\n # ie: catalog(pr.measurement_data, by: :item) #=\u003e sorting symbol should be known by user. It is a instance method created in a Struct class\n def catalog(collection, by:)\n catalog = Hash.new { |hash, key| hash[key] = [] }\n collection.each_with_object(catalog) do |item, catalog|\n catalog[item.send(by)] \u003c\u003c item\n end\n end\n \n def get_max_well_vol(culture_vol_ul:, media_vol_ul:)\n culture_vol_ul + media_vol_ul\n end\n \n def coordinates_96 \n ('A'..'H').to_a.map {|row| (1..12).to_a.map {|col| row + col.to_s}}\n end\n \n # could extend a module with common class variables needed in protocol writing\n def gather_materials(empty_containers: [], transfer_required: false, new_materials: [], take_items: [])\n new_materials = new_materials.select {|m| !Protocol.materials_list.include? m}\n if !new_materials.empty?\n show do\n title \"Gather The Following Materials\"\n separator\n note \"Gather the following and bring them to your bench:\"\n empty_containers.each {|c|\n ((c.is_a? Item) || (c.is_a? Collection)) ? (check \"#{c.object_type.name} and label as \u003cb\u003e#{c.id}\u003c/b\u003e\") : (check \"#{c}\")\n }\n new_materials.each {|m| check \"#{m}\" }\n end \n end\n take take_items, interactive: true \n Protocol.materials_list.concat(new_materials).concat(empty_containers)\n end\n \n # Allows for multiple key deeply nested hash[:item][:measurement][:day][:hour] = val\n def nested_hash_data_structure\n Hash.new { |hash, key| hash[key] = Hash.new(\u0026hash.default_proc) }\n end\n \n def get_transfer_display_hash(ops:, input_str:, output_str:, dilution_str:)\n display_hash = nested_hash_data_structure\n ops.each do |op|\n out_fv = op.output(output_str)\n dilution_factor = get_dilution_factor(op: op, fv_str: dilution_str)\n media_vol_ul, cult_vol_ul = get_culture_and_media_vols(dilution_factor: dilution_factor, measurement_item: op.output(output_str).collection)\n dh = display_hash[out_fv.row][out_fv.column]\n dh[:dilution_factor] = dilution_factor\n dh[:item_id] = op.input(input_str).item.id\n dh[:media_vol_ul] = media_vol_ul\n dh[:cult_vol_ul] = cult_vol_ul\n create_part_association(collection: op.output(output_str).collection, key: :source, row: out_fv.row, col: out_fv.column, val: op.input(input_str).item.id)\n end\n display_hash\n end\n \n def tech_transfer_cultures(collection:, display_hash:)\n collection = collection_from(collection)\n show do\n title \"Transfer Cultures to #{collection.object_type.name} #{collection}\"\n separator\n note \"Follow the table below to transfer the correct volume and culture to the appropriate well:\"\n table highlight_alpha_non_empty(collection) {|r, c| \"item_#{display_hash[r][c][:item_id]}\\n#{display_hash[r][c][:cult_vol_ul]}#{MICROLITERS}\" }\n end\n end\n \n def get_uninitialized_output_fv_object_type_id(op)\n AllowableFieldType.find(op.outputs[0].allowable_field_type_id).object_type_id\n end\n \n def get_uninitialized_output_object_type(op)\n oti = get_uninitialized_output_fv_object_type_id(op)\n ObjectType.find(oti)\n end\n \n # TODO: Make compataible with both statistics and matrix uploads\n # Takes in a csv upload file, extracts the information on it\n # into a datamatrix object which is returned.\n # Specificly tuned to the output file of the biotek plate reader.\n #\n # @param upload [Upload] the object which can be resolved to calibration csv\n # @return [WellMatrix] a WellMatrix holding the measurement for each well\n def extract_measurement_matrix_from_csv(upload:)\n table = parse_upload_csv(upload: upload)\n # dm = (table.length \u003e 25) ? Array.new(8) { Array.new(12) {-1} } : Array.new(4) { Array.new(6) {-1} }\n dm = Array.new(8) { Array.new(12) {-1} } \n # If the file that is saved, exported, and uploaded was formatted as a 'Statistics' table\n table.each_with_index do |row, idx|\n next if idx.zero?\n well_coord = row[2]\n next if well_coord.nil?\n measurement = row[3].to_f\n next if measurement.nil?\n r, c = find_rc_from_alpha_coord(well_coord).first\n (dm[r][c]) ? dm[r][c] = measurement : -1\n end\n dm\n end\n \n def parse_upload_csv(upload:)\n require 'csv'\n require 'open-uri'\n table = []\n CSV.new(open(upload.expiring_url)).each { |line| table.push(line) }\n return table\n end\n\n # Finds where an alpha_coordinate is in a 96 Well plate\n #\n # @param alpha_coord [array or string] can be a single alpha_coordinate or a list of alpha_coordinate strings ie: 'A1' or ['A1','H7']\n # @return rc_list [Array] a list of [r,c] coordinates that describe where the alpha_coord(s) are in a 96 well matrix\n def find_rc_from_alpha_coord(alpha_coord)\n # look for where alpha coord is 2-D array coord\n coordinates_96 = ('A'..'H').to_a.map {|row| (1..12).to_a.map {|col| row + col.to_s}} \n rc_list = []\n if alpha_coord.instance_of? Array\n # alpha_coord = alpha_coord.map {|a| a.upcase}\n alpha_coord.each {|a_coord|\n (!a_coord.nil?) ? coordinates_96.map.each_with_index { |row, r_idx| row.each_index.select {|col| row[col] == a_coord.upcase}.each { |c_idx| rc_list.push([r_idx, c_idx]) } } : next\n }\n else\n coordinates_96.map.each_with_index { |row, r_idx| row.each_index.select {|col| row[col] == alpha_coord.upcase}.each { |c_idx| rc_list.push([r_idx, c_idx]) } }\n end\n return rc_list\n end\n \n def add_solution_to_op(op:, fv_str:, item:)\n if item.instance_of? Collection\n ot = item.object_type\n elsif item.instance_of? Item\n ot = ObjectType.find(item.object_type_id)\n end\n t = op.add_input(fv_str, item.sample, ot)\n op.input(fv_str).set item: item\n end\n \nend # module PlateReaderHelper"}},{"library":{"name":"PlateReaderSoftware","category":"High Throughput Culturing","code_source":"# Eriberto Lopez\n# [email protected]\n# 03/14/19\n\nrequire 'date'\n\nmodule AqUpload\n # Provides a upload button in a showblock in order to upload a single file\n #\n # @param upload_filename [string] can be the name of the file that you want tech to upload\n # @return up_show [hash] is the upload hash created in the upload show block\n # @return up_sym [symbol] is the symbol created in upload show block that will be used to access upload\n def upload_show(saving_dir:, upload_filename:)\n upload_var = \"file\"\n up_show = show do\n title \"Upload Your Measurements\"\n separator\n note \"Select and Upload: #{saving_dir}/\u003cb\u003e#{upload_filename}\u003c/b\u003e\"\n upload var: upload_var.to_sym\n end\n return up_show, upload_var.to_sym\n end\n \n # Retrieves the upload object from upload show block\n #\n # @param up_show [hash] is the hash that is created in the upload show block\n # @param up_sym [symbol] is the symbol created in the upload show block and used to access file uploaded\n # @return upload [upload_object] is the file that was uploaded in the upload show block\n def get_upload_from_show(up_show:, up_sym:)\n (!up_show[up_sym].nil?) ? (upload = up_show[up_sym].map {|up_hash| Upload.find(up_hash[:id])}.shift) : nil #(show {warning \"no upload was found\".upcase})\n end\nend\n\nmodule KlavinsLabPlateReaderSoftware\n include AqUpload\n \n def setup_plate_reader_software_env(pr:, new_mtype:)\n open_software(pr: pr)\n new_mtype = select_measurement_type_template(pr: pr) if new_mtype\n preheat(pr: pr)\n return new_mtype\n end\n\n def take_measurement_and_upload_data(pr:)\n timepoint = read_plate(pr: pr)\n pr.measurement_data = export_save_and_upload_measurement_data(pr: pr, timepoint: timepoint)\n end\n\n def export_save_and_upload_measurement_data(pr:, timepoint:)\n sw = pr.software\n day = timepoint.strftime \"%m%d%Y\"\n hour = timepoint.strftime \"%H%M\"\n measurement_data = [] # Collect uploads for a measurement_item's measurement_type(s) ie: 'Optical Density \u0026 Green Fluorescence'\n export_measurement_types(pr: pr).each do |mt|\n measurement_filename = get_measurement_filename(measurement_item: pr.measurement_item, measurement_type: mt, timepoint: timepoint)\n show do\n title \"Export \u0026 Save #{mt} Measurements Plate Reader\"\n separator\n warning \"Make sure that no other Excel sheets are open before exporting!\".upcase\n end\n show do\n title \"Export \u0026 Save #{mt} Measurements Plate Reader\"\n separator\n # EXPORT\n image sw[:images][:export_new_data]\n bullet \"Select the \u003cb\u003e'Statistics'\u003c/b\u003e tab\"\n bullet \"Select Data: \u003cb\u003e#{sw[:export_mesurement_type][mt.to_sym][:dtype]}\u003c/b\u003e\"\n note \"Next, click the Excel sheet export button. \u003cb\u003eThe sheet will appear on the menu bar below\u003c/b\u003e.\"\n image sw[:images][:export_data_button]\n # SAVE\n warning \"Make sure to save file as a '.csv' file!!\"\n note \"Go to sheet and \u003cb\u003e'Save as'\u003c/b\u003e ==\u003e \u003cb\u003e#{measurement_filename}\u003c/b\u003e under the \u003cb\u003e#{sw[:saving_directory]}\u003c/b\u003e folder.\"\n image sw[:images][:save_export]\n end\n # UPLOAD\n attempt = 0\n up_show, up_sym = {}, nil #upload_show(saving_dir: sw[:saving_directory], upload_filename: measurement_filename)\n while ((up_show[up_sym].nil?) || (attempt == 3)) \u0026\u0026 (!debug) do\n up_show, up_sym = upload_show(saving_dir: sw[:saving_directory], upload_filename: measurement_filename)\n attempt+=1\n end\n upload = get_upload_from_show(up_show: up_show, up_sym: up_sym)\n upload_data = {\n mitem: pr.measurement_item,\n mt: mt.to_s.gsub(' ','_'),\n day: day,\n hour: hour,\n upload: upload\n }\n measurement_data.push(upload_data)\n end\n return measurement_data\n end\n\n def go_to_computer\n show {warning \"\u003cb\u003eThe next steps should be done on the plate reader computer\u003c/b\u003e.\".upcase}\n end\n \n def open_software(pr:)\n if (!pr.software_open)\n go_to_computer\n show do\n title \"Open #{pr.type} Plate Reader Software\"\n separator\n note \"Click on the icon shown below to open the plate reader software:\"\n image pr.software[:images][:open_software]\n end\n else\n log_info 'the software is already open!'.upcase\n end\n pr.software_open = true\n end\n \n def select_measurement_type_template(pr:)\n mt_template = pr.software[:measurement_type_templates][pr.measurement_type.to_sym]\n show do \n title \"Select #{pr.type} #{pr.measurement_type.to_s} Template\"\n separator\n note \"Under \u003cb\u003e'Create a New Item'\u003c/b\u003e click \u003cb\u003e'Experiment'\u003c/b\u003e\"\n note \"From the pop-up list select: \u003cb\u003e#{mt_template}\u003c/b\u003e\"\n end\n new_mtype = false\n return new_mtype # the measurement_type is no longer new and can be reaused if necessary\n end\n \n def preheat(pr:)\n show do \n title \"Warming Up...\"\n separator\n note \"Let the #{pr.type} Plate Reader warm up if necessary.\"\n note \"In the following preparative steps, minimize the amount of time that the \n \u003cb\u003e#{ObjectType.find(pr.experimental_item.object_type_id).name} #{pr.experimental_item}\u003c/b\u003e\n is not under experimental conditions prior to the measurement.\"\n end \n end\n \n def get_experiment_filename(pr:, timepoint:)\n \"jid_#{jid}_experiment_#{pr.measurement_type}_#{timepoint.strftime \"%m%d%Y\"}\".gsub(' ', '')\n end\n\n def read_plate(pr:)\n go_to_computer\n timepoint = Time.now\n show do \n title \"Take #{pr.measurement_type} Measurement\"\n separator\n note \"Click Read Plate icon shown below\"\n image pr.software[:images][:read_plate]\n note \"Next, click the \u003cb\u003e'READ'\u003c/b\u003e on the pop-up window.\"\n bullet \"Name experiment file: \u003cb\u003e#{get_experiment_filename(pr: pr, timepoint: timepoint)}\u003c/b\u003e\"\n bullet \"\u003cb\u003eSave\u003c/b\u003e it under the \u003cb\u003e#{pr.software[:saving_directory]}\u003c/b\u003e folder.\"\n note \"Finally, load plate #{pr.measurement_item} then, click \u003cb\u003e'OK'\u003c/b\u003e to take measurement\"\n end\n pr.measurement_item.location = pr.type.to_s\n return timepoint\n end\n \n def get_measurement_filename(measurement_item:, measurement_type:, timepoint:)\n mt = measurement_type.to_s.gsub(' ', '') \n \"jid_#{jid}_#{mt}_item_#{measurement_item.id}_t#{timepoint.strftime \"%H%M\"}_#{timepoint.strftime \"%m%d%Y\"}\".gsub(' ', '_')\n end\n \n def export_measurement_types(pr:)\n mt = pr.measurement_type.to_s\n case mt\n when 'Optical Density', 'Time Series', 'Green Fluorescence'\n [mt]\n when 'Optical Density \u0026 Green Fluorescence'\n mt.split(' \u0026 ')\n when 'Calibration'\n [\"#{mt} Optical Density\", \"#{mt} Green Fluorescence\"]\n else\n raise \"#{pr.measurement_type} is not supported by this plate reader or does not have the measurement_type added to the #{pr.type} software properties\"\n end\n end\nend # module Gen5BioTekSoftware\n"}},{"library":{"name":"AssociationManagement","category":"Standard Libs","code_source":"# frozen_string_literal: true\n\n# Module with methods and classes that seek to help with associating data\n# to, and retrieving data from, items, operations, plans, collections, and parts.\n#\nmodule AssociationManagement\n require 'matrix'\n\n # Associates a key and value to the associations hash of the given object.\n # Replaces an existing association for the given key.\n #\n # A part may be represented as a part item, or a collection and coordinate.\n #\n # @param object [DataAssociator] the object to associate data\n # @param key [String] the key for the association\n # @param data [serializable object] the data for the association\n # @param opts [Hash] additional method options\n # @option coord [Array] row, column pair if the object is a collection\n # @option data_matrix [String] optional data matrix for a collection\n def associate_data(object, key, data, opts = {})\n AssociationMap.associate_data(object, key, data, opts)\n end\n\n # Returns the associated value from the associations hash of a given object.\n # If an association doesn't exist for the key, returns nil.\n #\n # @param object [DataAssociator] the object to associated data\n # @param key [String] the key for the association\n # @param opts [Hash] additional method options\n # @option coord [tuple Array] row, column of part if object is a collection.\n # @option data_matrix [String] optional data matrix\n # @return [serializable object] the value associated with the given key\n def get_associated_data(object, key, opts = {})\n AssociationMap.get_associated_data(object, key, opts)\n end\n\n # Defines a map to manage the associations for an {Item}, {Operation}, or\n # {Plan} object, which are Aquarium classes that extend {DataAssociator}.\n #\n # Note: if `map` contains associations, it is necessary to call `map.save` for\n # the associations to be saved to Aquarium.\n #\n class AssociationMap\n DATAMATRIX_KEY = 'part_data'\n\n # Initializes an {AssociationMap} for the given item, operation, or plan.\n #\n # @param object [DataAssociator] the object to which to associated data\n def initialize(object)\n @object = object\n @map = {}\n\n\n\n @object.associations.each do |datum|\n @map[datum[0]] =\n if @object.upload(datum[0]).nil?\n datum[1]\n else\n UploadAssoc.new(datum[1], @object.upload(datum[0]))\n raise \"here\"\n end\n end\n\n \n\n if object.is_a? Collection\n initialize_part_data\n data_matrix_all(@object, @map[DATAMATRIX_KEY])\n end\n end\n\n # Retrieves part_data from the data associations of constituent parts.\n # achieves forward compatibility with AQ Part update\n def data_matrix_all(coll, data_matrix)\n pas = coll.part_associations\n part_ids = pas.collect(\u0026:part_id)\n das = DataAssociation.where(parent_class: 'Item', parent_id: part_ids)\n pas.each do |pa|\n data_matrix[pa.row][pa.column] = {}\n das.select { |da| da.parent_id == pa.part_id }.each do |da|\n data_matrix[pa.row][pa.column][da.key] = da.value\n end\n end\n data_matrix\n end\n\n # All in one static method which associates a key and value\n # to the associations hash of a given object. If an association already\n # exists at the given key, it will be replaced. Can associate to parts of collection either\n # using a part field value, or an optional coordinate specification with a collection\n #\n # @param object [DataAssociator] the object to which data is to be associated. Can be an io field value\n # @param key [String] the key for the association\n # @param data [serializable object] the data for the association\n # @param opts [Hash] additional method options\n # @option coord [tuple Array] specify r, c index of the data matrix of the object to upload to,\n # rather than directly to the object. Requires that object is a collection.\n # @option data_matrix [String] optionally, when associating to a part of a collection, use a\n # data matrix besides the default one\n def self.associate_data(object, key, data, opts = {})\n defaults = { data_matrix: DATAMATRIX_KEY }\n opts.merge defaults\n raise 'Bad Arguments: cannot associate to a part and specify coords at the same time' if object.is_a?(FieldValue) \u0026\u0026 opts[:coord]\n if object.is_a?(FieldValue)\n assoc_map = AssociationMap.new(object.collection)\n assoc_map.putrc(object.row, object.column, key, data)\n elsif opts[:coord]\n assoc_map = AssociationMap.new(object)\n assoc_map.putrc(opts[:coord][0], opts[:coord][1], key, data)\n else # Normal case that deals directly with object\n assoc_map = AssociationMap.new(object)\n assoc_map.put(key, data)\n end\n assoc_map.save\n end\n\n # All in one static method which gets an associated value\n # from the associations hash of a given object. If an association doesn't\n # exist at the given key, returns nil. Can get associations from parts of collection either\n # using a part field value, or an optional coordinate specification with a collection\n #\n # @param object [DataAssociator] the object to which data is to be associated, can be an io field value\n # @param key [String] the key for the association\n # @param opts [Hash] additional method options\n # @option coord [tuple Array] specify r, c index of the data matrix of the object to upload to,\n # rather than directly to the object. Requires that object is a collection.\n # @option data_matrix [String] optionally, when retrieving association from a part of a collection,\n # use a matrix besides the default one\n # @return [serializable object] the data stored in the associations of the given object at the given key\n def self.get_associated_data(object, key, opts = {})\n defaults = { data_matrix: DATAMATRIX_KEY }\n opts.merge defaults\n raise 'Bad Arguments: cannot get data from a part and specify coords at the same time' if object.is_a?(FieldValue) \u0026\u0026 opts[:coord]\n if object.is_a?(FieldValue)\n assoc_map = AssociationMap.new(object.collection)\n return assoc_map.getrc(object.row, object.column, key)\n elsif opts[:coord]\n assoc_map = AssociationMap.new(object)\n return assoc_map.getrc(opts[:coord][0], opts[:coord][1], key)\n else # Normal case that deals directly with object\n assoc_map = AssociationMap.new(object)\n return assoc_map.get(key)\n end\n end\n\n # Adds an association for the data with the key.\n # The data must be serializable.\n #\n # @param key [String] the key for the association\n # @param data [serializable object] the data for the association\n # @param opts [Hash] Additional Options\n # @option tag [String] If putting an Upload, optionally specify an extra label\n def put(key, data, opts = { tag: {} })\n @map[key] = if data.is_a?(Upload)\n UploadAssoc.new(opts[:tag], data)\n else\n data\n end\n end\n\n # Adds an association for the data with the key, for\n # a specific row, column coordinate within a collection\n # If the data_matrix for the collection has not been created yet, it is initialized\n #\n # @requires current object is a Collection, and r,c corresponds to a valid location in the object\n # @param r [Integer] the row of the part within the collection to associate to\n # @param c [Integer] the column of the part within the collection to associate to\n # @param key [String] the key for the association\n # @param data [serializable object] the data for the association\n # @param data_matrix [String/Symbol] optionally specify a data matrix to access besides the default one,\n # for example, you might have the default part data, alongside a routing matrix\n def putrc(row, column, key, data, data_matrix = DATAMATRIX_KEY)\n # if the data_matrix for this collection does not exist yet, initialize it.\n initialize_part_data(data_matrix)\n @map[data_matrix][row][column][key] = data\n end\n\n # To be called when the object of association is a collection,\n # establishes a matrix parallel to the sample matrix which can\n # be used to store additional information about individual parts\n # Each slot in the matrix will be a new empty hash.\n #\n # @param coll [Collection] the object for which part-data matrix will be initialized\n # @param data_matrix [String/Symbol] optionally specify a data matrix to access besides the default one,\n # for example, you might have the default part data, alongside a routing matrix\n def initialize_part_data(data_matrix = DATAMATRIX_KEY)\n raise \"Invalid Method Call: cannot associate part data to an object that isn't a collection\" unless @object.is_a?(Collection)\n # TODO: fix the following so that can use the Base method\n # coll = collection_from(@object.id)\n coll = Collection.find(@object.id)\n @map[data_matrix] = Array.new(coll.dimensions[0]) { Array.new(coll.dimensions[1]) { {} } } if @map[data_matrix].nil?\n end\n\n # Returns the associated data for the key, if any.\n #\n # @param key [String] the key for the association\n # @returns the data object for the key, `nil` otherwise\n def get(key)\n data = @map[key]\n if data.is_a?(UploadAssoc)\n data.upload\n else\n data\n end\n end\n\n # Gets an association for the data with the key, for\n # a specific row, column coordinate within a collection\n # Returns the associated data for the key, if any.\n #\n # @requires current object is a Collection, and r,c corresponds to a valid location in the object\n # @param r [Integer] the row of the part within the collection to associate to\n # @param c [Integer] the column of the part within the collection to associate to\n # @param key [String] the key for the association\n # @param data_matrix [String/Symbol] optionally specify a data matrix to access besides the default one,\n # for example, you might have the default part data, alongside a routing matrix\n # @returns the data object for the key, `nil` otherwise\n def getrc(row, column, key, data_matrix = DATAMATRIX_KEY)\n @map[data_matrix][row][column][key] unless @map[data_matrix].nil?\n end\n\n # Retrieve the associations for all parts of the collection\n # as a matrix.\n # @requires current object is a collection\n # @param data_matrix [String/Symbol] optionally specify a data matrix to access besides the default one,\n # for example, you might have the default part data, alongside a routing matrix\n # @returns the data matrix, if one exists\n def get_data_matrix(data_matrix = DATAMATRIX_KEY)\n Matrix.rows(@map[data_matrix])\n end\n\n # Replace or initialize the data matrix for this object\n # with a custom one.\n # @requires the current object is a collection\n # `matrix` have the same row column dimensions as the collection\n #\n # @param new_matrix [Matrix] the new data matrix\n # @param data_matrix [String/Symbol] optionally specify a data matrix (by key) to access besides the default one,\n # for example, you might have the default part data, alongside a routing matrix\n\n def set_data_matrix(matrix, data_matrix = DATAMATRIX_KEY)\n @map[data_matrix] = matrix.to_a\n end\n\n # Saves the associations in this map to Aquarium.\n def save\n das = []\n @map.each_key do |key|\n if key == DATAMATRIX_KEY\n das.concat save_data_matrix_alt(@object, @map[key])\n elsif @map[key].is_a? UploadAssoc\n # TODO: update this to lazy associate once aq is updated to hav lazy upload assoc (on master, just not on server yet)\n @object.associate(key, @map[key].tag, @map[key].upload)\n else\n das \u003c\u003c @object.lazy_associate(key, @map[key])\n end\n end\n DataAssociation.import(das, on_duplicate_key_update: [:object]) unless das.empty?\n @object.save\n nil\n end\n\n # saves part_data to the data associations of constituent parts.\n # achieves forward compatibility with AQ Part update\n # built off of set_data_matrix from collection.rb\n def save_data_matrix_alt(coll, matrix, offset: [0, 0])\n pm = coll.part_matrix\n das = []\n\n uniq_keys = matrix.flatten.map(\u0026:keys).flatten.uniq\n dms_by_key = {}\n uniq_keys.each do |key|\n dms_by_key[key] = coll.data_matrix(key)\n end\n\n coll.each_row_col(matrix, offset: offset) do |x, y, ox, oy|\n next unless !matrix[x][y].nil? \u0026\u0026 pm[ox][oy] # this part has das\n matrix[x][y].each do |k, v|\n if pm[ox][oy]\n if dms_by_key[k][ox][oy]\n da = dms_by_key[k][ox][oy]\n da.object = { k =\u003e v }.to_json\n das \u003c\u003c da\n else\n das \u003c\u003c pm[ox][oy].lazy_associate(k, v)\n end\n end\n end\n end\n\n das\n end\n\n # Returns an array of all the keys in this map\n def keys\n @map.keys\n end\n\n # Returns the string representation of the map\n def to_string\n @map.to_s\n end\n\n alias to_s to_string\n end\n\n # private class that is used to deal with associating upload objects alongside their tag\n class UploadAssoc\n def initialize(tag, upload)\n @upload = upload\n @tag = tag || {}\n end\n\n def change_tag(new_tag)\n @tag = new_tag\n end\n\n attr_reader :upload\n\n attr_reader :tag\n end\n\n # Utilizes the part-data matrix of collections to store information about the history of\n # parts of a collection. PartProvenance initializes and relies on two fields of every part-data\n # slot: `source` and `destination`.\n # `source` will store a list of item ids (with rc index if applicable),\n # of all the ingredients used to make this part, and destination will use the same data format\n # to record all of the places this part was used in.\n # Item-Item provenance can technically be recorded as well with this library, but it will not\n # be necessary.\n #\n module PartProvenance\n SOURCE = 'source'\n DESTINATION = 'destination'\n\n # Record an entry to the provenance data between two parts, or a part and an item.\n # This will populate the destination field of `from`, and the source field\n # of the `to` in their respective associations. If from_coord or to_coord is specified, then\n # the associations of the part of the from/to collection at that coordinate will\n # populated instead.\n #\n # @param opts [Hash] Arguments specifying which objects to record relation for\n # @option from [Item/Collection] the item or collection where sample transfer originated\n # @option to [Item/Collection] the item or collection for destination of sample transfer\n # @option from_coord [Tuple Array] optionally, specify the coordinate selecting a part of the collection, if `to` was a collection\n # @option to_coord [Tuple Array] optionally, specify the coordinate selecting a part of the collection, if `from` was a collection\n # @option additional_relation_data [Hash] optionally, add additional key/value pairs to add to both object's routing data\n # for this relation. For example, you might want to specify the volume of the transfer,\n # or which colony was picked from a plate\n # @option from_map [AssociationMap] existing AssociationMap for the given from-object, required to successfully associate provenance to\n # the `from` item\n # @option to_map [AssociationMap] existing AssociationMap for the given to-object, required to successfully associate provenance to\n # the `to` item\n def add_provenance(opts = {})\n if opts[:from] == opts[:to] # special case: provenance between two parts on the same collection\n opts[:from_map] = opts[:to_map] # ensure from map and to map are the same object for this case\n end\n\n # creating information hashes to represent `from` and `to` relationship data\n from_info = serialize_as_simple_tag(opts[:from], opts[:from_coord], opts[:additional_relation_data])\n to_info = serialize_as_simple_tag(opts[:to], opts[:to_coord], opts[:additional_relation_data])\n\n # in destination field of `from`, add information tag representing `to`\n append_to_association(opts[:from_map], DESTINATION, to_info, coord: opts[:from_coord]) if opts[:from_map]\n\n # in source field of `to`, add information tag representing `from`\n append_to_association(opts[:to_map], SOURCE, from_info, coord: opts[:to_coord]) if opts[:to_map]\n end\n\n # Retrieves a list of sources that were used to construct the given part\n # of a Collection\n #\n # @param object [FieldValue/Collection] the part of interest, or the collection which\n # contains the part of interest. For the second case, coord must also be specified\n # @param coord [Tuple Array] the r,c index of the target part\n def sources(object, coord = nil)\n if coord\n AssociationMap.get_associated_data(object, SOURCE, coord: coord)\n else\n AssociationMap.get_associated_data(object, SOURCE)\n end\n end\n\n # Retrieves a list of destinations that were made using the given part\n # of a Collection\n #\n # @param object [FieldValue/Collection] the part of interest, or the collection which\n # contains the part of interest. For the second case, coord must also be specified\n # @param coord [Tuple Array] the r,c index of the target part\n def destinations(object, coord = nil)\n if coord\n AssociationMap.get_associated_data(object, DESTINATION, coord: opts[:coord])\n else\n AssociationMap.get_associated_data(object, DESTINATION)\n end\n end\n\n # For the given associatable target object, appends or concatenates the given datum_to_append to the association\n # at `key` for that object\n #\n # @param association_map [AssocioationMap] an AssociationMap that will have its associations appended to.\n # @param key [String/Symbol] The association key which maps to an appendable object\n # @param datum_to_append [Serializable Object] the element to append to the list at the value for the given key\n # @param opts [Hash] additional options\n # @option coord [Tuple array] coordinate of target part, if association target is a collection\n def append_to_association(association_map, key, datum_to_append, opts = {})\n if opts[:coord] # we will be interacting with the associations of a part of a collection if coord is specified\n association_map.putrc(opts[:coord][0], opts[:coord][1], key, []) if association_map.getrc(opts[:coord][0], opts[:coord][1], key).nil?\n association_map.getrc(opts[:coord][0], opts[:coord][1], key) \u003c\u003c datum_to_append\n else\n association_map.put(key, []) if association_map.get(key).nil?\n association_map.get(key) \u003c\u003c datum_to_append\n end\n end\n\n # Given an item, or a part of a collection, serializes it into a simple tag which can be used to retrieve it.\n #\n # @param item [Item/FieldValue] can be either an Item, or\n # an i/o object corresponding to a part of a collection, which can be thought of\n # as constituting a 'sub item'\n def serialize_as_simple_tag(item, coord, additional_info)\n info = if item.collection? \u0026\u0026 coord\n { id: item.id, row: coord[0], column: coord[1] }\n elsif (item.is_a? Item) || (item.is_a? Collection)\n { id: item.id }\n else\n raise 'Argument is neither a part nor an item'\n end\n info.merge!(additional_info) unless additional_info.nil?\n info\n end\n end\nend\n"}},{"library":{"name":"Debug","category":"Standard Libs","code_source":"module Debug\n def print_object obj\n if [Numeric, String].any? { |c| obj.is_a? c }\n obj\n elsif [Array].any? { |c| obj.is_a? c }\n obj.map { |item| print_object item }\n elsif [Hash].any? { |c| obj.is_a? c }\n Hash[obj.map { |k, v| [k, print_object(v)] }]\n else\n s = obj ? obj.id.to_s : \"\"\n s += \" #{obj.name}\" if obj.class.method_defined? :name\n s\n end\n end\n\n def log_info *args\n if debug\n show do\n title \"Debug slide (#{args.length} #{\"arg\".pluralize args.length})\"\n\n args.each do |arg|\n note \"#{arg.class}: #{print_object arg}\"\n end\n end\n end\n end\n\n def inspect(object, ident=nil)\n show do\n title \"\u003cspan style=\\\"background-color:yellow\\\"\u003eINSPECTING #{ident} (#{object.class})\u003c/span\u003e\"\n if object.kind_of?(Array)\n table object\n else\n note object.to_json\n end\n end\n end\nend\n"}},{"library":{"name":"Units","category":"Standard Libs","code_source":"module Units\n \n EMPTY = -1\n \n # Volume\n MICROLITERS = 'µl'\n MILLILITERS = 'ml'\n \n # Weight\n NANOGRAMS = 'ng'\n MICROGRAMS = 'µg'\n \n # Concentration\n PICOMOLAR = 'pM'\n NANOMOLAR = 'nM'\n MICROMOLAR = 'µM'\n MILLIMOLAR = 'mM'\n MOLAR = 'M'\n \n # Temperature\n DEGREES_C = '°C'\n \n # Time\n MINUTES = 'min'\n SECONDS = 'sec'\n HOURS ='hr'\n # Force\n TIMES_G = 'x g'\n \n # R/DNA Length\n BASEPAIRS = 'bp'\n KILOBASEPAIRS = 'kbp'\n MEGABASEPAIRS = 'mbp'\n GIGABASEPAIRS = 'gbp'\n \n # Voltage\n VOLTS = 'V'\n\n def self.qty_display(qty)\n \"#{qty[:qty]} #{qty[:units]}\"\n end\n \n def qty_display(qty)\n \"#{qty[:qty]} #{qty[:units]}\"\n end\n \n def add_qty_display(options)\n new_items = {}\n \n options.each do |key, value|\n key =~ /^(.+_)+([a-z]+)$/\n \n case $2\n when 'microliters'\n units = MICROLITERS\n when 'milliliters'\n units = MILLILITERS\n when 'minutes'\n units = MINUTES\n else\n next\n end\n \n qty = value.to_f\n \n new_items[\"#{$1}qty\".to_sym] = { qty: qty, units: units }\n end\n \n options.update(new_items)\n end\n \n # Return the unit constant for the the unit name if there is one.\n #\n # @param unit_name [String] the name of the unit\n # @returns the value of the constant with the given name\n # @raises BadUnitNameError if the name is not the name of a defined unit\n def self.get_unit(unit_name:)\n self.const_get(unit_name.upcase)\n rescue\n raise BadUnitNameError.new(name: unit_name)\n end\n \n # Exception class for bad unit name arguments to Units::get_unit.\n #\n # @attr_reader [String] name the bad unit name\n class BadUnitNameError \u003c StandardError\n attr_reader :name\n \n def initialize(msg: \"Unknown unit name\", name:)\n @name = name\n super(msg)\n end\n end\n \n # Return a key for the measure hash defined on the given object type.\n #\n # The measure hash must be defined in the data proerty of the object type as JSON.\n # For instance\n #\n # { \"measure\": { \"type\": \"concentration\", \"unit\": \"micromolar\" } }\n #\n # The key is constructed as the type name, an underscore, and the unit name.\n #\n # @param object_type [ObjectType] the object type\n # @returns the key for the measure of the the object type if there is one\n # @raises MissingObjectTypeMeasure if the object type has no measure data_object\n def self.get_measure_key(object_type:)\n data_object = object_type.data_object\n raise MissingObjectTypeMeasureError.new(name: object_type.name) if !data_object.key?(:measure)\n \n measure = object_type.data_object[:measure]\n type_name = measure[:type]\n unit_name = measure[:unit]\n \"#{type_name}_#{self.get_unit(unit_name: unit_name)}\"\n end\n \n # Exception class for an object type with out a measure hash definition.\n #\n # @attr_reader [String] name the name of the object type where measure has was expected\n class MissingObjectTypeMeasureError \u003c StandardError\n attr_reader :name\n \n def initialize(msg: \"ObjectType has no measure in data object\", name:)\n @name = name\n super(msg)\n end\n end\n \nend"}},{"library":{"name":"CollectionDisplay","category":"Tissue Culture Libs","code_source":"module CollectionDisplay\n def create_collection_table collection\n size = collection.object_type.rows * collection.object_type.columns\n slots = (1..size).to_a\n slots.each_slice(collection.object_type.columns).map do |row|\n row.map do |col|\n {content: col, class: 'td-empty-slot'}\n end\n end\n end\n\n def highlight tbl, row, col, id\n tbl[row][col] = {content: id, class: 'td-filled-slot', check: true}\n end\n\n # [r,c,x] list\n def highlight_rcx collection, rcx_list\n tbl = create_collection_table collection\n rcx_list.each do |r, c, x|\n highlight tbl, r, c, x\n end\n tbl\n end\n\n def highlight_rc collection, rc_list, \u0026rc_block\n rcx_list = rc_list.map { |r, c|\n block_given? ? [r, c, yield(r, c)] : [r, c, \"\"]\n }\n highlight_rcx collection, rcx_list\n end\n\n def highlight_non_empty collection, \u0026rc_block\n highlight_rc collection, collection.get_non_empty, \u0026rc_block\n end\n\n def highlight_collection ops, id_block=nil, \u0026fv_block\n g = ops.group_by { |op| fv_block.call(op).collection }\n tables = g.map do |collection, grouped_ops|\n rcx_list = grouped_ops.map do |op|\n fv = fv_block.call(op)\n id = id_block.call(op) if id_block\n id ||= fv.sample.id\n [fv.row, fv.column, id]\n end\n tbl = highlight_rcx collection, rcx_list\n [collection, tbl]\n end\n tables\n end\n\n def r_c_to_slot collection, r, c\n rows, cols = collection.dimensions = collection.object_type.rows\n r*cols + c+1\n end\n \n \n \n \n def create_alpha_numeric_table(collection)\n size = collection.object_type.rows * collection.object_type.columns\n slots = (1..size).to_a\n alpha_r = ('A'..'H').to_a\n slots.each_slice(collection.object_type.columns).each_with_index.map do |row, r_idx|\n row.each_with_index.map do |col, c_idx|\n {content: \"#{alpha_r[r_idx]}#{c_idx + 1}\", class: 'td-empty-slot'}\n end\n end\n end\n \n def highlight_alpha_rc collection, rc_list, \u0026rc_block\n rcx_list = rc_list.map { |r, c|\n block_given? ? [r, c, yield(r, c)] : [r, c, \"\"]\n }\n highlight_alpha_rcx(collection, rcx_list)\n end\n \n def highlight_alpha_rcx(collection, rcx_list)\n tbl = create_alpha_numeric_table(collection)\n rcx_list.each do |r, c, x|\n highlight tbl, r, c, x\n end\n return tbl\n end\n\n def highlight_alpha_non_empty collection, \u0026rc_block\n highlight_alpha_rc collection, collection.get_non_empty, \u0026rc_block\n end\n \nend"}}]}