PageRenderTime 56ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/components/tools/OmeroPy/test/integration/metadata/test_populate.py

https://github.com/will-moore/openmicroscopy
Python | 416 lines | 368 code | 20 blank | 28 comment | 6 complexity | ef281f05f0c144de40bebb0582c9ee60 MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (C) 2015 Glencoe Software, Inc. All Rights Reserved.
  5. # Use is subject to license terms supplied in LICENSE.txt
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. """
  21. Test of the Tables service with the populate_metadata.py
  22. and populate_roi.py scripts.
  23. """
  24. import library as lib
  25. import string
  26. import csv
  27. import os.path
  28. import re
  29. from omero.api import RoiOptions
  30. from omero.grid import ImageColumn
  31. from omero.grid import RoiColumn
  32. from omero.grid import StringColumn
  33. from omero.model import PlateI, WellI, WellSampleI, OriginalFileI
  34. from omero.model import FileAnnotationI, MapAnnotationI, PlateAnnotationLinkI
  35. from omero.model import RoiAnnotationLinkI
  36. from omero.model import RoiI, PointI
  37. from omero.rtypes import rdouble, rint, rstring, unwrap
  38. from omero.util.populate_metadata import (
  39. ParsingContext, BulkToMapAnnotationContext, DeleteMapAnnotationContext)
  40. from omero.util.populate_roi import AbstractMeasurementCtx
  41. from omero.util.populate_roi import AbstractPlateAnalysisCtx
  42. from omero.util.populate_roi import MeasurementParsingResult
  43. from omero.util.populate_roi import PlateAnalysisCtxFactory
  44. from omero.constants.namespaces import NSBULKANNOTATIONS
  45. from omero.constants.namespaces import NSMEASUREMENT
  46. from omero.util.temp_files import create_path
  47. from pytest import skip
  48. def coord2offset(coord):
  49. """
  50. Convert a coordinate of the form AB12 into 0-based row-column indices
  51. TODO: This should go into a utils file somewhere
  52. """
  53. ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  54. m = re.match('([A-Z]+)([0-9]+)$', coord.upper())
  55. assert m
  56. ra, ca = m.groups()
  57. r = 0
  58. for a in ra:
  59. r = r * 26 + ALPHA.find(a) + 1
  60. c = int(ca)
  61. return r - 1, c - 1
  62. class BasePopulate(lib.ITest):
  63. def createCsv(
  64. self,
  65. colNames="Well,Well Type,Concentration",
  66. rowData=("A1,Control,0", "A2,Treatment,10")
  67. ):
  68. csvFileName = create_path("test", ".csv")
  69. csvFile = open(csvFileName, 'w')
  70. try:
  71. csvFile.write(colNames)
  72. csvFile.write("\n")
  73. csvFile.write("\n".join(rowData))
  74. finally:
  75. csvFile.close()
  76. return str(csvFileName)
  77. def createPlate(self, rowCount, colCount):
  78. plates = self.importPlates(plateRows=rowCount,
  79. plateCols=colCount)
  80. return plates[0]
  81. def createPlate1(self, rowCount, colCount):
  82. uuid = self.ctx.sessionUuid
  83. def createWell(row, column):
  84. well = WellI()
  85. well.row = rint(row)
  86. well.column = rint(column)
  87. ws = WellSampleI()
  88. image = self.new_image(name=uuid)
  89. ws.image = image
  90. well.addWellSample(ws)
  91. return well
  92. plate = PlateI()
  93. plate.name = rstring("TestPopulateMetadata%s" % uuid)
  94. for row in range(rowCount):
  95. for col in range(colCount):
  96. well = createWell(row, col)
  97. plate.addWell(well)
  98. return self.client.sf.getUpdateService().saveAndReturnObject(plate)
  99. class TestPopulateMetadata(BasePopulate):
  100. def setup_method(self, method):
  101. self.csvName = self.createCsv()
  102. self.rowCount = 1
  103. self.colCount = 2
  104. self.plate = self.createPlate(self.rowCount, self.colCount)
  105. def get_plate_annotations(self):
  106. query = """select p from Plate p
  107. left outer join fetch p.annotationLinks links
  108. left outer join fetch links.child
  109. where p.id=%s""" % self.plate.id.val
  110. qs = self.client.sf.getQueryService()
  111. plate = qs.findByQuery(query, None)
  112. anns = plate.linkedAnnotationList()
  113. return anns
  114. def get_well_annotations(self):
  115. query = """
  116. SELECT wal.child,wal.parent.id,wal.parent.row,wal.parent.column
  117. FROM WellAnnotationLink wal
  118. WHERE wal.parent.plate.id=%d""" % self.plate.id.val
  119. qs = self.client.sf.getQueryService()
  120. was = unwrap(qs.projection(query, None))
  121. return was
  122. def testPopulateMetadataPlate(self):
  123. """
  124. We should really test each of the parsing contexts in separate tests
  125. but in practice each one uses data created by the others, so for
  126. now just run them all together
  127. """
  128. try:
  129. import yaml
  130. print yaml, "found"
  131. except Exception:
  132. skip("PyYAML not installed.")
  133. self._test_parsing_context()
  134. self._test_bulk_to_map_annotation_context()
  135. self._test_delete_map_annotation_context()
  136. def _test_parsing_context(self):
  137. """
  138. Create a small csv file, use populate_metadata.py to parse and
  139. attach to Plate. Then query to check table has expected content.
  140. """
  141. ctx = ParsingContext(self.client, self.plate, file=self.csvName)
  142. ctx.parse()
  143. ctx.write_to_omero()
  144. # Get file annotations
  145. anns = self.get_plate_annotations()
  146. # Only expect a single annotation which is a 'bulk annotation'
  147. assert len(anns) == 1
  148. tableFileAnn = anns[0]
  149. assert unwrap(tableFileAnn.getNs()) == NSBULKANNOTATIONS
  150. fileid = tableFileAnn.file.id.val
  151. # Open table to check contents
  152. r = self.client.sf.sharedResources()
  153. t = r.openTable(OriginalFileI(fileid), None)
  154. cols = t.getHeaders()
  155. rows = t.getNumberOfRows()
  156. assert rows == self.rowCount * self.colCount
  157. for hit in range(rows):
  158. rowValues = [col.values[0] for col in t.read(range(len(cols)),
  159. hit, hit+1).columns]
  160. assert len(rowValues) == 4
  161. if "a1" in rowValues:
  162. assert "Control" in rowValues
  163. elif "a2" in rowValues:
  164. assert "Treatment" in rowValues
  165. else:
  166. assert False, "Row does not contain 'a1' or 'a2'"
  167. def _test_bulk_to_map_annotation_context(self):
  168. # self._testPopulateMetadataPlate()
  169. assert len(self.get_well_annotations()) == 0
  170. cfg = os.path.join(
  171. os.path.dirname(__file__), 'bulk_to_map_annotation_context.yml')
  172. fileid = self.get_plate_annotations()[0].file.id.val
  173. ctx = BulkToMapAnnotationContext(
  174. self.client, self.plate, fileid=fileid, cfg=cfg)
  175. ctx.parse()
  176. assert len(self.get_well_annotations()) == 0
  177. ctx.write_to_omero()
  178. was = self.get_well_annotations()
  179. assert len(was) == 2
  180. for ma, wid, wr, wc in was:
  181. assert isinstance(ma, MapAnnotationI)
  182. assert unwrap(ma.getNs()) == NSBULKANNOTATIONS
  183. mv = ma.getMapValueAsMap()
  184. assert mv['Well'] == str(wid)
  185. assert coord2offset(mv['Well Name']) == (wr, wc)
  186. if (wr, wc) == (0, 0):
  187. assert mv['Well Type'] == 'Control'
  188. assert mv['Concentration'] == '0'
  189. else:
  190. assert mv['Well Type'] == 'Treatment'
  191. assert mv['Concentration'] == '10'
  192. def _test_delete_map_annotation_context(self):
  193. # self._test_bulk_to_map_annotation_context()
  194. assert len(self.get_well_annotations()) == 2
  195. ctx = DeleteMapAnnotationContext(self.client, self.plate)
  196. ctx.parse()
  197. assert len(self.get_well_annotations()) == 2
  198. ctx.write_to_omero()
  199. assert len(self.get_well_annotations()) == 0
  200. class MockMeasurementCtx(AbstractMeasurementCtx):
  201. def well_name_to_number(self, well):
  202. m = re.match("(?P<COL>[a-z]+)(?P<ROW>\d+)",
  203. well, re.IGNORECASE)
  204. if not m:
  205. raise Exception("Bad well: %s" % well)
  206. col = m.group("COL").upper()
  207. row = m.group("ROW")
  208. row_num = int(row) - 1
  209. col_num = 0
  210. for c in col:
  211. i = string.ascii_uppercase.find(c)
  212. col_num += i + 1
  213. # wellnumber_from_colrow
  214. numcols = self.analysis_ctx.numcols
  215. return (col_num * numcols) + row_num
  216. def parse(self):
  217. provider = self.original_file_provider
  218. data = provider.get_original_file_data(self.original_file)
  219. try:
  220. rows = list(csv.reader(data, delimiter=","))
  221. finally:
  222. data.close()
  223. columns = [
  224. ImageColumn("Image", "", list()),
  225. RoiColumn("ROI", "", list()),
  226. StringColumn("Type", "", 12, list()),
  227. ]
  228. for row in rows[1:]:
  229. wellnumber = self.well_name_to_number(row[0])
  230. image = self.analysis_ctx.\
  231. image_from_wellnumber(wellnumber)
  232. # TODO: what to do with the field?!
  233. # field = int(row[1])
  234. # image = images[field]
  235. roi = RoiI()
  236. shape = PointI()
  237. shape.cx = rdouble(float(row[2]))
  238. shape.cy = rdouble(float(row[3]))
  239. shape.textValue = rstring(row[4])
  240. roi.addShape(shape)
  241. roi.image = image.proxy()
  242. rid = self.update_service\
  243. .saveAndReturnIds([roi])[0]
  244. columns[0].values.append(image.id.val)
  245. columns[1].values.append(rid)
  246. columns[2].values.append(row[4])
  247. return MeasurementParsingResult([columns])
  248. def get_name(self, *args, **kwargs):
  249. # Strip .csv
  250. return self.original_file.name.val[:-4]
  251. def parse_and_populate_roi(self, columns):
  252. # Remove from interface
  253. # Using this as a place to set file annotation
  254. self.file_annotation =\
  255. self.update_service.saveAndReturnObject(
  256. self.file_annotation)
  257. rois = columns[1].values
  258. for roi in rois:
  259. link = RoiAnnotationLinkI()
  260. link.parent = RoiI(roi, False)
  261. link.child = self.file_annotation.proxy()
  262. self.update_service\
  263. .saveObject(link)
  264. def populate(self, columns):
  265. self.update_table(columns)
  266. class MockPlateAnalysisCtx(AbstractPlateAnalysisCtx):
  267. def __init__(self, images, original_files,
  268. original_file_image_map,
  269. plate_id, service_factory):
  270. super(MockPlateAnalysisCtx, self).__init__(
  271. images, original_files, original_file_image_map,
  272. plate_id, service_factory
  273. )
  274. for original_file in original_files:
  275. name = original_file.name.val
  276. if name.endswith("csv"):
  277. self.measurements[len(self.measurements)] = \
  278. original_file
  279. def is_this_type(klass, original_files):
  280. for original_file in original_files:
  281. name = unwrap(original_file.name)
  282. if name.endswith(".csv"):
  283. return True
  284. is_this_type = classmethod(is_this_type)
  285. def get_measurement_count(self):
  286. return len(self.measurements)
  287. def get_measurement_ctx(self, index):
  288. sf = self.service_factory
  289. provider = self.DEFAULT_ORIGINAL_FILE_PROVIDER(sf)
  290. return MockMeasurementCtx(
  291. self, sf, provider,
  292. self.measurements[index], None)
  293. def get_result_file_count(self, index):
  294. return 1
  295. class TestPopulateRois(BasePopulate):
  296. def testPopulateRoisPlate(self):
  297. """
  298. Create a small csv file, use populate_roi.py to parse and
  299. attach to Plate. Then query to check table has expected content.
  300. """
  301. csvName = self.createCsv(
  302. colNames="Well,Field,X,Y,Type",
  303. rowData=("A1,0,15,15,Test",))
  304. rowCount = 1
  305. colCount = 1
  306. plate = self.createPlate(rowCount, colCount)
  307. # As opposed to the ParsingContext, here we are expected
  308. # to link the file ourselves
  309. ofile = self.client.upload(csvName).proxy()
  310. ann = FileAnnotationI()
  311. ann.file = ofile
  312. link = PlateAnnotationLinkI()
  313. link.parent = plate.proxy()
  314. link.child = ann
  315. link = self.client.sf.getUpdateService()\
  316. .saveAndReturnObject(link)
  317. # End linking
  318. factory = PlateAnalysisCtxFactory(self.client.sf)
  319. factory.implementations = (MockPlateAnalysisCtx,)
  320. ctx = factory.get_analysis_ctx(plate.id.val)
  321. assert 1 == ctx.get_measurement_count()
  322. meas = ctx.get_measurement_ctx(0)
  323. meas.parse_and_populate()
  324. # Get file annotations
  325. query = """select p from Plate p
  326. left outer join fetch p.annotationLinks links
  327. left outer join fetch links.child as ann
  328. left outer join fetch ann.file as file
  329. where p.id=%s""" % plate.id.val
  330. qs = self.client.sf.getQueryService()
  331. plate = qs.findByQuery(query, None)
  332. anns = plate.linkedAnnotationList()
  333. # Only expect a single annotation which is a 'bulk annotation'
  334. # the other is the original CSV
  335. assert len(anns) == 2
  336. files = dict(
  337. [(a.ns.val, a.file.id.val) for a in anns if a.ns])
  338. fileid = files[NSMEASUREMENT]
  339. # Open table to check contents
  340. r = self.client.sf.sharedResources()
  341. t = r.openTable(OriginalFileI(fileid), None)
  342. cols = t.getHeaders()
  343. rows = t.getNumberOfRows()
  344. assert rows == 1
  345. data = t.read(range(len(cols)), 0, 1)
  346. imag = data.columns[0].values[0]
  347. rois = self.client.sf.getRoiService()
  348. anns = rois.getRoiMeasurements(imag, RoiOptions())
  349. assert anns