opentrons 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a694979ecde2e5c60ccc42fb31cd781b696c21b6
4
- data.tar.gz: dde63e64ad2ea5c05d45aaf045d687edd1254486
3
+ metadata.gz: 95cdf5d0184ba33c091182c256d97d90cdd15d59
4
+ data.tar.gz: b0b638480c593846786765098f61942f05b9fbeb
5
5
  SHA512:
6
- metadata.gz: 1e552df10640109eab25d0757ed386d47a8048e7d897fd5f9f0fe91a1ebfe5a7c66f8aa324c6b894012b5eaed970642a6c5f2472950ef71743b173867b90df6f
7
- data.tar.gz: aa45050996fdd491ab40d78890e551eea07eae02e1805b6ee6a0c72d930e463840aa15b7a27feffdd40f0838de883caf78bbcf5610d0c3c29ee0a409fa1156d0
6
+ metadata.gz: 6f1cd9e362d4288610f59f7cc19fed1d49b36c0c646876f2386e66332d3f459e3be336eff467014d38fb56ecf0a2f0f7bf2707216a6b43f20c87a18f629dba7a
7
+ data.tar.gz: 39b157b04679225ba97a5658dd995ea762c2ab13a62eb5221bfdb8c724b712e2f7bac2a052cfe7dedf7896dc91d7d798e2a2edd0a7d9de28ac23c542047fa490
@@ -0,0 +1,127 @@
1
+ module OpenTrons
2
+ class Commands
3
+ attr_accessor :protocol, :command_list
4
+
5
+ def initialize(protocol)
6
+ @protocol = protocol
7
+ @command_list = []
8
+ end
9
+
10
+ # return a pure list of hashes
11
+ def to_list
12
+ return command_list.map {|command| command.to_hash}
13
+ end
14
+
15
+ def to_s
16
+ "<OpenTron::Commands:0x#{self.__id__.to_s(16)}>"
17
+ end
18
+
19
+ def inspect
20
+ to_s
21
+ end
22
+ end
23
+
24
+ class Command
25
+ attr_accessor :command, :params
26
+
27
+ #parent class for all the comands
28
+ def initialize(command, params)
29
+ @command = command
30
+ @params = params
31
+ end
32
+
33
+ def to_hash
34
+ as_hash = {}
35
+ as_hash["command"] = command
36
+ as_hash["params"] = params
37
+ return as_hash
38
+ end
39
+
40
+ def to_s
41
+ "<OpenTron::Command:0x#{self.__id__.to_s(16)}>"
42
+ end
43
+
44
+ def inspect
45
+ to_s
46
+ end
47
+ end
48
+
49
+ class PipetteCommand < Command
50
+ attr_accessor :pipette, :location
51
+
52
+ def initialize(command, pipette, location)
53
+ super(command, {})
54
+
55
+ pipette_id = pipette.instruments.instrument_hash.key pipette
56
+ params["pipette"] = pipette_id
57
+
58
+ if location.is_a? Array
59
+ labware_item = location[0].labware_item
60
+ params["labware"] = labware_item.labware.labware_hash.key labware_item
61
+ params["well"] = location[0].location
62
+ params["position"] = location[1]
63
+ else
64
+ labware_item = location.labware_item
65
+ params["labware"] = labware_item.labware.labware_hash.key labware_item
66
+ params["well"] = location.location
67
+ end
68
+ end
69
+ end
70
+
71
+ class VolumeCommand < PipetteCommand
72
+ attr_accessor :volume
73
+
74
+ def initialize(command, pipette, volume, location)
75
+ super(command, pipette, location)
76
+ params["volume"] = volume
77
+ end
78
+ end
79
+
80
+ class Delay < Command
81
+ def initialize(wait, message)
82
+ super("delay", {"wait" => wait, "message" => message})
83
+ end
84
+ end
85
+
86
+ class Aspirate < VolumeCommand
87
+ def initialize(pipette, volume, location)
88
+ super("aspirate", pipette, volume, location)
89
+ end
90
+ end
91
+
92
+ class Dispense < VolumeCommand
93
+ def initialize(pipette, volume, location)
94
+ super("dispense", pipette, volume, location)
95
+ end
96
+ end
97
+
98
+ class AirGap < VolumeCommand
99
+ def initialize(pipette, volume, location)
100
+ super("air-gap", pipette, volume, location)
101
+ end
102
+ end
103
+
104
+ class PickUpTip < PipetteCommand
105
+ def initialize(pipette, location)
106
+ super("pick-up-tip", pipette, location)
107
+ end
108
+ end
109
+
110
+ class DropTip < PipetteCommand
111
+ def initialize(pipette, location)
112
+ super("drop-tip", pipette, location)
113
+ end
114
+ end
115
+
116
+ class TouchTip < PipetteCommand
117
+ def initialize(pipette, location)
118
+ super("touch-tip", pipette, location)
119
+ end
120
+ end
121
+
122
+ class BlowOut < PipetteCommand
123
+ def initialize(pipette, location)
124
+ super("blowout", pipette, location)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,243 @@
1
+ module OpenTrons
2
+ PIPETTE_NAMES = {
3
+ P10_Single: "p10_single_v1",
4
+ P10_Multi: "p10_multi_v1",
5
+ P50_Single: "p50_single_v1",
6
+ P50_Multi: "p50_multi_v1",
7
+ P300_Single: "p300_single_v1",
8
+ P300_Multi: "p300_multi_v1",
9
+ P1000_Single: "p1000_single_v1",
10
+ P1000_Multi: "p1000_multi_v1"
11
+ }
12
+
13
+ class Instruments
14
+ attr_accessor :protocol, :instrument_hash
15
+
16
+ def initialize(protocol)
17
+ @protocol = protocol
18
+ @instrument_hash = {}
19
+ end
20
+
21
+ # Individual methods to maintain similarities w/ Python API.
22
+ PIPETTE_NAMES.each do |method_name, pipette_name|
23
+ define_method(method_name) do |**kwargs|
24
+
25
+ mount = kwargs.fetch(:mount, "left")
26
+ tip_racks = kwargs.fetch(:tip_racks, [])
27
+ tip_model = kwargs.fetch(:tip_model, nil)
28
+
29
+ return add_pipette(pipette_name, mount: mount, tip_racks: tip_racks, tip_model: tip_model)
30
+ end
31
+ end
32
+
33
+ def add_pipette(model, mount: "left", tip_racks: [], tip_model: nil)
34
+ instrument_hash.each do |key, item|
35
+ if item.mount == mount
36
+ raise ArgumentError.new "Cannot place #{model} on mount #{mount} (already occupied)."
37
+ end
38
+ end
39
+
40
+ generated_id = ""
41
+ loop do
42
+ generated_id = model + "-" + rand(100000...999999).to_s
43
+ break if !(instrument_hash.key? generated_id)
44
+ end
45
+
46
+ if model.include? "multi"
47
+ pipette = MultiPipette.new(protocol, self, model, mount: mount, tip_racks: tip_racks, tip_model: tip_model)
48
+ else
49
+ pipette = Pipette.new(protocol, self, model, mount: mount, tip_racks: tip_racks, tip_model: tip_model)
50
+ end
51
+
52
+ instrument_hash[generated_id] = pipette
53
+
54
+ return pipette
55
+ end
56
+
57
+ def to_hash
58
+ as_hash = {}
59
+ instrument_hash.each do |key, item|
60
+ as_hash[key] = item.to_hash
61
+ end
62
+ return as_hash
63
+ end
64
+
65
+ def to_s
66
+ "<OpenTron::Instruments:0x#{self.__id__.to_s(16)}>"
67
+ end
68
+
69
+ def inspect
70
+ to_s
71
+ end
72
+ end
73
+
74
+ class Pipette
75
+ attr_accessor :protocol, :instruments, :mount, :model, :tip_racks, :tip_model
76
+
77
+ def initialize(protocol, instruments, model, mount: "left", tip_racks: [], tip_model: nil)
78
+ @protocol = protocol
79
+ @instruments = instruments
80
+ @model = model
81
+
82
+ @mount = mount
83
+ @tip_racks = tip_racks
84
+ @tip_model = tip_model
85
+ end
86
+
87
+ def to_hash
88
+ as_hash = {}
89
+ as_hash["mount"] = mount
90
+ as_hash["model"] = model
91
+ return as_hash
92
+ end
93
+
94
+ def to_s
95
+ "<OpenTron::Pipette:0x#{self.__id__.to_s(16)}>"
96
+ end
97
+
98
+ def inspect
99
+ to_s
100
+ end
101
+
102
+ def aspirate(volume, location)
103
+ command = Aspirate.new(self, volume, location)
104
+ protocol.commands.command_list << command
105
+ return command
106
+ end
107
+
108
+ def dispense(volume, location)
109
+ command = Dispense.new(self, volume, location)
110
+ protocol.commands.command_list << command
111
+ return command
112
+ end
113
+
114
+ # For whatever reason, air gap has no location in Python API but has a location in the JSON schema.
115
+ # Not implemented for now.
116
+ # def air_gap(volume, location)
117
+ # command = Aspirate.new(self, volume, location)
118
+ # self.protocol.commands << command
119
+ # return command
120
+ # end
121
+
122
+ def get_next_tip(multi: false)
123
+ location = nil
124
+ catch :tip_found do
125
+ tip_racks.each do |tip_rack|
126
+ tip_rack.well_list.each do |column|
127
+ if multi
128
+ if column.all? {|x| x.tip}
129
+ location = column[0]
130
+ throw :tip_found
131
+ end
132
+ else
133
+ column.each do |x|
134
+ if x.tip
135
+ location = x
136
+ throw :tip_found
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ return false
143
+ end
144
+
145
+ return location
146
+ end
147
+
148
+ def pick_up_tip(location=false)
149
+ if !location
150
+ tip_location = self.get_next_tip()
151
+ # If no tip found and tip model is provided, create a tip rack.
152
+ if !tip_location
153
+ if tip_model
154
+ tip_racks << protocol.labware.load(tip_model, protocol.labware.free_slots[-1], 'Auto-generated-tip-rack')
155
+ else
156
+ raise ArgumentError.new "pick_up_tip called without location and pipette is out of tips."
157
+ end
158
+ end
159
+ tip_location = self.get_next_tip()
160
+ location = tip_location
161
+ end
162
+
163
+ if location.is_a? Array
164
+ well = location[0]
165
+ else
166
+ well = location
167
+ end
168
+
169
+ if !(well.tip)
170
+ puts "Warning: Already picked up tip at #{location}."
171
+ else
172
+ well.tip = false
173
+ end
174
+
175
+ command = PickUpTip.new(self, location)
176
+ protocol.commands.command_list << command
177
+ return command
178
+ end
179
+
180
+ def drop_tip(location=false)
181
+ location = protocol.trash.wells(0) if !location
182
+ command = DropTip.new(self, location)
183
+ protocol.commands.command_list << command
184
+ return command
185
+ end
186
+
187
+ def touch_tip(location)
188
+ command = TouchTip.new(self, location)
189
+ protocol.commands.command_list << command
190
+ return command
191
+ end
192
+
193
+ def blowout(location)
194
+ command = Blowout.new(self, location)
195
+ protocol.commands.command_list << command
196
+ return command
197
+ end
198
+
199
+ def delay(wait, message: "")
200
+ command = Delay.new(wait, message)
201
+ protocol.commands.command_list << command
202
+ return command
203
+ end
204
+ end
205
+
206
+ class MultiPipette < Pipette
207
+ def initialize(protocol, instruments, model, mount: "left", tip_racks: [], tip_model: nil)
208
+ super(protocol, instruments, model, mount: mount, tip_racks: tip_racks, tip_model: tip_model)
209
+ end
210
+
211
+ def pick_up_tip(location: false)
212
+ if !location
213
+ tip_location = self.get_next_tip(multi: true)
214
+ # If no tip found and tip model is provided, create a tip rack.
215
+ if !tip_location
216
+ if tip_model
217
+ tip_racks += protocol.labware.load(tip_model, protocol.labware.free_slots[-1], 'Auto-generated-tip-rack')
218
+ else
219
+ raise ArgumentError.new "pick_up_tip called without location and pipette is out of tips."
220
+ end
221
+ end
222
+ tip_location = self.get_next_tip(multi: true)
223
+ location = tip_location
224
+ end
225
+
226
+ if location.is_a? Array
227
+ well = location[0]
228
+ else
229
+ well = location
230
+ end
231
+
232
+ column = well.labware_item.well_list.find do |column|
233
+ column.include? well
234
+ end
235
+
236
+ column.each {|x| x.tip = false}
237
+
238
+ command = PickUpTip.new(self, location)
239
+ protocol.commands.command_list << command
240
+ return command
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,156 @@
1
+ module OpenTrons
2
+ class Labware
3
+ attr_accessor :protocol, :labware_hash, :labware_definitions
4
+
5
+ def initialize(protocol)
6
+ @protocol = protocol
7
+ @labware_hash = {}
8
+
9
+ #TODO: Better system for dealing with labware defs, including user-specified.
10
+ @labware_definitions = []
11
+ directory = File.expand_path(File.dirname(__FILE__))
12
+ directory = File.join(directory, "..", "..", "definitions")
13
+ Dir[directory + "/*.json"].each do |filename|
14
+ labware_definitions << JSON.parse(File.read(filename))
15
+ end
16
+ end
17
+
18
+ def load(model, slot, display_name="")
19
+ generated_id = ""
20
+ loop do
21
+ generated_id = display_name + "-" + rand(100000...999999).to_s
22
+ break if !(labware_hash.key?(generated_id))
23
+ end
24
+
25
+ labware_item = LabwareItem.new(self, model, slot, display_name)
26
+
27
+ labware_hash[generated_id] = labware_item
28
+
29
+ return labware_item
30
+ end
31
+
32
+ def free_slots
33
+ slots = (1..12).to_a.map{|x| x.to_s}
34
+ taken_slots = labware_hash.map {|key, item| item.slot}
35
+ return slots.select{|x| !(taken_slots.include? x)}
36
+ end
37
+
38
+ # Returns a pure hash of hashes.
39
+ def to_hash
40
+ as_hash = {}
41
+ labware_hash.each do |key, item|
42
+ as_hash[key] = item.to_hash
43
+ end
44
+ return as_hash
45
+ end
46
+
47
+ def to_s
48
+ "<OpenTron::Labware:#{object_id}>"
49
+ end
50
+
51
+ def inspect
52
+ to_s
53
+ end
54
+ end
55
+
56
+ class LabwareItem
57
+ attr_accessor :labware, :well_list, :model, :slot, :display_name, :definition
58
+
59
+ def initialize(labware, model, slot, display_name)
60
+ if labware.labware_hash.map {|key, item| item.slot}.include? slot
61
+ raise ArgumentError.new "Cannot place #{display_name} in slot #{slot} (already occupied)."
62
+ end
63
+
64
+ @labware = labware
65
+ @model = model
66
+ @slot = slot
67
+ @display_name = display_name
68
+ @definition = labware.labware_definitions.find{|x| x["metadata"]["name"] == model}
69
+ @well_list = []
70
+ definition["ordering"].each do |column|
71
+ well_list << column.map {|x| Well.new(self, x)}
72
+ end
73
+
74
+ end
75
+
76
+ def wells(location=nil)
77
+ if location.is_a? String
78
+ well_list.each do |column|
79
+ column.each do |x|
80
+ if x.location == location
81
+ return x
82
+ end
83
+ end
84
+ end
85
+ return ArgumentError.new "Well #{location} is out of range."
86
+ elsif location.is_a? Integer
87
+ i = location
88
+ well_list.each do |column|
89
+ column.each do |x|
90
+ if i == 0
91
+ return x
92
+ end
93
+ i -= 1
94
+ end
95
+ end
96
+ return ArgumentError.new "Well #{location} is out of range."
97
+ else
98
+ return well_list
99
+ end
100
+ end
101
+
102
+ def to_hash
103
+ as_hash = {}
104
+ as_hash["model"] = model
105
+ as_hash["slot"] = slot
106
+ as_hash["display-name"] = display_name
107
+ return as_hash
108
+ end
109
+
110
+ def to_s
111
+ "<OpenTron::LabwareItem:0x#{self.__id__.to_s(16)}>"
112
+ end
113
+
114
+ def inspect
115
+ to_s
116
+ end
117
+ end
118
+
119
+ class Well
120
+ attr_accessor :labware_item, :location, :tip
121
+
122
+ def initialize(labware_item, location)
123
+ @labware_item = labware_item
124
+ @location = location
125
+ @tip = true
126
+ end
127
+
128
+ def top(z)
129
+ position = {
130
+ "anchor" => "top",
131
+ "offset" => {
132
+ "z" => z
133
+ }
134
+ }
135
+ return [self, position]
136
+ end
137
+
138
+ def bottom(z)
139
+ position = {
140
+ "anchor" => "bottom",
141
+ "offset" => {
142
+ "z" => z
143
+ }
144
+ }
145
+ return [self, position]
146
+ end
147
+
148
+ def to_s
149
+ "<OpenTron::Well:0x#{self.__id__.to_s(16)}>"
150
+ end
151
+
152
+ def inspect
153
+ to_s
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,81 @@
1
+ module OpenTrons
2
+ # This gem is not a product of OpenTrons.
3
+ #
4
+ # This version of the gem provides some basic functionality for creating and editing OpenTrons JSON-format
5
+ # protocols in Ruby. The organization of the methods should be familiar to anyone who has used
6
+ # the official OpenTrons Python API.
7
+ #
8
+ # Examples:
9
+ # Import the gem: require 'opentrons'
10
+ # Create a protocol: p = OpenTrons::OTProtocol.new
11
+ # Create a labware item: block = p.labware.load('96-deep-well', '2', 'culture_block')
12
+ # Create a tiprack: tip_rack_1 = p.labware.load('tiprack-10ul', '3', 'tiprack-10ul')
13
+ # Create a pipette: p10 = p.instruments.P10_Single(mount: 'left', tip_racks: [tip_rack_1])
14
+ # Pick up a tip: p10.pick_up_tip()
15
+ # Add an aspirate command: p10.aspirate(10, block.wells(0))
16
+ # Add a dispense command: p10.dispense(10, block.wells(1).top(2))
17
+ # Discard tip: p10.drop_tip()
18
+ #
19
+ # Examples for saving protocols:
20
+ # Generate a hash of the protocol: p.to_hash
21
+ # Generate a JSON string of the protocol: p.to_json
22
+ # Save a protocol as a JSON file: File.open("protocol.json", 'w') {|f| f.write(p.to_json)}
23
+ #
24
+ # Limitations of current version:
25
+ # -Built for OT protocol JSON schema 1.0 (which is not the final version).
26
+ # https://github.com/Opentrons/opentrons/blob/391dcebe52411c432bb6f680d8aa5952a11fe90f/shared-data/protocol-json-schema/protocol-schema.json
27
+ # -Custom containers yet supported
28
+ # -Modules (heat and magnetic) not yet supported
29
+ # -Complex liquid handling shortcuts not yet supported
30
+ # -Loading and editing existing protocols from JSON not yet supported.
31
+ # -Error checking not very robust
32
+ # -And more...
33
+ class OTProtocol
34
+ attr_accessor :protocol_schema, :robot, :designer_application, :metadata, :labware, :instruments, :commands, :trash
35
+
36
+ def initialize(params: {})
37
+ @protocol_schema = params.fetch(:protocol_schema, "1.0.0")
38
+ @robot = params.fetch(:robot, {"model" => "OT-2 Standard"})
39
+ @designer_application = params.fetch(:designer_application, {})
40
+ @metadata = params.fetch(:metadata, {})
41
+
42
+ @labware = params.fetch(:labware, Labware.new(self))
43
+ @trash = labware.load('fixed-trash', '12', 'Trash')
44
+
45
+ @instruments = params.fetch(:instruments, Instruments.new(self))
46
+
47
+ @commands = params.fetch(:commands, Commands.new(self))
48
+ end
49
+
50
+ def to_hash(check_validity: true)
51
+ # Returns entire protocol as an OT-protocol-format hash (which can then be converted to json).
52
+ protocol_hash = {}
53
+
54
+ protocol_hash["protocol-schema"] = protocol_schema
55
+ protocol_hash["robot"] = robot
56
+ protocol_hash["designer_application"] = designer_application
57
+ protocol_hash["metadata"] = metadata
58
+
59
+ protocol_hash["labware"] = labware.to_hash
60
+
61
+ protocol_hash["pipettes"] = instruments.to_hash
62
+
63
+ protocol_hash["procedure"] = [{"subprocedure" => commands.to_list}]
64
+
65
+ return protocol_hash
66
+ end
67
+
68
+ def to_json(check_validity: true)
69
+ #converts protocol to a JSON-formatted string
70
+ return self.to_hash.to_json
71
+ end
72
+
73
+ def to_s
74
+ "<OpenTron::OTProtocol:0x#{self.__id__.to_s(16)}>"
75
+ end
76
+
77
+ def inspect
78
+ to_s
79
+ end
80
+ end
81
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentrons
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Emery
@@ -18,6 +18,10 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - lib/opentrons.rb
21
+ - lib/opentrons/commands.rb
22
+ - lib/opentrons/instruments.rb
23
+ - lib/opentrons/labware.rb
24
+ - lib/opentrons/otprotocol.rb
21
25
  homepage: https://github.com/emernic/opentrons_gem
22
26
  licenses:
23
27
  - LICENSE.txt