Source code for galsim.position

# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions, and the disclaimer given in the accompanying LICENSE
#    file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions, and the disclaimer given in the documentation
#    and/or other materials provided with the distribution.
#

__all__ = [ 'Position', 'PositionI', 'PositionD', '_PositionI', '_PositionD', ]

from . import _galsim

[docs]class Position: """A class for representing 2D positions on the plane. Position is a base class for two slightly different kinds of positions: PositionD describes positions with floating point values in ``x`` and ``y``. PositionI described positions with integer values in ``x`` and ``y``. In the C++ layer, these are templates, but of course no such thing exists in Python, so the trailing D or I indicate the type. Initialization: For the float-valued position class, example initializations include:: >>> pos = galsim.PositionD(x=0.5, y=-0.5) >>> pos = galsim.PositionD(0.5, -0.5) >>> pos = galsim.PositionD( (0.5, -0.5) ) And for the integer-valued position class, example initializations include:: >>> pos = galsim.PositionI(x=45, y=13) >>> pos = galsim.PositionI(45, 13) >>> pos = galsim.PositionD( (45, 15) ) Attributes: x: The x component of the position y: The y component of the position Arithmetic: Most arithmetic that makes sense for a position is allowed:: >>> pos1 + pos2 >>> pos1 - pos2 >>> pos * x >>> pos / x >>> -pos >>> pos1 += pos2 >>> pos1 -= pos2 >>> pos *= x >>> pos -= x Note though that the types generally need to match. For example, you cannot multiply a PositionI by a float add a PositionD to a PositionI in place. Position instances can be sheared by a `galsim.Shear`:: >>> pos = galsim.PositionD(x=0.5, y=-0.5) >>> shear = galsim.Shear(g1=0.1, g2=-0.1) >>> sheared_pos = pos.shear(shear) Note that this operation will always return a PositionD even if an integer position is being sheared. """ def __init__(self): raise NotImplementedError("Cannot instantiate the base class. " "Use either PositionD or PositionI.") def _parse_args(self, *args, **kwargs): if len(kwargs) == 0: if len(args) == 2: self.x, self.y = args elif len(args) == 0: self.x = self.y = 0 elif len(args) == 1: if isinstance(args[0], (Position, _galsim.PositionD, _galsim.PositionI)): self.x = args[0].x self.y = args[0].y else: try: self.x, self.y = args[0] except (TypeError, ValueError): raise TypeError("Single argument to %s must be either a Position " "or a tuple."%self.__class__) from None else: raise TypeError("%s takes at most 2 arguments (%d given)"%( self.__class__, len(args))) elif len(args) != 0: raise TypeError("%s takes x and y as either named or unnamed arguments (given %s, %s)"%( self.__class__, args, kwargs)) else: try: self.x = kwargs.pop('x') self.y = kwargs.pop('y') except KeyError: raise TypeError( "Keyword arguments x,y are required for %s"%self.__class__) from None if kwargs: raise TypeError("Got unexpected keyword arguments %s"%kwargs.keys()) def __mul__(self, other): self._check_scalar(other, 'multiply') return self.__class__(self.x * other, self.y * other) def __rmul__(self, other): return self.__mul__(other) def __div__(self, other): self._check_scalar(other, 'divide') return self.__class__(self.x / other, self.y / other) __truediv__ = __div__ def __neg__(self): return self.__class__(-self.x, -self.y) def __add__(self, other): if isinstance(other, Bounds): return other + self elif not isinstance(other,Position): raise TypeError("Can only add a Position to a %s"%self.__class__.__name__) elif isinstance(other, self.__class__): return self.__class__(self.x + other.x, self.y + other.y) else: return PositionD(self.x + other.x, self.y + other.y) def __sub__(self, other): if not isinstance(other,Position): raise TypeError("Can only subtract a Position from a %s"%self.__class__.__name__) elif isinstance(other, self.__class__): return self.__class__(self.x - other.x, self.y - other.y) else: return _PositionD(self.x - other.x, self.y - other.y) def __repr__(self): return "galsim.%s(x=%r, y=%r)"%(self.__class__.__name__, self.x, self.y) def __str__(self): return "galsim.%s(%s,%s)"%(self.__class__.__name__, self.x, self.y) def __eq__(self, other): return (self is other or (isinstance(other, self.__class__) and self.x == other.x and self.y == other.y)) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.__class__.__name__, self.x, self.y))
[docs] def shear(self, shear): """Shear the position. See the doc string of `galsim.Shear.getMatrix` for more details. Parameters: shear: a `galsim.Shear` instance Returns: a `galsim.PositionD` instance. """ shear_mat = shear.getMatrix() return PositionD( self.x * shear_mat[0, 0] + self.y * shear_mat[0, 1], self.x * shear_mat[1, 0] + self.y * shear_mat[1, 1], )
[docs]class PositionD(Position): """A Position that takes floating point values. See the Position doc string for more details. """ def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) self.x = float(self.x) self.y = float(self.y) @property def _p(self): return _galsim.PositionD(self.x, self.y)
[docs] def round(self): """Return the rounded-off PositionI version of this position. """ return _PositionI(int(round(self.x)), int(round(self.y)))
def _check_scalar(self, other, op): try: if other == float(other): return except (TypeError, ValueError): pass raise TypeError("Can only %s a PositionD by float values"%op)
[docs]class PositionI(Position): """A Position that takes only integer values. Typically used for coordinate positions on an image. See the Position doc string for more details. """ def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) if self.x != int(self.x) or self.y != int(self.y): raise TypeError("PositionI must be initialized with integer values") self.x = int(self.x) self.y = int(self.y) @property def _p(self): return _galsim.PositionI(self.x, self.y) def round(self): # Just for consistency between PositionD and PositionI return self def _check_scalar(self, other, op): try: if other == int(other): return except (TypeError, ValueError): pass raise TypeError("Can only %s a PositionI by integer values"%op)
def _PositionD(x, y): """Equivalent to `PositionD` constructor, but skips some sanity checks and argument parsing. This requires that x,y are floats. """ ret = PositionD.__new__(PositionD) ret.x = float(x) ret.y = float(y) return ret def _PositionI(x, y): """Equivalent to `PositionI` constructor, but skips some sanity checks and argument parsing. This requires that x,y are ints. """ ret = PositionI.__new__(PositionI) ret.x = int(x) ret.y = int(y) return ret
[docs]def parse_pos_args(args, kwargs, name1, name2, integer=False, others=[]): """Parse the args and kwargs of a function call to be some kind of position. We allow four options: f(x,y) f(galsim.PositionD(x,y)) or f(galsim.PositionI(x,y)) f( (x,y) ) (or any indexable thing) f(name1=x, name2=y) If the inputs must be integers, set ``integer=True``. If there are other args/kwargs to parse after these, then their names should be be given as the parameter ``others``, which are passed back in a tuple after the position. Parameters: args: The args of the original function. kwargs: The kwargs of the original function. name1: The allowed kwarg for the first coordinate. name2: The allowed kwarg for the second coordinate. integer: Whether to return a `PositionI` rather than a `PositionD`. [default: False] others: If given, other values to also parse and return from the kwargs. [default: []] Returns: a `Position` instance, possibly also with other values if ``others`` is given. """ def canindex(arg): try: arg[0], arg[1] except (TypeError, IndexError): return False else: return True other_vals = [] if len(args) == 0: # Then name1,name2 need to be kwargs try: x = kwargs.pop(name1) y = kwargs.pop(name2) except KeyError: raise TypeError( 'Expecting kwargs %s, %s. Got %s'%(name1, name2, kwargs.keys())) from None elif ( ( isinstance(args[0], PositionI) or (not integer and isinstance(args[0], PositionD)) ) and len(args) <= 1+len(others) ): x = args[0].x y = args[0].y for arg in args[1:]: other_vals.append(arg) others.pop(0) elif canindex(args[0]) and len(args) <= 1+len(others): x = args[0][0] y = args[0][1] for arg in args[1:]: other_vals.append(arg) others.pop(0) elif len(args) == 1: if integer: raise TypeError("Cannot parse argument %s as a PositionI"%(args[0])) else: raise TypeError("Cannot parse argument %s as a PositionD"%(args[0])) elif len(args) <= 2 + len(others): x = args[0] y = args[1] for arg in args[2:]: other_vals.append(arg) others.pop(0) else: raise TypeError("Too many arguments supplied") # Read any remaining other kwargs if others: for name in others: val = kwargs.pop(name) other_vals.append(val) if kwargs: raise TypeError("Received unexpected keyword arguments: %s",kwargs) if integer: pos = _PositionI(int(x),int(y)) else: pos = _PositionD(float(x),float(y)) if other_vals: return (pos,) + tuple(other_vals) else: return pos
# Put this at the bottom to avoid circular import error from .bounds import Bounds