def test_center_and_dimensions(self): b = Box.fromDegrees(-90, -45, 90, 45) self.assertEqual(b.getCenter(), LonLat.fromDegrees(0, 0)) self.assertEqual(b.getWidth(), Angle.fromDegrees(180)) self.assertEqual(b.getHeight(), Angle.fromDegrees(90)) self.assertEqual(b.getLon().getA(), NormalizedAngle.fromDegrees(-90)) self.assertEqual(b.getLat().getB(), Angle.fromDegrees(45))
def test_relationships(self): e = Ellipse(UnitVector3d.X(), Angle(math.pi / 3), Angle(math.pi / 6), Angle(0)) self.assertTrue(e.contains(UnitVector3d.X())) self.assertTrue(UnitVector3d.X() in e) c = Circle(UnitVector3d.X(), Angle(math.pi / 2)) self.assertEqual(c.relate(e), CONTAINS) self.assertEqual(e.relate(c), WITHIN)
def testComparisonOperators(self): a1 = Angle(1) a2 = Angle(2) self.assertNotEqual(a1, a2) self.assertLess(a1, a2) self.assertLessEqual(a1, a2) self.assertGreater(a2, a1) self.assertGreaterEqual(a2, a1)
def test_center_and_dimensions(self): e = Ellipse(UnitVector3d.X(), UnitVector3d.Y(), Angle(2 * math.pi / 3)) self.assertAlmostEqual(e.getF1().dot(UnitVector3d.X()), 1.0) self.assertAlmostEqual(e.getF2().dot(UnitVector3d.Y()), 1.0) self.assertAlmostEqual(e.getAlpha(), Angle(2 * math.pi / 3)) f = Ellipse(UnitVector3d.X(), Angle(math.pi / 3), Angle(math.pi / 6), Angle(0)) self.assertEqual(f.getCenter(), UnitVector3d.X())
def testConstruction(self): a1 = Angle(1.0) a2 = Angle.fromRadians(1.0) a3 = Angle.fromDegrees(57.29577951308232) self.assertEqual(a1, a2) self.assertEqual(a1.asRadians(), 1.0) self.assertEqual(a1, a3) self.assertEqual(a1.asDegrees(), 57.29577951308232)
def testArithmeticOperators(self): a = Angle(1) b = -a self.assertEqual(a + b, Angle(0)) self.assertEqual(a - b, 2.0 * a) self.assertEqual(a - b, a * 2.0) self.assertEqual(a / 1.0, a) self.assertEqual(a / a, 1.0) a += a a *= 2 a -= b a /= 5 self.assertEqual(a.asRadians(), 1)
def test_relationships(self): c = Circle(UnitVector3d.X(), Angle.fromDegrees(0.1)) d = Circle(UnitVector3d(1, 1, 1), Angle(math.pi / 2)) e = Circle(-UnitVector3d.X()) self.assertTrue(c.contains(UnitVector3d.X())) self.assertTrue(UnitVector3d.X() in c) self.assertTrue(d.contains(c)) self.assertTrue(c.isWithin(d)) self.assertTrue(c.intersects(d)) self.assertTrue(c.intersects(UnitVector3d.X())) self.assertTrue(e.isDisjointFrom(d)) self.assertEqual(d.relate(c), CONTAINS) self.assertEqual(e.relate(d), DISJOINT)
def test_construction(self): self.assertTrue(Circle.empty().isEmpty()) self.assertTrue(Circle().isEmpty()) self.assertTrue(Circle.full().isFull()) c = Circle(UnitVector3d.X()) self.assertEqual(c.getOpeningAngle(), Angle(0)) self.assertEqual(c.getSquaredChordLength(), 0) c = Circle(UnitVector3d.Z(), 2.0) self.assertTrue(c.contains(UnitVector3d.Z())) c = Circle(UnitVector3d.Z(), Angle(math.pi)) self.assertTrue(c.isFull()) d = c.clone() self.assertEqual(c, d) self.assertNotEqual(id(c), id(d)) e = Circle(d) self.assertEqual(d, e)
def test_construction(self): self.assertTrue(Ellipse.empty().isEmpty()) self.assertTrue(Ellipse().isEmpty()) self.assertTrue(Ellipse.full().isFull()) e = Ellipse(Circle(UnitVector3d.X(), Angle(math.pi / 2))) f = Ellipse(UnitVector3d.X(), Angle(math.pi / 2)) self.assertEqual(e, f) self.assertEqual(e.getAlpha(), e.getBeta()) self.assertTrue(e.isCircle()) self.assertTrue(e.isGreatCircle()) g = Ellipse(e) h = e.clone() self.assertEqual(e, g) self.assertEqual(g, h) self.assertNotEqual(id(e), id(g)) self.assertNotEqual(id(g), id(h))
def test_dilation_and_erosion(self): a = Angle(math.pi / 2) c = Circle(UnitVector3d.X()) d = c.dilatedBy(a).erodedBy(a) c.dilateBy(a).erodeBy(a) self.assertEqual(c, d) self.assertEqual(c, Circle(UnitVector3d.X()))
def testArithmeticOperators(self): a = NormalizedAngle(1) b = -a self.assertEqual(a + b, Angle(0)) self.assertEqual(a - b, 2.0 * a) self.assertEqual(a - b, a * 2.0) self.assertEqual(a / 1.0, a) self.assertEqual(a / a, 1.0)
def _makePixelRanges(): """Generate pixel ID ranges for some envelope region""" pointing_v = UnitVector3d(1., 1., -1.) fov = 0.05 # radians region = Circle(pointing_v, Angle(fov / 2)) pixelator = HtmPixelization(HTM_LEVEL) indices = pixelator.envelope(region, 128) return indices.ranges()
def test_string(self): c = Circle(UnitVector3d.Z(), Angle(1.0)) self.assertEqual(str(c), 'Circle([0.0, 0.0, 1.0], 1.0)') self.assertEqual(repr(c), 'Circle(UnitVector3d(0.0, 0.0, 1.0), Angle(1.0))') self.assertEqual( c, eval(repr(c), dict(Angle=Angle, Circle=Circle, UnitVector3d=UnitVector3d)))
def testConstruction(self): v = Vector3d(1, 1, 1) u = UnitVector3d.orthogonalTo(v) self.assertAlmostEqual(u.dot(v), 0.0, places=15) u = UnitVector3d(1, 1, 1) self.assertEqual(u, UnitVector3d(Vector3d(1, 1, 1))) self.assertAlmostEqual(u.x(), math.sqrt(3.0) / 3.0, places=15) self.assertAlmostEqual(u.y(), math.sqrt(3.0) / 3.0, places=15) self.assertAlmostEqual(u.z(), math.sqrt(3.0) / 3.0, places=15) u = UnitVector3d(Angle.fromDegrees(45), Angle.fromDegrees(45)) self.assertEqual(u, UnitVector3d(LonLat.fromDegrees(45, 45))) self.assertAlmostEqual(u.x(), 0.5, places=15) self.assertAlmostEqual(u.y(), 0.5, places=15) self.assertAlmostEqual(u.z(), 0.5 * math.sqrt(2.0), places=15) u = UnitVector3d.northFrom(u.asVector3d()) w = UnitVector3d(LonLat.fromDegrees(225, 45)) self.assertAlmostEqual(u.x(), w.x(), places=15) self.assertAlmostEqual(u.y(), w.y(), places=15) self.assertAlmostEqual(u.z(), w.z(), places=15)
def testConstruction(self): v = Vector3d(1, 1, 1) u = UnitVector3d.orthogonalTo(v) self.assertAlmostEqual(u.dot(v), 0.0, places=15) a = UnitVector3d(1, 1, 1) self.assertEqual(a, UnitVector3d(Vector3d(1, 1, 1))) self.assertAlmostEqual(a.x(), math.sqrt(3.0) / 3.0, places=15) self.assertAlmostEqual(a.y(), math.sqrt(3.0) / 3.0, places=15) self.assertAlmostEqual(a.z(), math.sqrt(3.0) / 3.0, places=15) b = UnitVector3d(Angle.fromDegrees(45), Angle.fromDegrees(45)) self.assertEqual(b, UnitVector3d(LonLat.fromDegrees(45, 45))) self.assertAlmostEqual(b.x(), 0.5, places=15) self.assertAlmostEqual(b.y(), 0.5, places=15) self.assertAlmostEqual(b.z(), 0.5 * math.sqrt(2.0), places=15) c = UnitVector3d.northFrom(b) d = UnitVector3d(LonLat.fromDegrees(225, 45)) self.assertAlmostEqual(c.x(), d.x(), places=15) self.assertAlmostEqual(c.y(), d.y(), places=15) self.assertAlmostEqual(c.z(), d.z(), places=15)
def test_envelope_and_interior(self): pixelization = HtmPixelization(3) c = Circle(UnitVector3d(1, 1, 1), Angle.fromDegrees(0.1)) rs = pixelization.envelope(c) self.assertTrue(rs == RangeSet(0x3ff)) rs = pixelization.envelope(c, 1) self.assertTrue(rs == RangeSet(0x3ff)) self.assertTrue(rs.isWithin(pixelization.universe())) rs = pixelization.interior(c) self.assertTrue(rs.empty())
def test_envelope_and_interior(self): pixelization = Mq3cPixelization(1) c = Circle(UnitVector3d(1.0, -0.5, -0.5), Angle.fromDegrees(0.1)) rs = pixelization.envelope(c) self.assertTrue(rs == RangeSet(44)) rs = pixelization.envelope(c, 1) self.assertTrue(rs == RangeSet(44)) self.assertTrue(rs.isWithin(pixelization.universe())) rs = pixelization.interior(c) self.assertTrue(rs.empty())
def testConstruction(self): p = LonLat.fromDegrees(45, 45) self.assertEqual(p, LonLat(NormalizedAngle.fromDegrees(45), Angle.fromDegrees(45))) u = UnitVector3d(p) q = LonLat(u) self.assertAlmostEqual(p.getLon().asRadians(), q.getLon().asRadians(), places=13) self.assertAlmostEqual(p.getLat().asRadians(), q.getLat().asRadians(), places=13) self.assertAlmostEqual(p.getLon().asRadians(), LonLat.latitudeOf(u).asRadians(), places=13) self.assertAlmostEqual(p.getLon().asRadians(), LonLat.longitudeOf(u).asRadians(), places=13)
def test_construction(self): b = Box(Box.allLongitudes(), Box.allLatitudes()) self.assertTrue(b.isFull()) b = Box.fromDegrees(-90, -45, 90, 45) self.assertEqual(b, Box(b.getLon(), b.getLat())) a = Box.fromRadians(-0.5 * math.pi, -0.25 * math.pi, 0.5 * math.pi, 0.25 * math.pi) b = Box(LonLat.fromRadians(-0.5 * math.pi, -0.25 * math.pi), LonLat.fromRadians(0.5 * math.pi, 0.25 * math.pi)) c = Box(LonLat.fromRadians(0, 0), Angle(0.5 * math.pi), Angle(0.25 * math.pi)) d = c.clone() self.assertEqual(a, b) self.assertEqual(b, c) self.assertEqual(c, d) self.assertNotEqual(id(c), id(d)) b = Box() self.assertTrue(b.isEmpty()) self.assertTrue(Box.empty().isEmpty()) self.assertTrue(Box.full().isFull())
def test_string(self): c = Ellipse(UnitVector3d.Z(), Angle(1.0)) self.assertEqual(str(c), 'Ellipse([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], 1.0)') self.assertEqual( repr(c), 'Ellipse(UnitVector3d(0.0, 0.0, 1.0), ' 'UnitVector3d(0.0, 0.0, 1.0), Angle(1.0))') self.assertEqual( c, eval(repr(c), dict(Angle=Angle, Ellipse=Ellipse, UnitVector3d=UnitVector3d)))
def testConstruction(self): p = LonLat.fromDegrees(45, 45) self.assertEqual(p, LonLat(NormalizedAngle.fromDegrees(45), Angle.fromDegrees(45))) u = UnitVector3d(p) q = LonLat(u) self.assertAlmostEqual( p.getLon().asRadians(), q.getLon().asRadians(), places=13) self.assertAlmostEqual( p.getLat().asRadians(), q.getLat().asRadians(), places=13) self.assertAlmostEqual(p.getLon().asRadians(), LonLat.latitudeOf(u).asRadians(), places=13) self.assertAlmostEqual(p.getLon().asRadians(), LonLat.longitudeOf(u).asRadians(), places=13)
def testRotation(self): v = UnitVector3d.Y().rotatedAround(UnitVector3d.X(), Angle(0.5 * math.pi)) self.assertAlmostEqual(v.x(), 0.0, places=15) self.assertAlmostEqual(v.y(), 0.0, places=15) self.assertAlmostEqual(v.z(), 1.0, places=15)
def test_dilation_and_erosion(self): a = Box.fromRadians(0.5, -0.5, 1.5, 0.5) b = a.dilatedBy(Angle(0.5), Angle(0.5)).erodedBy(Angle(1), Angle(1)) a.dilateBy(Angle(0.5), Angle(0.5)).erodeBy(Angle(1), Angle(1)) self.assertEqual(a, b) self.assertEqual(a, LonLat.fromRadians(1, 0))
def test_complement(self): e = Ellipse(UnitVector3d.X(), Angle(math.pi / 3), Angle(math.pi / 6), Angle(0)) f = e.complemented().complement() self.assertEqual(e, f)
def test_codec(self): e = Ellipse(UnitVector3d.X(), UnitVector3d.Y(), Angle(2 * math.pi / 3)) s = e.encode() self.assertEqual(Ellipse.decode(s), e) self.assertEqual(Region.decode(s), e)
def run(self) -> Optional[int]: """Run whole shebang. """ # load configurations if self.args.app_config: self.config.load(self.args.app_config) if self.args.backend == "sql": self.dbconfig = ApdbSqlConfig() if self.args.config: self.dbconfig.load(self.args.config) elif self.args.backend == "cassandra": self.dbconfig = ApdbCassandraConfig() if self.args.config: self.dbconfig.load(self.args.config) if self.args.dump_config: self.config.saveToStream(sys.stdout) self.dbconfig.saveToStream(sys.stdout) return 0 # instantiate db interface db: Apdb if self.args.backend == "sql": db = ApdbSql(config=self.dbconfig) elif self.args.backend == "cassandra": db = ApdbCassandra(config=self.dbconfig) visitInfoStore = VisitInfoStore(self.args.visits_file) num_tiles = 1 if self.config.divide != 1: tiles = geom.make_tiles(self.config.FOV_rad, self.config.divide) num_tiles = len(tiles) # check that we have reasonable MPI setup if self.config.mp_mode == "mpi": comm = MPI.COMM_WORLD num_proc = comm.Get_size() rank = comm.Get_rank() node = MPI.Get_processor_name() _LOG.info( COLOR_YELLOW + "MPI job rank=%d size=%d, node %s" + COLOR_RESET, rank, num_proc, node) if num_proc != num_tiles: raise ValueError( f"Number of MPI processes ({num_proc}) " f"does not match number of tiles ({num_tiles})") if rank != 0: # run simple loop for all non-master processes self.run_mpi_tile_loop(db, comm) return None # Initialize starting values from database visits table last_visit = visitInfoStore.lastVisit() if last_visit is not None: start_visit_id = last_visit.visitId + 1 nsec = last_visit.visitTime.nsecs( DateTime.TAI) + self.config.interval * 1_000_000_000 start_time = DateTime(nsec, DateTime.TAI) else: start_visit_id = self.config.start_visit_id start_time = self.config.start_time_dt if self.config.divide > 1: _LOG.info("Will divide FOV into %d regions", num_tiles) src_read_period = self.config.src_read_period src_read_visits = round(self.config.src_read_period * self.config.src_read_duty_cycle) _LOG.info("Will read sources for %d visits out of %d", src_read_visits, src_read_period) # read sources file _LOG.info("Start loading variable sources from %r", self.config.sources_file) var_sources = numpy.load(self.config.sources_file) _LOG.info("Finished loading variable sources, count = %s", len(var_sources)) # diaObjectId for last new DIA object, for variable sources we use their # index as objectId, for transients we want to use ID outside that range if last_visit is not None and last_visit.lastObjectId is not None: self.lastObjectId = max(self.lastObjectId, last_visit.lastObjectId) if self.lastObjectId < len(var_sources): _LOG.error('next object id is too low: %s', self.lastObjectId) return 1 _LOG.debug("lastObjectId: %s", self.lastObjectId) # diaSourceId for last DIA source stored in database if last_visit is not None and last_visit.lastSourceId is not None: self.lastSourceId = max(self.lastSourceId, last_visit.lastSourceId) _LOG.info("lastSourceId: %s", self.lastSourceId) # loop over visits visitTimes = _visitTimes(start_time, self.config.interval, self.args.num_visits) for visit_id, dt in enumerate(visitTimes, start_visit_id): if visit_id % 1000 == 0: _LOG.info(COLOR_YELLOW + "+++ Start daily activities" + COLOR_RESET) db.dailyJob() _LOG.info(COLOR_YELLOW + "+++ Done with daily activities" + COLOR_RESET) _LOG.info( COLOR_GREEN + "+++ Start processing visit %s at %s" + COLOR_RESET, visit_id, dt) loop_timer = timer.Timer().start() with timer.Timer("DIA"): # point telescope in random southern direction pointing_xyz = generators.rand_sphere_xyz(1, -1)[0] pointing_v = UnitVector3d(pointing_xyz[0], pointing_xyz[1], pointing_xyz[2]) ra = LonLat.longitudeOf(pointing_v).asDegrees() decl = LonLat.latitudeOf(pointing_v).asDegrees() # sphgeom.Circle opening angle is actually a half of opening angle region = Circle(pointing_v, Angle(self.config.FOV_rad / 2)) _LOG.info("Pointing ra, decl = %s, %s; xyz = %s", ra, decl, pointing_xyz) # Simulating difference image analysis dia = DIA.DIA( pointing_xyz, self.config.FOV_rad, var_sources, self.config.false_per_visit + self.config.transient_per_visit) sources, indices = dia.makeSources() _LOG.info("DIA generated %s sources", len(sources)) # assign IDs to transients for i in range(len(sources)): if indices[i] < 0: self.lastObjectId += 1 indices[i] = self.lastObjectId # print current database row counts, this takes long time # so only do it once in a while modu = 200 if visit_id <= 10000 else 1000 if visit_id % modu == 0: if hasattr(db, "tableRowCount"): counts = db.tableRowCount() # type: ignore for tbl, count in sorted(counts.items()): _LOG.info('%s row count: %s', tbl, count) # numpy seems to do some multi-threaded stuff which "leaks" CPU cycles to the code below # and it gets counted as resource usage in timers, add a short delay here so that threads # finish and don't influence our timers below. time.sleep(0.1) if self.config.divide == 1: # do it in-process with timer.Timer("VisitProcessing"): self.visit(db, visit_id, dt, region, sources, indices) else: if self.config.mp_mode == "fork": tiles = geom.make_tiles(self.config.FOV_rad, self.config.divide, pointing_v) with timer.Timer("VisitProcessing"): # spawn subprocesses to handle individual tiles children = [] for ix, iy, region in tiles: # make sure lastSourceId is unique in in each process self.lastSourceId += len(sources) tile = (ix, iy) pid = os.fork() if pid == 0: # child self.visit(db, visit_id, dt, region, sources, indices, tile) # stop here sys.exit(0) else: _LOG.debug("Forked process %d for tile %s", pid, tile) children.append(pid) # wait until all children finish for pid in children: try: pid, status = os.waitpid(pid, 0) if status != 0: _LOG.warning( COLOR_RED + "Child process PID=%s failed: %s" + COLOR_RESET, pid, status) except OSError as exc: _LOG.warning( COLOR_RED + "wait failed for PID=%s: %s" + COLOR_RESET, pid, exc) elif self.config.mp_mode == "mpi": tiles = geom.make_tiles(self.config.FOV_rad, self.config.divide, pointing_v) _LOG.info("Split FOV into %d tiles for MPI", len(tiles)) # spawn subprocesses to handle individual tiles, special # care needed for self.lastSourceId because it's # propagated back from (0, 0) lastSourceId = self.lastSourceId tile_data = [] for ix, iy, region in tiles: lastSourceId += len(sources) tile = (ix, iy) tile_data += [(visit_id, dt, region, sources, indices, tile, lastSourceId)] # make sure lastSourceId is unique in in each process with timer.Timer("VisitProcessing"): _LOG.info("Scatter sources to %d tile processes", len(tile_data)) self.run_mpi_tile(db, MPI.COMM_WORLD, tile_data) self.lastSourceId = lastSourceId if not self.args.no_update: # store last visit info visitInfoStore.saveVisit(visit_id, dt, self.lastObjectId, self.lastSourceId) _LOG.info( COLOR_BLUE + "--- Finished processing visit %s, time: %s" + COLOR_RESET, visit_id, loop_timer) # stop MPI slaves if num_tiles > 1 and self.config.mp_mode == "mpi": _LOG.info("Stopping MPI tile processes") tile_data_stop = [None] * self.config.divide**2 self.run_mpi_tile(db, MPI.COMM_WORLD, tile_data_stop) return 0
def index(self, exposure_or_metadata, data_id, database): """Spatially index an |exposure| or |metadata| object. Parameters ---------- exposure_or_metadata : lsst.afw.image.Exposure[DFILU] or lsst.daf.base.PropertySet An afw |exposure| or corresponding |metadata| object. data_id : object An object identifying a single exposure (e.g. as used by the butler). It must be possible to pickle `data_id`. database : sqlite3.Connection or str A connection to (or filename of) a SQLite 3 database. Returns ------- ``None``, unless the |defer_writes| coniguration parameter is ``True``. In that case, an :class:`.ExposureInfo` object containing a pickled data-id and an |encoded| |polygon| is returned. """ # Get a pixel index bounding box for the exposure. if isinstance(exposure_or_metadata, daf_base.PropertySet): md = exposure_or_metadata # Map (LTV1, LTV2) to LSST (x0, y0). LSST convention says that # (x0, y0) is the location of the sub-image origin (the bottom-left # corner) relative to the origin of the parent, whereas LTVi encode # the origin of the parent relative to the origin of the subimage. pixel_bbox = afw_image.bboxFromMetadata(md) wcs = afw_image.makeWcs(md, False) else: pixel_bbox = exposure_or_metadata.getBBox() wcs = exposure_or_metadata.getWcs() # Pad the box by a configurable amount and bail if the result is empty. pixel_bbox.grow(self.config.pad_pixels) if pixel_bbox.isEmpty(): self.log.warn("skipping exposure indexing for dataId=%s: " "empty bounding box", data_id) return corners = [] for c in pixel_bbox.getCorners(): # Convert the box corners from pixel indexes to pixel positions, # and then to sky coordinates. c = wcs.pixelToSky(afw_image.indexToPosition(c.getX()), afw_image.indexToPosition(c.getY())) c = (c.getLongitude().asRadians(), c.getLatitude().asRadians()) # Bail if any coordinate is not finite. if any(math.isinf(x) or math.isnan(x) for x in c): self.log.warn("skipping exposure indexing for dataId=%s: " "NaN or Inf in bounding box sky coordinate(s)" " - bad WCS?", data_id) return # Convert from sky coordinates to unit vectors. corners.append(UnitVector3d(Angle.fromRadians(c[0]), Angle.fromRadians(c[1]))) # Create a convex polygon containing the exposure pixels. When sphgeom # gains support for non-convex polygons, this could be changed to map # exposure.getPolygon() to a spherical equivalent, or to subdivide box # edges in pixel space to account for non linear projections. This # would have higher accuracy than the current approach of connecting # corner sky coordinates with great circles. poly = ConvexPolygon(corners) # Finally, persist or return the exposure information. info = ExposureInfo(pickle.dumps(data_id), poly.encode()) if self.config.defer_writes: return info store_exposure_info(database, self.config.allow_replace, info)
def test_comparison_operators(self): e = Ellipse(UnitVector3d.X(), UnitVector3d.Y(), Angle(2 * math.pi / 3)) f = Ellipse(UnitVector3d.X(), Angle(math.pi / 3), Angle(math.pi / 6), Angle(0)) self.assertEqual(e, e) self.assertNotEqual(e, f)
def testString(self): self.assertEqual(str(Angle(1)), '1.0') self.assertEqual(repr(Angle(1)), 'Angle(1.0)') a = Angle(2.5) self.assertEqual(a, eval(repr(a), dict(Angle=Angle)))
def test_yaml(self): a = Ellipse(UnitVector3d.X(), UnitVector3d.Y(), Angle(2 * math.pi / 3)) b = yaml.safe_load(yaml.dump(a)) self.assertEqual(a, b)
def test_pickle(self): a = Ellipse(UnitVector3d.X(), UnitVector3d.Y(), Angle(2 * math.pi / 3)) b = pickle.loads(pickle.dumps(a, pickle.HIGHEST_PROTOCOL)) self.assertEqual(a, b)
def testPickle(self): a = Angle(1.5) b = pickle.loads(pickle.dumps(a)) self.assertEqual(a, b)