Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

#!/usr/bin/env python3 

 

import re, numbers, collections 

from .helpers import round_to_pipet, UserInputError 

 

class Reaction: 

 

    def __init__(self, std_reagents=None): 

        self.reagents = collections.OrderedDict() 

        self._num_reactions = 1 

        self._extra_master_mix = 10 

        self.show_each_rxn = True 

        self.show_master_mix = True 

        self.show_totals = True 

 

        if std_reagents: 

            self.load_std_reagents(std_reagents) 

 

    def __repr__(self): 

        return 'Reaction()' 

 

    def __getitem__(self, name): 

        if name == 'master mix': 

            return MasterMix(self) 

 

        if name not in self.reagents: 

            if name.lower() == 'water': 

                self.reagents[name] = Water(self, name) 

            else: 

                self.reagents[name] = Reagent(self, name) 

 

        return self.reagents[name] 

 

    def __iter__(self): 

        yield from self.reagents.values() 

 

    def __str__(self): 

        return self.reagent_table 

 

    @property 

    def num_reactions(self): 

        return self._num_reactions 

 

    @num_reactions.setter 

    def num_reactions(self, num_reactions): 

        self._num_reactions = int(num_reactions) 

 

    @property 

    def extra_master_mix(self): 

        return self._extra_master_mix 

 

    @extra_master_mix.setter 

    def extra_master_mix(self, percent): 

        self._extra_master_mix = float(percent) 

 

    @property 

    def scale(self): 

        return self.num_reactions * (1 + self.extra_master_mix / 100) 

 

    @property 

    def volume(self): 

        return sum(x.std_volume for x in self) 

 

    @volume.setter 

    def volume(self, volume): 

        ratio = volume / self.volume 

        for reagent in self: 

            reagent.std_volume *= ratio 

 

    @property 

    def volume_unit(self): 

        return next(iter(self)).volume_unit 

 

    @property 

    def volume_str(self): 

        return '{} {}'.format(round_to_pipet(self.volume), self.volume_unit) 

 

    @property 

    def reagent_table(self): 

 

        def cols(*cols): 

            """ 

            Eliminate columns the user doesn't want to see. 

            """ 

            cols = list(cols) 

            if not self.show_master_mix: 

                del cols[3] 

            if not self.show_each_rxn: 

                del cols[2] 

            return cols 

 

        # Figure out how big the table should be. 

 

        column_titles = cols( 

                "Reagent", 

                "Conc", 

                "Each Rxn", 

                "Master Mix", 

        ) 

        column_footers = cols( 

                '', 

                '', 

                self.volume_str, 

                self['master mix'].volume_str, 

        ) 

        column_getters = cols( 

                lambda x: x.name, 

                lambda x: x.stock_conc_str, 

                lambda x: x.volume_str, 

                lambda x: x.scaled_volume_str, 

        ) 

        column_widths = [ 

                max(map(len, 

                    [title, footer] + [getter(x) for x in self])) 

                for title, footer,getter in 

                    zip(column_titles, column_footers, column_getters) 

        ] 

        column_alignments = '<>>>' 

        row_template = '  '.join( 

                '{{:{}{}}}'.format(column_alignments[i], column_widths[i]) 

                for i in range(len(column_titles)) 

        ) 

 

        # Print the table 

 

        rule = '─' * (sum(column_widths) + 2 * len(column_widths) - 2) 

        rows = [ 

            row_template.format(*column_titles), 

            rule, 

        ] + [ 

            row_template.format( 

                *[getter(reagent) for getter in column_getters]) 

            for reagent in self 

        ] 

        if self.show_totals and (self.show_each_rxn or self.show_master_mix): 

            rows += [ 

                rule, 

                row_template.format(*column_footers), 

            ] 

        return '\n'.join(rows) + '/rxn' 

 

    def load_std_reagents(self, std_reagents): 

        lines = std_reagents.strip().split('\n') 

 

        # Examine the second line of the table to determine where each column  

        # starts and stops. 

 

        column_slices = [ 

                slice(x.start(), x.end()) 

                for x in re.finditer('[-=]+', lines[1]) 

        ] 

        if len(column_slices) != 4: 

            raise UserInputError("Expected to find 4 columns, delineated by '=' or '-' in the second line.") 

 

        def split_columns(line): 

            return tuple(line[x].strip() for x in column_slices) 

 

        # Parse standard concentrations and volumes from each line in the  

        # table. 

 

        def parse_amount(x, unit_required=True): 

            try: 

                amount, unit = x.split() 

                return float(amount), unit 

            except ValueError: 

                if unit_required: raise 

                else: return x or None 

 

        for line in lines[2:]: 

            reagent, stock_conc, volume, master_mix = split_columns(line) 

            self[reagent].std_stock_conc = parse_amount(stock_conc, False) 

            self[reagent].std_volume = parse_amount(volume) 

            self[reagent].master_mix = (master_mix == 'yes') 

 

 

class Reagent: 

 

    def __init__(self, reaction, name): 

        self.reaction = reaction 

        self.name = name 

        self._std_volume = None 

        self._std_stock_conc = None 

        self.volume_unit = None 

        self.conc_unit = None 

        self.master_mix = False 

        self._stock_conc = None 

        self._conc = None 

 

    def __repr__(self): 

        return 'Reagent({0.name})'.format(self) 

 

    def __str__(self): 

        return '{0.volume_str} {0.name}'.format(self) 

 

    @property 

    def std_stock_conc(self): 

        return self._std_stock_conc 

 

    @std_stock_conc.setter 

    def std_stock_conc(self, conc): 

        if isinstance(conc, tuple): 

            self._std_stock_conc, self.conc_unit = conc 

        else: 

            self._std_stock_conc = conc 

 

    @property 

    def std_conc(self): 

        return self.std_stock_conc * self.std_volume / self.reaction.volume 

 

    @property 

    def std_volume(self): 

        return self._std_volume 

 

    @std_volume.setter 

    def std_volume(self, volume): 

        if isinstance(volume, tuple): 

            self._std_volume, self.volume_unit = volume 

        else: 

            self._std_volume = float(volume) 

 

    @property 

    def stock_conc(self): 

        return self._stock_conc or self.std_stock_conc 

 

    @stock_conc.setter 

    def stock_conc(self, stock_conc): 

        if not isinstance(self.std_stock_conc, numbers.Real): 

            raise UserInputError("The 'std_stock_conc' for '{}' isn't numeric, so 'stock_conc' can't be changed.".format(self.name)) 

        self._stock_conc = float(stock_conc) 

 

    @property 

    def stock_conc_str(self): 

        if self.stock_conc and self.conc_unit: 

            return '{0.stock_conc:.0f} {0.conc_unit}'.format(self) 

        elif self.stock_conc: 

            return '{0.stock_conc}'.format(self) 

        else: 

            return '' 

 

    @property 

    def conc(self): 

        return self._conc or self.std_conc 

 

    @conc.setter 

    def conc(self, conc): 

        if not isinstance(self.std_stock_conc, numbers.Real): 

            raise UserInputError("The 'std_stock_conc' for '{}' isn't numeric, so 'conc' can't be changed.".format(self.name)) 

        self._conc = float(conc) 

 

    @property 

    def volume(self): 

        # It isn't possible to calculate std_conc for reagents for which  

        # std_stock_conc is a string (i.e. '10x') or left undefined (i.e.   

        # water).  So don't calculate a new volume if std_volume will do. 

        if self._conc is None: 

            return self.std_volume 

        else: 

            return self.reaction.volume * self.conc / self.stock_conc 

 

    @property 

    def volume_str(self): 

        return '{} {}'.format(round_to_pipet(self.volume), self.volume_unit) 

 

    @property 

    def scaled_volume_str(self): 

        if not self.master_mix: 

            return '' 

        else: 

            return '{} {}'.format( 

                    round_to_pipet(self.volume * self.reaction.scale), 

                    self.volume_unit) 

 

 

class Water (Reagent): 

 

    @property 

    def volume(self): 

        volume = self.reaction.volume 

 

        for reagent in self.reaction: 

            if reagent is not self: 

                volume -= reagent.volume 

 

        if volume < 0: 

            from warnings import warn 

            warn("Reaction volume exceeds {}".format(self.reaction.volume_str)) 

 

        return volume 

 

 

class MasterMix (Reagent): 

 

    def __init__(self, reaction): 

        super().__init__(reaction, 'master mix') 

        self.master_mix = True 

 

    def __iter__(self): 

        yield from (x for x in self.reaction if x.master_mix) 

 

    @property 

    def volume(self): 

        return sum(x.volume for x in self) 

 

    @property 

    def volume_unit(self): 

        return self.reaction.volume_unit 

 

    @volume_unit.setter 

    def volume_unit(self, unit): 

        pass