Beispiel #1
0
    def size(self, size):
        if not positive_real_valued(size):
            raise ValueError('argument "size" must be a positive int')
        else:
            self.__size = int(size)
            self.dvh = DVH(self.size)

            # default to uniformly weighted voxels
            self.voxel_weights = np.ones(self.size)
Beispiel #2
0
	def size(self, size):
		if not positive_real_valued(size):
			raise ValueError('argument "size" must be a positive int')
		else:
			self.__size = int(size)
			self.dvh = DVH(self.size)

			# default to uniformly weighted voxels
			self.voxel_weights = ones(self.size)
Beispiel #3
0
class Structure(object):
    """
	:class:`Structure` manages the dose information (including the dose
	influence matrix, dose calculations and dose volume histogram), as
	well as optimization objective information---including dose
	constraints---for a set of voxels (volume elements) in the patient
	volume to be treated as a logically homogeneous unit with respect to
	the optimization process.

	There are usually three types of structures:
		- Anatomical structures, such as a kidney or the spinal
			cord, termed organs-at-risk (OARs),
		- Clinically delineated structures, such as a tumor or a target
			volume, and,
		- Tissues grouped together by virtue of not being explicitly
			delineated by a clinician, typically lumped together under
			the catch-all category "body".

	Healthy tissue structures, including OARs and "body", are treated as
	non-target, are prescribed zero dose, and only subject to an
	overdose penalty during optimization.

	Target tissue structures are prescribed a non-zero dose, and subject
	to both an underdose and an overdose penalty.

	Attributes:
		label: (:obj:`int` or :obj:`str`): Label, applied to each voxel
			in the structure, usually generated during CT contouring
			step in the clinical workflow for treatment planning.
		name (:obj:`str`): Clinical or anatomical name.
		is_target (:obj:`bool`): ``True`` if structure is a target.
		dvh (:class:`DVH`): Dose volume histogram.
		constraints (:class:`ConstraintList`): Mutable collection of
			dose constraints to be applied to structure during
			optimization.

		 """
    def __init__(self, label, name, is_target, size=None, **options):
        """
		Initialize target/non-target :class:`Structure` with label and name.

		Arguments:
			label (:obj:`int` or :obj:`str`): Structure label.
			name: Name of structure (e.g., 'PTV', 'body', or
				'spinal cord').
			is_target (:obj:`bool`): ``True`` if structure is intended
				to receive a non-zero dose level during treatment.
			size (:obj:`int`, optional): Number of voxels (volume
				elements) in structure.
			**options: Arbitrary keyword arguments.

		Raises:
			TypeError: If ``label`` is not an :obj:`int` or :obj:`str`.
		"""
        # basic information
        if not isinstance(label, (int, str)):
            raise TypeError('argument "label" must be of type {} or {}'
                            ''.format(int, str))
        self.label = label
        self.name = str(name)
        self.is_target = bool(is_target)
        self.__size = None
        self.__weighted_size = None
        self.__objective = None
        self.__dose = 0. * Gy
        self.__boost = 1.
        self.__A_full = None
        self.__A_mean = None
        self.__voxel_weights = None
        self.__y = None
        self.__y_mean = np.nan
        self.dvh = None
        self.constraints = ConstraintList()

        objective = options.pop('objective', None)
        if objective is not None:
            self.objective = objective
        else:
            objective_constructor = options.pop(
                'objective_constructor',
                TargetObjectivePWL if is_target else NontargetObjectiveLinear)
            self.objective = objective_constructor(**options)

        if size is not None:
            self.size = size

        if is_target:
            self.dose = self.objective.dose

        self.A_full = options.pop('A', None)
        self.A_mean = options.pop('A_mean', None)

    @property
    def plannable(self):
        """
		True if structure's attached data is sufficient for optimization.

		Minimum requirements:
			- Structure size determined, and
			- Dose matrix assigned, *or*
			- Structure collapsable and mean dose matrix assigned.
		"""
        size_determined = positive_real_valued(self.size)
        full_mat_usable = sparse_or_dense(self.A_full)
        if full_mat_usable:
            full_mat_usable &= self.size == self.A_full.shape[0]

        collapsed_mat_usable = bool(
            isinstance(self.A_mean, np.ndarray) and self.collapsable)

        usable_matrix_loaded = full_mat_usable or collapsed_mat_usable
        return size_determined and usable_matrix_loaded

    @property
    def size(self):
        """
		Structure size (i.e., number of voxels in structure).

		Raises:
			ValueError: If ``size`` not an :obj:`int`.
		"""
        return self.__size

    @size.setter
    def size(self, size):
        if not positive_real_valued(size):
            raise ValueError('argument "size" must be a positive int')
        else:
            self.__size = int(size)
            self.dvh = DVH(self.size)

            # default to uniformly weighted voxels
            self.voxel_weights = np.ones(self.size)

    @property
    def weighted_size(self):
        if self.voxel_weights is None:
            return self.size
        return self.__weighted_size

    @property
    def objective(self):
        return self.__objective

    @objective.setter
    def objective(self, objective):
        if not isinstance(objective, TreatmentObjective):
            raise TypeError(
                'objective must be of type {}'.format(TreatmentObjective))
        compatible = self.is_target and objective.is_target_objective
        compatible |= self.is_target < objective.is_nontarget_objective
        if not compatible:
            raise ValueError('objective incompatible with structure:\n'
                             'structure is target? {}\n'
                             'objective target-compatible? {}\n'
                             'objective nontarget-compatible? {}'
                             ''.format(self.is_target,
                                       objective.is_target_objective,
                                       objective.is_nontarget_objective))
        self.__objective = objective

    def reset_matrices(self):
        """ Reset structure's dose and mean dose matrices to ``None`` """
        self.__A_full = None
        self.__A_mean = None

    @property
    def collapsable(self):
        """ ``True`` if optimization can be performed with mean dose only. """
        return self.constraints.mean_only and isinstance(
            self.objective, NontargetObjectiveLinear)

    @property
    def A_full(self):
        """
		Full dose matrix (dimensions = voxels x beams).

		Setter method will perform two additional tasks:
			- If :attr:`Structure.size` is not set, set it based on
				number of rows in ``A_full``.
			- Trigger :attr:`Structure.A_mean` to be calculated from
				:attr:`Structure.A_full`.

		Raises:
			TypeError: If ``A_full`` is not a matrix in
				:class:`np.ndarray`, :class:`sp.csc_matrix`, or
				:class:`sp.csr_matrix` formats.
			ValueError: If :attr:`Structure.size` is set, and the number
				of rows in ``A_full`` does not match
				:attr:`Structure.size`.
		"""
        return self.__A_full

    @A_full.setter
    def A_full(self, A_full):
        if A_full is None:
            return

        # verify type of A_full
        if not sparse_or_dense(A_full):
            raise TypeError('input A must by a numpy or scipy csr/csc '
                            'sparse matrix')

        if self.size is not None:
            if A_full.shape[0] != self.size:
                raise ValueError('# rows of "A_full" must correspond to '
                                 'value  of property size ({}) of {} '
                                 'object'.format(self.size, Structure))
        else:
            self.size = A_full.shape[0]

        self.__A_full = A_full

        # Pass "None" to self.A_mean setter to trigger calculation of
        # mean dose matrix from full dose matrix.
        self.A_mean = None

    @property
    def A_mean(self):
        """
		Mean dose matrix (dimensions = ``1`` x ``beams``).

		Setter expects a one dimensional :class:`np.ndarray`
		representing the mean dose matrix for the structure. If this
		optional argument is not provided, the method will attempt to
		calculate the mean dose from :attr:`Structure.A_full`.

		Raises:
			TypeError: If ``A_mean`` provided and not of type
				:class:`np.ndarray`, *or* if mean dose matrix is to be
				calculated from :attr:`Structure.A_full`, but full dose
				matrix is not a :mod:`conrad`-recognized matrix type.
			ValueError: If ``A_mean`` is not dimensioned as a row or
				column vector, or number of beams implied by ``A_mean``
				conflicts with number of beams implied by
				:attr:`Structure.A_full`.
		 """
        return self.__A_mean

    @A_mean.setter
    def A_mean(self, A_mean=None):
        if A_mean is not None:
            if not isinstance(A_mean, np.ndarray):
                raise TypeError('if argument "A_mean" is provided, it must be '
                                'of type {}'.format(np.ndarray))
            elif not A_mean.size in A_mean.shape:
                raise ValueError(
                    'if argument "A_mean" is provided, it must be '
                    'a row or column vector. shape of argument: {}'
                    ''.format(A_mean.shape))
            else:
                if self.__A_full is not None:
                    if len(A_mean) != self.__A_full.shape[1]:
                        raise ValueError(
                            'field "A_full" already set; proposed '
                            'value for "A_mean" must have same '
                            'number of entries ({}) as columns in '
                            'A_full ({})'.format(len(A_mean),
                                                 self.__A_full.shape[1]))
            self.__A_mean = vec(A_mean)
        elif self.__A_full is not None:
            if not sparse_or_dense(self.A_full):
                raise TypeError('cannot calculate structure.A_mean from'
                                'structure.A_full: A_full must be one of '
                                '({},{},{})'.format(np.ndarray, sp.csc_matrix,
                                                    sp.csr_matrix))
            else:
                if isinstance(self.A_full, np.ndarray):
                    self.__A_mean = np.dot(self.voxel_weights, self.A_full)
                else:
                    self.__A_mean = vec(self.voxel_weights * self.A_full)
                self.__A_mean /= float(self.weighted_size)

    @property
    def A(self):
        """ Alias for :attr:`Structure.A_full`. """
        return self.__A_full

    @property
    def voxel_weights(self):
        """
		Voxel weights, or relative volumes of voxels.

		The voxel weights are the ``1`` vector if the structure volume
		is regularly discretized, and some other set of integer values
		if voxels are clustered.

		Raises:
			ValueError: If :attr:`Structure.voxel_weights` setter called
				before :attr:`Structure.size` is defined, or if length
				of input does not match :attr:`Structure.size`, or if
				any of the provided weights are negative.
		"""
        return self.__voxel_weights

    @voxel_weights.setter
    def voxel_weights(self, weights):
        if self.size in (None, np.nan, 0):
            raise ValueError('structure size must be defined to add voxel '
                             'weights')
        if len(weights) != self.size and np.sum(weights) != self.size:
            raise ValueError('either the length ({}) or sum ({}) of input '
                             '`weights` does not match structure size ({}) '
                             'of this {} object'.format(
                                 len(weights), np.sum(weights), self.size,
                                 Structure))
        if any(weights < 0):
            raise ValueError('negative voxel weights not allowed')
        self.__voxel_weights = vec(weights)
        self.__weighted_size = np.sum(self.__voxel_weights)
        self.objective.normalization = 1. / self.weighted_size
        if self.weighted_size != self.size and self.A_full is not None:
            # Pass "None" to self.A_mean setter to trigger calculation of
            # mean dose matrix from full dose matrix.
            self.A_mean = None
        if self.y is not None:
            self.__y_mean = np.dot(self.voxel_weights,
                                   self.y) / self.weighted_size

    def set_constraint(self, constr_id, threshold=None, relop=None, dose=None):
        """
		Modify threshold, relop, and dose of an existing constraint.

		Arguments:
			constr_id (:obj:`str`): Key to a constraint in
				:attr:`Structure.constraints`.
			threshold (optional): Percentile threshold to assign if
				queried constraint is of type
				:class:`PercentileConstraint`, no-op otherwise. Must be
				compatible with the setter method for
				:attr:`PercentileConstraint.percentile`.
			relop (optional): Inequality constraint sense. Must be
				compatible with the setter method for
				:attr:`Constraint.relop`.
			dose (optional): Constraint dose. Must be compatible with
				setter method for :attr:`Constraint.dose`.

		Returns:
			None

		Raises:
			ValueError: If ``constr_id`` is not the key to a constraint
				in the :class:`Constraintlist` located at
				:attr:`Structure.constraints`.
		"""
        if constr_id in self.constraints.items:
            if isinstance(self.constraints[constr_id], PercentileConstraint) \
              and threshold is not None:
                self.constraints[constr_id].percentile = threshold
            if relop is not None:
                self.constraints[constr_id].relop = relop
            if dose is not None:
                self.constraints[constr_id].dose = dose
        else:
            raise ValueError('contraint with ID {} not found in constraints '
                             'attached to this {}'.format(
                                 constr_id, Structure))

    @property
    def dose(self):
        """
		Dose level targeted in structure's optimization objective.

		The dose has two components: the precribed dose,
		:attr:`Structure.dose_rx`, and a multiplicative adjustment
		factor, :attr:`Structure.boost`.

		Once the structure's dose has been initialized, setting
		:attr:`Structure.dose` will change the adjustment factor. This
		is to distinguish (and allow for differences) between the dose
		level prescribed to a structure by a clinician and the dose
		level request to a numerical optimization algorithm that yields
		a desirable distribution, since the latter may require some
		offset relative to the former. To change the reference dose
		level, use the :attr:`Structure.dose_rx` setter.

		Setter is no-op for non-target structures, since zero dose is
		prescribed always.

		Raises:
			TypeError: If requested dose does not have units of
				:class:`DeliveredDose`.
			ValueError: If zero dose is requested to a target structure.
		"""
        if hasattr(self.objective, 'dose'):
            return self.objective.dose
        else:
            return self.__dose

    @dose.setter
    def dose(self, dose):
        if not self.is_target:
            return
        if not isinstance(dose, DeliveredDose):
            raise TypeError('argument "dose" must be of type {}'
                            ''.format(DeliveredDose))
        if dose.value == 0:
            raise ValueError('zero dose invalid for target structure')
        if self.__dose.value == 0:
            self.__boost = 1.
            self.__dose = dose
        else:
            self.__boost = dose.to_Gy.value / self.__dose.to_Gy.value
        self.objective.dose = self.boost * self.__dose

    @property
    def boost(self):
        """
		Adjustment factor from precription dose to optimization dose.
		"""
        return self.__boost

    @property
    def dose_rx(self):
        """
		Prescribed dose level.

		Setting this field sets :attr:`Structure.dose` to the requested
		value and :attr:`Structure.boost` to ``1``.
		"""
        return self.__dose

    @dose_rx.setter
    def dose_rx(self, dose):
        self.__dose.value = 0
        self.dose = dose

    @property
    def dose_unit(self):
        """
		One times the :class:`DeliveredDose` unit of the structure dose.
		"""
        u = 1 * self.__dose
        u.value = 1
        return u

    def calculate_dose(self, beam_intensities):
        """ Alias for :meth:`Structure.calc_y`. """
        self.calc_y(beam_intensities)

    def assign_dose(self, y):
        """
		Assign dose vector to structure.

		Arguments:
			y: Vector-like input of voxel doses.

		Returns:
			None

		Raises:
			ValueError: if structure size is known and incompatible with
				length of ``y``.
		"""
        y = vec(y)
        if self.size is None:
            self.size = y.size
        elif self.size != y.size:
            raise ValueError('size of dose vector ({}) incompatible with size '
                             'of structure ({})'.format(y.size, self.size))
        self.__y = y
        self.__y_mean = np.dot(self.voxel_weights, y) / self.weighted_size
        self.dvh.data = self.__y

    def calc_y(self, x):
        """
		Calculate voxel doses as:
		attr:`Structure.y` = :attr:`Structure.A` * ``x``.

		Arguments:
			x: Vector-like input of beam intensities.

		Returns:
			None
		"""

        # calculate dose from input vector x:
        # 	y = Ax
        x = vec(x)
        if isinstance(self.A, (sp.csr_matrix, sp.csc_matrix)):
            self.__y = np.squeeze(self.A * x)
        elif isinstance(self.A, np.ndarray):
            self.__y = self.A.dot(x)

        self.__y_mean = self.A_mean.dot(x)
        if isinstance(self.__y_mean, np.ndarray):
            self.__y_mean = self.__y_mean[0]

        # make DVH curve from calculated dose
        if self.y is not None:
            self.dvh.data = self.y

    @property
    def y(self):
        """ Vector of structure's voxel doses. """
        return self.__y

    @property
    def y_mean(self):
        """ Value of structure's mean voxel dose. """
        return self.__y_mean

    @property
    def mean_dose(self):
        """ Average dose to structure's voxels. """
        return self.__y_mean * self.dose_unit

    @property
    def min_dose(self):
        """ Minimum dose to structure's voxels. """
        if self.dvh is None:
            return np.nan * Gy
        return self.dvh.min_dose * self.dose_unit

    @property
    def max_dose(self):
        """ Maximum dose to structure's voxels. """
        if self.dvh is None:
            return np.nan * Gy
        return self.dvh.max_dose * self.dose_unit

    def satisfies(self, constraint):
        """
		Test whether structure's voxel doses satisfy ``constraint``.

		Arguments:
			constraint (:class:`Constraint`): Dose constraint to test
				against structure's voxel doses.

		Returns:
			:obj:`bool`: ``True`` if structure's voxel doses conform to
			the queried	constraint.

		Raises:
			TypeError: If ``constraint`` not of type :class:`Constraint`.
			ValueError: If :attr:`Structure.dvh` not initialized or not
				populated with dose data.
		"""
        if not isinstance(constraint, Constraint):
            raise TypeError('argument "constraint" must be of type '
                            'conrad.dose.Constraint')

        if self.dvh is None and constraint.threshold != 'mean':
            raise ValueError('structure DVH does not exist, cannot evaluate '
                             'constraint satisfaction.\n(assign structure '
                             'size explicitly by setting field "{}.size"\nor '
                             'impicitly by assigning a dose matrix with '
                             'field "{}.A_full"\nto trigger DVH instantiation)'
                             ''.format(Structure, Structure))
        if not self.dvh.populated and constraint.threshold != 'mean':
            raise ValueError('structure DVH not populated by dose data, '
                             'cannot evaluate constraint satisfaction\n'
                             '(assign dose by setting field "{}.y")'
                             ''.format(Structure))

        relop = operator.le if constraint.relop == RELOPS.LEQ else operator.ge

        if isinstance(constraint.threshold, str):
            if constraint.threshold == 'mean':
                dose_achieved = self.mean_dose
            elif constraint.threshold == 'min':
                dose_achieved = self.min_dose
            elif constraint.threshold == 'max':
                dose_achieved = self.max_dose
        else:
            dose_achieved = self.dvh.dose_at_percentile(constraint.threshold)

        status = relop(float(dose_achieved), float(constraint.dose))
        dose = float(dose_achieved) / float(constraint.dose) * constraint.dose
        return (status, dose)

    def satisfies_all(self, constraint_list):
        return all(
            listmap(
                lambda status_dose_tuple: status_dose_tuple[0],
                listmap(self.satisfies,
                        ConstraintList(constraint_list).list)))

    def plotting_data(self, constraints_only=False, maxlength=None):
        """
		Dictionary of :mod:`matplotlib`-compatible plotting data.

		Data includes DVH curve, constraints, and prescribed dose.

		Args:
			constraints_only (:obj:`bool`, optional): If ``True``,
				return only the constraints associated with the
				structure.
			maxlength (:obj:`int`, optional): If specified, re-sample
				the structure's DVH plotting data to have a maximum
				series length of ``maxlength``.
		"""
        if constraints_only:
            return self.constraints.plotting_data
        else:
            return {
                'curve': self.dvh.resample(maxlength).plotting_data,
                'constraints': self.constraints.plotting_data,
                'rx': self.dose_rx.value,
                'target': self.is_target,
                'name': self.name
            }

    @property
    def __header_string(self):
        """ Header string, comprising structure name and label. """
        out = '\nStructure: '
        if self.name != '':
            out += '{}'.format(self.name)
        else:
            out += '<unnamed>'
        out += ' (label = {})\n'.format(self.label)
        return out

    # @property
    # def objective(self):
    # 	""" Dictionary of objective data. """
    # 	return {
    # 			'is_target': self.is_target,
    # 			'dose': self.dose,
    # 			'weight_under': self.__w_under,
    # 			'weight_over': self.__w_over
    # 			}

    @property
    def __obj_string(self):
        """ String of objective data. """
        out = 'target? %s\n' % self.is_target
        out += 'rx dose: %s\n' % self.dose_rx
        out += 'objective:\n%s' % self.objective.string(offset=1)
        out += '\n'
        return out

    @property
    def __constr_string(self):
        """ String of constraints attached to :class:`Structure`. """
        out = ''
        for key in self.constraints.items:
            out += self.constraints[key].__str__()
            out += '\n'
        return out

    def summary(self, percentiles=[2, 25, 75, 98]):
        """
		Dictionary summarizing dose statistics.

		Arguments:
			percentiles (:obj:`list`, optional): Percentile levels at
				which to query the structure dose. If not provided, will
				query doses at default percentile levels of 2%, 25%, 75%
				and 98%.

		Returns:
			:obj:`dict`: Dictionary of doses at requested percentiles,
			plus mean, minimum and maximum voxel doses.
		"""
        s = {}
        s['mean'] = self.mean_dose
        s['min'] = self.min_dose
        s['max'] = self.max_dose
        for p in percentiles:
            s['D' + str(p)] = self.dvh.dose_at_percentile(p) * self.dose_unit
        return s

    @property
    def __summary_string(self):
        """
		String summarizing dose statistics.

		Includes MEAN, MIN, and MAX doses, as well as doses at several
		default percentiles: 98%, 75%, 25%, 2%
		"""
        summary = self.summary()
        hdr = str(6 * '{!s:^10}|' + '{!s:^10}\n').format(
            'mean', 'min', 'max', 'D98', 'D75', 'D25', 'D2')
        vals = str(6 * '{!s:^10}|' + '{!s:^10}\n').format(
            summary['mean'], summary['min'], summary['max'], summary['D98'],
            summary['D75'], summary['D25'], summary['D2'])
        return hdr + vals

    @property
    def objective_string(self):
        """ String of structure header and objectives """
        return self.__header_string + self.__obj_string

    @property
    def constraints_string(self):
        """ String of structure header and constraints """
        return self.__header_string + self.__constr_string

    @property
    def summary_string(self):
        """ String of structure header and dose summary """
        return self.__header_string + self.__summary_string

    def __str__(self):
        """ String of structure header, objectives, and constraints """
        return self.__header_string + self.__obj_string + self.__constr_string
Beispiel #4
0
class Structure(object):
	"""
	:class:`Structure` manages the dose information (including the dose
	influence matrix, dose calculations and dose volume histogram), as
	well as optimization objective information---including dose
	constraints---for a set of voxels (volume elements) in the patient
	volume to be treated as a logically homogeneous unit with respect to
	the optimization process.

	There are usually three types of structures:
		- Anatomical structures, such as a kidney or the spinal
			cord, termed organs-at-risk (OARs),
		- Clinically delineated structures, such as a tumor or a target
			volume, and,
		- Tissues grouped together by virtue of not being explicitly
			delineated by a clinician, typically lumped together under
			the catch-all category "body".

	Healthy tissue structures, including OARs and "body", are treated as
	non-target, are prescribed zero dose, and only subject to an
	overdose penalty during optimization.

	Target tissue structures are prescribed a non-zero dose, and subject
	to both an underdose and an overdose penalty.

	Attributes:
		label: (:obj:`int` or :obj:`str`): Label, applied to each voxel
			in the structure, usually generated during CT contouring
			step in the clinical workflow for treatment planning.
		name (:obj:`str`): Clinical or anatomical name.
		is_target (:obj:`bool`): ``True`` if structure is a target.
		dvh (:class:`DVH`): Dose volume histogram.
		constraints (:class:`ConstraintList`): Mutable collection of
			dose constraints to be applied to structure during
			optimization.

		 """
	def __init__(self, label, name, is_target, size=None, **options):
		"""
		Initialize target/non-target :class:`Structure` with label and name.

		Arguments:
			label (:obj:`int` or :obj:`str`): Structure label.
			name: Name of structure (e.g., 'PTV', 'body', or
				'spinal cord').
			is_target (:obj:`bool`): ``True`` if structure is intended
				to receive a non-zero dose level during treatment.
			size (:obj:`int`, optional): Number of voxels (volume
				elements) in structure.
			**options: Arbitrary keyword arguments.

		Raises:
			TypeError: If ``label`` is not an :obj:`int` or :obj:`str`.
		"""
		# basic information
		if not isinstance(label, (int, str)):
			raise TypeError('argument "label" must be of type {} or {}'
							''.format(int, str))
		self.label = label
		self.name = str(name)
		self.is_target = bool(is_target)
		self.__size = None
		self.__dose = 0. * Gy
		self.__boost = 1.
		self.__w_under = nan
		self.__w_over = nan
		self.__A_full = None
		self.__A_mean = None
		self.__voxel_weights = None
		self.__y = None
		self.__y_mean = nan
		self.dvh = None
		self.constraints = ConstraintList()

		if size is not None:
			self.size = size
		if is_target:
			self.dose = options.pop('dose', 1. * Gy)
		self.A_full = options.pop('A', None)
		self.A_mean = options.pop('A_mean', None)

		WU_DEFAULT = W_UNDER_DEFAULT if is_target else 0.
		WO_DEFAULT = W_OVER_DEFAULT if is_target else W_NONTARG_DEFAULT

		self.w_under = options.pop('w_under', WU_DEFAULT)
		self.w_over = options.pop('w_over', WO_DEFAULT)

	@property
	def plannable(self):
		"""
		True if structure's attached data is sufficient for optimization.

		Minimum requirements:
			- Structure size determined, and
			- Dose matrix assigned, *or*
			- Structure collapsable and mean dose matrix assigned.
		"""
		size_set = positive_real_valued(self.size)
		full_mat_usable = sparse_or_dense(self.A_full)
		if full_mat_usable:
			full_mat_usable &= self.size == self.A_full.shape[0]

		collapsed_mat_usable = bool(
				isinstance(self.A_mean, ndarray) and self.collapsable)

		usable_matrix_loaded = full_mat_usable or collapsed_mat_usable
		return size_set and usable_matrix_loaded

	@property
	def size(self):
		"""
		Structure size (i.e., number of voxels in structure).

		Raises:
			ValueError: If ``size`` not an :obj:`int`.
		"""
		return self.__size

	@size.setter
	def size(self, size):
		if not positive_real_valued(size):
			raise ValueError('argument "size" must be a positive int')
		else:
			self.__size = int(size)
			self.dvh = DVH(self.size)

			# default to uniformly weighted voxels
			self.voxel_weights = ones(self.size)

	def reset_matrices(self):
		""" Reset structure's dose and mean dose matrices to ``None`` """
		self.__A_full = None
		self.__A_mean = None

	@property
	def collapsable(self):
		""" ``True`` if optimization can be performed with mean dose only. """
		return self.constraints.mean_only and not self.is_target

	@property
	def A_full(self):
		"""
		Full dose matrix (dimensions = voxels x beams).

		Setter method will perform two additional tasks:
			- If :attr:`Structure.size` is not set, set it based on
				number of rows in ``A_full``.
			- Trigger :attr:`Structure.A_mean` to be calculated from
				:attr:`Structure.A_full`.

		Raises:
			TypeError: If ``A_full`` is not a matrix in
				:class:`ndarray`, :class:`csc_matrix`, or
				:class:`csr_matrix` formats.
			ValueError: If :attr:`Structure.size` is set, and the number
				of rows in ``A_full`` does not match
				:attr:`Structure.size`.
		"""
		return self.__A_full

	@A_full.setter
	def A_full(self, A_full):
		if A_full is None:
			return

		# verify type of A_full
		if not sparse_or_dense(A_full):
			raise TypeError('input A must by a numpy or scipy csr/csc '
							'sparse matrix')

		if self.size is not None:
			if A_full.shape[0] != self.size:
				raise ValueError('# rows of "A_full" must correspond to '
								 'value  of property size ({}) of {} '
								 'object'.format(self.size, Structure))
		else:
			self.size = A_full.shape[0]

		self.__A_full = A_full

		# Pass "None" to self.A_mean setter to trigger calculation of
		# mean dose matrix from full dose matrix.
		self.A_mean = None

	@property
	def A_mean(self):
		"""
		Mean dose matrix (dimensions = ``1`` x ``beams``).

		Setter expects a one dimensional :class:`ndarray` representing
		the mean dose matrix for the structure. If this optional
		argument is not provided, the method will attempt to calculate
		the mean dose from :attr:`Structure.A_full`.

		Raises:
			TypeError: If ``A_mean`` provided and not of type
				:class:`ndarray`, *or* if mean dose matrix is to be
				calculated from :attr:`Structure.A_full`, but full dose
				matrix is not a :mod:`conrad`-recognized matrix type.
			ValueError: If ``A_mean`` is not dimensioned as a row or
				column vector, or number of beams implied by ``A_mean``
				conflicts with number of beams implied by
				:attr:`Structure.A_full`.
		 """
		return self.__A_mean

	@A_mean.setter
	def A_mean(self, A_mean=None):
		if A_mean is not None:
			if not isinstance(A_mean, ndarray):
				raise TypeError('if argument "A_mean" is provided, it '
								'must be of type {}'.format(ndarray))
			elif not A_mean.size in A_mean.shape:
				raise ValueError('if argument "A_mean" is provided, it must be'
								 ' a row or column vector. shape of argument: '
								 '{}'.format(A_mean.shape))
			else:
				if self.__A_full is not None:
					if len(A_mean) != self.__A_full.shape[1]:
						raise ValueError('field "A_full" already set; '
										 'proposed value for "A_mean" '
										 'must have same number of entries '
										 '({}) as columns in A_full ({})'
										 ''.format(len(A_mean),
										 self.__A_full.shape[1]))
				self.__A_mean = vec(A_mean)
		elif self.__A_full is not None:
			if not sparse_or_dense(self.A_full):
				raise TypeError('cannot calculate structure.A_mean from'
								'structure.A_full: A_full must be one of'
								' ({},{},{})'.format(ndarray, csc_matrix,
								csr_matrix))
			else:
				self.__A_mean = self.A_full.sum(0) / self.A_full.shape[0]
				if not isinstance(self.A_full, ndarray):
					self.__A_mean = vec(self.__A_mean)

	@property
	def A(self):
		""" Alias for :attr:`Structure.A_full`. """
		return self.__A_full

	@property
	def voxel_weights(self):
		"""
		Voxel weights, or relative volumes of voxels.

		The voxel weights are the ``1`` vector if the structure volume
		is regularly discretized, and some other set of integer values
		if voxels are clustered.

		Raises:
			ValueError: If :attr:`Structure.voxel_weights` setter called
				before :attr:`Structure.size` is defined, or if length
				of input does not match :attr:`Structure.size`, or if
				any of the provided weights are negative.
		"""
		return self.__voxel_weights

	@voxel_weights.setter
	def voxel_weights(self, weights):
		if self.size in (None, nan, 0):
			raise ValueError('structure size must be defined to add '
							 'voxel weights')
		if len(weights) != self.size:
			raise ValueError('length of input "weights" ({}) does not '
							 'match structure size ({}) of this {} '
							 'object'
							 ''.format(len(weights), self.size, Structure))
		if any(weights < 0):
			raise ValueError('negative voxel weights not allowed')
		self.__voxel_weights = vec(weights)

	def set_constraint(self, constr_id, threshold=None, relop=None, dose=None):
		"""
		Modify threshold, relop, and dose of an existing constraint.

		Arguments:
			constr_id (:obj:`str`): Key to a constraint in
				:attr:`Structure.constraints`.
			threshold (optional): Percentile threshold to assign if
				queried constraint is of type
				:class:`PercentileConstraint`, no-op otherwise. Must be
				compatible with the setter method for
				:attr:`PercentileConstraint.percentile`.
			relop (optional): Inequality constraint sense. Must be
				compatible with the setter method for
				:attr:`Constraint.relop`.
			dose (optional): Constraint dose. Must be compatible with
				setter method for :attr:`Constraint.dose`.

		Returns:
			None

		Raises:
			ValueError: If ``constr_id`` is not the key to a constraint
				in the :class:`Constraintlist` located at
				:attr:`Structure.constraints`.
		"""
		if constr_id in self.constraints.items:
			if isinstance(self.constraints[constr_id], PercentileConstraint) \
					and threshold is not None:
				self.constraints[constr_id].percentile = threshold
			if relop is not None:
				self.constraints[constr_id].relop = relop
			if dose is not None:
				self.constraints[constr_id].dose = dose
		else:
			raise ValueError('contraint with ID {} not found in constraints '
							 'attached to this {}'.format(constr_id,
							 Structure))

	@property
	def dose(self):
		"""
		Dose level targeted in structure's optimization objective.

		The dose has two components: the precribed dose,
		:attr:`Structure.dose_rx`, and a multiplicative adjustment
		factor, :attr:`Structure.boost`.

		Once the structure's dose has been initialized, setting
		:attr:`Structure.dose` will change the adjustment factor. This
		is to distinguish (and allow for differences) between the dose
		level prescribed to a structure by a clinician and the dose
		level request to a numerical optimization algorithm that yields
		a desirable distribution, since the latter may require some
		offset relative to the former. To change the reference dose
		level, use the :attr:`Structure.dose_rx` setter.

		Setter is no-op for non-target structures, since zero dose is
		prescribed always.

		Raises:
			TypeError: If requested dose does not have units of
				:class:`DeliveredDose`.
			ValueError: If zero dose is requested to a target structure.
		"""
		return self.boost * self.__dose

	@dose.setter
	def dose(self, dose):
		if not self.is_target: return
		if not isinstance(dose, DeliveredDose):
			raise TypeError('argument "dose" must be of type {}'
							''.format(DeliveredDose))
		if dose.value == 0:
			raise ValueError('zero dose invalid for target structure')
		if self.__dose.value == 0:
			self.__dose = dose
			self.__boost = 1.
		else:
			self.__boost = dose.to_Gy.value / self.__dose.to_Gy.value

	@property
	def boost(self):
		"""
		Adjustment factor from precription dose to optimization dose.
		"""
		return self.__boost

	@property
	def dose_rx(self):
		"""
		Prescribed dose level.

		Setting this field sets :attr:`Structure.dose` to the requested
		value and :attr:`Structure.boost` to ``1``.
		"""
		return self.__dose

	@dose_rx.setter
	def dose_rx(self, dose):
		self.__dose.value = 0
		self.dose = dose

	@property
	def dose_unit(self):
		"""
		One times the :class:`DeliveredDose` unit of the structure dose.
		"""
		u = 1 * self.__dose
		u.value = 1
		return u

	@property
	def w_under(self):
		"""
		Underdose weight for structure's optimization objective.

		Getter returns weight normalized by structure size. Setter sets
		raw weight.

		Raises:
			TypeError: If ``w_under`` not :obj:`int` or :obj:`float`.
			ValueError: If ``w_under`` negative.
		"""
		if not positive_real_valued(self.size):
			return nan

		if isinstance(self.__w_under, (float, int)):
		    return self.__w_under / float(self.size)
		else:
			return None

	@w_under.setter
	def w_under(self, weight):
		if not self.is_target:
			self.__w_under = 0.
			return

		if isinstance(weight, (int, float)):
			self.__w_under = max(0., float(weight))
			if weight < 0:
				raise ValueError('negative objective weights not allowed')
		else:
			raise TypeError('argument "weight" must be a float >= 0')

	@property
	def w_under_raw(self):
		""" Raw underdose weight, not normalized for structure size. """
		return self.__w_under

	@property
	def w_over(self):
		"""
		Overdose weight for structure's optimization objective.

		Getter returns weight normalized by structure size. Setter sets
		raw weight.

		Raises:
			TypeError: If ``w_over`` not :obj:`int` or :obj:`float`.
			ValueError: If ``w_over`` negative.
		"""
		if not positive_real_valued(self.size):
			return nan

		if isinstance(self.__w_over, (float, int)):
			return self.__w_over / float(self.size)
		else:
			return None

	@w_over.setter
	def w_over(self, weight):
		if isinstance(weight, (int, float)):
			self.__w_over = max(0., float(weight))
			if weight < 0:
				raise ValueError('negative objective weights not allowed')
		else:
			raise TypeError('argument "weight" must be a float >= 0')

	@property
	def w_over_raw(self):
		""" Raw overdose weight, not normalized for structure size. """
		return self.__w_over

	def calculate_dose(self, beam_intensities):
		""" Alias for :meth:`Structure.calc_y`. """
		self.calc_y(beam_intensities)

	def calc_y(self, x):
		"""
		Calculate voxel doses as :attr:`Structure.y` = :attr:`Structure.A` * ``x``.

		Arguments:
			x: Vector-like input of beam intensities.

		Returns:
			None
		"""

		# calculate dose from input vector x:
		# 	y = Ax
		x = vec(x)
		if isinstance(self.A, (csr_matrix, csc_matrix)):
			self.__y = squeeze(self.A * x)
		elif isinstance(self.A, ndarray):
			self.__y = self.A.dot(x)

		self.__y_mean = self.A_mean.dot(x)
		if isinstance(self.__y_mean, ndarray):
			self.__y_mean = self.__y_mean[0]

		# make DVH curve from calculated dose
		self.dvh.data = self.__y

	@property
	def y(self):
		""" Vector of structure's voxel doses. """
		return self.__y

	@property
	def mean_dose(self):
		""" Average dose to structure's voxels. """
		return self.__y_mean * self.dose_unit

	@property
	def min_dose(self):
		""" Minimum dose to structure's voxels. """
		if self.dvh is None:
			return nan * Gy
		return self.dvh.min_dose * self.dose_unit

	@property
	def max_dose(self):
		""" Maximum dose to structure's voxels. """
		if self.dvh is None:
			return nan * Gy
		return self.dvh.max_dose * self.dose_unit

	def satisfies(self, constraint):
		"""
		Test whether structure's voxel doses satisfy ``constraint``.

		Arguments:
			constraint (:class:`Constraint`): Dose constraint to test
				against structure's voxel doses.

		Returns:
			:obj:`bool`: ``True`` if structure's voxel doses conform to
			the queried	constraint.

		Raises:
			TypeError: If ``constraint`` not of type :class:`Constraint`.
			ValueError: If :attr:`Structure.dvh` not initialized or not
				populated with dose data.
		"""
		if self.dvh is None:
			raise ValueError('structure DVH does not exist, cannot evaluate '
							 'constraint satisfaction.\n(assign structure '
							 'size explicitly by setting field "{}.size"\nor '
							 'impicitly by assigning a dose matrix with '
							 'field "{}.A_full"\nto trigger DVH instantiation)'
							 ''.format(Structure, Structure))
		if not self.dvh.populated:
			raise ValueError('structure DVH not populated by dose data, '
							 'cannot evaluate constraint satisfaction\n'
							 '(assign dose by setting field "{}.y")'
							 ''.format(Structure))

		if not isinstance(constraint, Constraint):
			raise TypeError('argument "constraint" must be of type '
				'conrad.dose.Constraint')

		dose = constraint.dose.value
		relop = constraint.relop

		if isinstance(constraint.threshold, str):
			if constraint.threshold == 'mean':
				dose_achieved = self.mean_dose
			elif constraint.threshold == 'min':
				dose_achieved = self.min_dose
			elif constraint.threshold == 'max':
				dose_achieved = self.max_dose
		else:
			dose_achieved = self.dvh.dose_at_percentile(
				constraint.threshold)

		if relop == RELOPS.LEQ:
			status = dose_achieved <= dose
		elif relop == RELOPS.GEQ:
			status = dose_achieved >= dose

		return (status, dose_achieved)

	@property
	def plotting_data(self):
		"""
		Dictionary of :mod:`matplotlib`-compatible plotting data.

		Data includes DVH curve, constraints, and prescribed dose.
	 	"""
		return {'curve': self.dvh.plotting_data,
				'constraints': self.constraints.plotting_data,
				'rx': self.dose_rx.value,
				'target': self.is_target}

	@property
	def __header_string(self):
		""" Header string, comprising structure name and label. """
		out = '\nStructure: '
		if self.name != '':
			out += '{}'.format(self.name)
		else:
			out += '<unnamed>'
		out += ' (label = {})\n'.format(self.label)
		return out

	@property
	def objective(self):
		""" Dictionary of objective data. """
		return {
				'is_target': self.is_target,
				'dose': self.dose,
				'weight_under': self.__w_under,
				'weight_over': self.__w_over
				}

	@property
	def __obj_string(self):
		""" String of objective data. """
		out = 'target? {}\n'.format(self.is_target)
		out += 'rx dose: {}\n'.format(self.dose)
		if self.is_target:
			out += 'weight_under: {}\n'.format(self.__w_under)
			out += 'weight_over: {}\n'.format(self.__w_over)
		else:
			out += 'weight: {}\n'.format(self.__w_over)
		out += "\n"
		return out

	@property
	def __constr_string(self):
		""" String of constraints attached to :class:`Structure`. """
		out = ''
		for key in self.constraints.items:
			out += self.constraints[key].__str__()
			out += '\n'
		return out

	def summary(self, percentiles=[2, 25, 75, 98]):
		"""
		Dictionary summarizing dose statistics.

		Arguments:
			percentiles (:obj:`list`, optional): Percentile levels at
				which to query the structure dose. If not provided, will
				query doses at default percentile levels of 2%, 25%, 75%
				and 98%.

		Returns:
			:obj:`dict`: Dictionary of doses at requested percentiles,
			plus mean, minimum and maximum voxel doses.
		"""
		s = {}
		s['mean'] = self.mean_dose
		s['min'] = self.min_dose
		s['max'] = self.max_dose
		for p in percentiles:
			s['D' + str(p)] = self.dvh.dose_at_percentile(p) * self.dose_unit
		return s

	@property
	def __summary_string(self):
		"""
		String summarizing dose statistics.

		Includes MEAN, MIN, and MAX doses, as well as doses at several
		default percentiles: 98%, 75%, 25%, 2%
		"""
		summary = self.summary()
		hdr = str(6 * '{!s:^10}|' + '{!s:^10}\n').format(
						'mean', 'min', 'max', 'D98', 'D75', 'D25', 'D2')
		vals = str(6 * '{!s:^10}|' + '{!s:^10}\n').format(
				summary['mean'], summary['min'], summary['max'],
				summary['D98'], summary['D75'], summary['D25'], summary['D2'])
		return hdr + vals

	@property
	def objective_string(self):
		""" String of structure header and objectives """
		return self.__header_string + self.__obj_string

	@property
	def constraints_string(self):
		""" String of structure header and constraints """
		return self.__header_string + self.__constr_string

	@property
	def summary_string(self):
		""" String of structure header and dose summary """
		return self.__header_string + self.__summary_string

	def __str__(self):
		""" String of structure header, objectives, and constraints """
		return self.__header_string + self.__obj_string + self.__constr_string