Geometry Classes

__init__.py

"""
Package for general geometry classes

Library Version 1.0

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/
"""

from geometry.conversions import convert, conversion_factor
from geometry.conversions import validate_length, validate_area
from geometry.conversions import validate_volume, validate_weight
from geometry.conversions import validate_unit, validate_compatible_units
from geometry.conversions import InvalidUnit, InvalidLengthUnit
from geometry.conversions import InvalidAreaUnit, InvalidVolumeUnit
from geometry.conversions import InvalidWeightUnit, IncompatibleUnits

from geometry.shapes import Rectangle, Square, Circle

from geometry.solids import Cuboid, Cube, Sphere, Cone, Cylinder, Tube

from geometry.materials import DefaultMaterial, Water, MildSteel, Aluminium
from geometry.materials import Copper, Gold, Silver, Iron, Lead, Tin, Zinc

shapes.py

"""
Shapes classes.

Library Version 1.0

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/
"""

# pylint: disable=R0201

from math import pi
from geometry.conversions import validate_length, validate_area, convert


class _Shape():

    """
    Base shape class.
    """

    def __init__(self, units):

        """
        Class initializer.

        Arguments:
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        """

        validate_length(units)
        self._units = units
        self._units_area = "{0}2".format(units)

    def _area_internal_units(self):

        """
        Virtual method - returns the area of the shape
        in squared internal units.
        """

        return 1

    def _perimeter_internal_units(self):

        """
        Virtual method - returns the perimeter of the shape
        in internal units.
        """

        return 1

    def area(self, areaunits=None):

        """
        Returns the area of the shape in user units.

        Arguments:
        areaunits -- units in which to return area, default is
        squared internal length units
        """

        if not areaunits:
            areaunits = self._units_area
        else:
            validate_area(areaunits)

        area_int = self._area_internal_units()
        return convert(area_int, self._units_area, areaunits)

    def perimeter(self, units=None):

        """
        Returns the perimeter of the shape in user units.

        Arguments:
        units -- units in which to return perimeter, default is
        internal length units
        """

        if not units:
            units = self._units
        else:
            validate_length(units)

        perim_int = self._perimeter_internal_units()
        return convert(perim_int, self._units, units)


class Rectangle(_Shape):

    """
    Rectangle class.
    """

    def __init__(self, sides, units="cm"):

        """
        Class initializer.

        Arguments:
        sides -- a two-element tuple containing the lengths of
        the two sides of the rectangle
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        """

        _Shape.__init__(self, units)

        self._side_a, self._side_b = sides

    def _area_internal_units(self):

        """
        Returns the area of the rectangle in squared internal units.
        """

        return self._side_a * self._side_b

    def _perimeter_internal_units(self):

        """
        Returns the perimeter of the rectangle in squared internal units.
        """

        return (self._side_a + self._side_b) * 2


class Square(Rectangle):

    """
    Square class.
    """

    def __init__(self, side, units="cm"):

        """
        Class initializer.

        Arguments:
        side -- the length of the sides of the square
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        """

        Rectangle.__init__(self, (side, side), units)


class Circle(_Shape):

    """
    Circle class.
    """

    def __init__(self, radius, units="cm"):

        """
        Class initializer.

        Arguments:
        radius -- the radius of the circle
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        """

        _Shape.__init__(self, units)

        self._radius = radius

    def _area_internal_units(self):

        """
        Returns the area of the circle in squared internal units.
        """

        return self._radius ** 2 * pi

    def _perimeter_internal_units(self):

        """
        Returns the perimeter of the circle in squared internal units.
        """

        return self._radius * 2 * pi

solids.py

"""
Solids classes.

Library Version 1.0

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/
"""

# pylint: disable=R0201

from math import pi, sqrt, tan
from geometry.materials import DefaultMaterial
from geometry.conversions import validate_length, validate_area
from geometry.conversions import validate_volume, validate_weight, convert
from geometry.shapes import Rectangle, Circle


class _Solid():

    """
    Base solid class.
    """

    def __init__(self, units, material):

        """
        Class initializer.

        Arguments:
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        validate_length(units)
        self._units = units
        self._units_area = "{0}2".format(units)
        self._units_volume = "{0}3".format(units)

        self._material = material

    def _volume_internal_units(self):

        """
        Virtual method - returns the volume of the solid in internal units.
        """

        return 1

    def _surface_area_internal_units(self):

        """
        Virtual method - returns the surface area of the solid
        in internal units.
        """

        return 1

    def volume(self, volumeunits=None):

        """
        Returns the volume of the cuboid in user units.

        Arguments:
        volumeunits -- units in which to return volume, default is
        cubed internal length units.
        """

        if not volumeunits:
            volumeunits = self._units_volume
        else:
            validate_volume(volumeunits)

        volume_int = self._volume_internal_units()
        return convert(volume_int, self._units_volume, volumeunits)

    def surface_area(self, areaunits=None):

        """
        Returns the surface area of the cuboid in user units.

        Arguments:
        areaunits -- units in which to return surface area, default is
        squared internal length units.
        """

        if not areaunits:
            areaunits = self._units_area
        else:
            validate_area(areaunits)

        area_int = self._surface_area_internal_units()
        return convert(area_int, self._units_area, areaunits)

    def weight(self, weightunits="grams"):

        """
        Virtual method - returns the weight of the solid.
        """

        validate_weight(weightunits)

        return self._material.weight(self.volume("cc"), "cc", weightunits)


class Cuboid(_Solid):

    """
    Cuboid class.
    """

    def __init__(self, edges, units="cm", material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        edges --- a three-element tuple containing the lengths of
        the edges of the cuboid
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        _Solid.__init__(self, units, material)

        self._edge_a, self._edge_b, self._edge_c = edges

    def _volume_internal_units(self):

        """
        Returns the volume of the cuboid in cubic internal units.
        """

        return self._edge_a * self._edge_b * self._edge_c

    def _surface_area_internal_units(self):

        """
        Returns the surface area of the cuboid in squared internal units.
        """

        return (Rectangle((self._edge_a, self._edge_b)).area() +
                Rectangle((self._edge_b, self._edge_c)).area() +
                Rectangle((self._edge_c, self._edge_a)).area()) * 2


class Cube(Cuboid):

    """
    Cube class.
    """

    def __init__(self, edge, units="cm", material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        edge --- the length of the edges of the cube
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        Cuboid.__init__(self, (edge, edge, edge), units, material)


class Sphere(_Solid):

    """
    Sphere class.
    """

    def __init__(self, radius, units="cm", material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        radius --- the radius of the sphere
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        _Solid.__init__(self, units, material)

        self._radius = radius

    def _volume_internal_units(self):

        """
        Returns the volume of the sphere in cubic internal units.
        """

        return self._radius ** 3 * pi * 4 / 3

    def _surface_area_internal_units(self):

        """
        Returns the surface area of the sphere in squared internal units.
        """

        return Circle(self._radius).area() * 4


class Cone(_Solid):

    """
    Cone class.
    """

    def __init__(self, radius, height, units="cm",
                 material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        radius --- the radius of the base of the cone
        height --- the height of the cone
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        _Solid.__init__(self, units, material)

        self._radius = radius
        self._height = height

    def _volume_internal_units(self):

        """
        Returns the volume of the cone in cubic internal units.
        """

        return Circle(self._radius).area() * self._height / 3

    def _surface_area_internal_units(self):

        """
        Returns the surface area of the cone in squared internal units.
        """

        hyp = sqrt(self._radius ** 2 + self._height ** 2)
        return self._radius * pi * hyp + Circle(self._radius).area()


class Cylinder(_Solid):

    """
    Cylinder class.
    """

    def __init__(self, radius, height, units="cm",
                 material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        radius --- the radius of the bases of the cylinder
        height --- the height (or length) of the cylinder
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        _Solid.__init__(self, units, material)

        self._radius = radius
        self._height = height

    def _volume_internal_units(self):

        """
        Returns the volume of the cylinder in cubic internal units.
        """

        return Circle(self._radius).area() * self._height

    def _surface_area_internal_units(self):

        """
        Returns the surface area of the cylinder in squared internal units.
        """

        return (self._lateral_sa_internal_units() +
                self._base_sa_internal_units() * 2)

    def _lateral_sa_internal_units(self):

        """
        Returns the lateral surface area of the cylinder in squared
        internal units.
        """

        return Circle(self._radius).perimeter() * self._height

    def _base_sa_internal_units(self):

        """
        Returns the base surface area of the cylinder in squared
        internal units.
        """

        return Circle(self._radius).area()

    def lateral_surface_area(self, areaunits=None):

        """
        Returns the lateral surface area of the cylinder in user units.

        Arguments:
        areaunits -- units in which to return surface area, default is
        squared internal length units.
        """

        if not areaunits:
            areaunits = self._units_area
        else:
            validate_area(areaunits)

        area_int = self._lateral_sa_internal_units()
        return convert(area_int, self._units_area, areaunits)

    def base_surface_area(self, areaunits=None):

        """
        Returns the base surface area of the cylinder in user units.

        Arguments:
        areaunits -- units in which to return surface area, default is
        squared internal length units.
        """

        if not areaunits:
            areaunits = self._units_area
        else:
            validate_area(areaunits)

        area_int = self._base_sa_internal_units()
        return convert(area_int, self._units_area, areaunits)


class Tube(_Solid):

    """
    Tube class.
    """

    def __init__(self, radii, length, units="cm",
                 material=DefaultMaterial()):

        """
        Class initializer.

        Arguments:
        radii --- a two-element tuple containing firstly the outer
        radius of the tube, and secondly the inner radius of the tube
        length --- the length of the tube
        units -- a string containing the length units for the
        shape, e.g. "mm", "km", "furlong", "mile"
        material -- a material object containing the material
        with which to construct the solid
        """

        _Solid.__init__(self, units, material)

        self._radius_outer, self._radius_inner = radii
        self._length = length

    def _volume_internal_units(self):

        """
        Returns the volume of the tube in cubic internal units.
        """

        return (Cylinder(self._radius_outer, self._length).volume() -
                Cylinder(self._radius_inner, self._length).volume())

    def _surface_area_internal_units(self):

        """
        Returns the surface area of the tube in squared internal units.
        """

        r_o, r_i = self._radius_outer, self._radius_inner
        length = self._length

        end = (Cylinder(r_o, length).base_surface_area() -
               Cylinder(r_i, length).base_surface_area())

        lat = (Cylinder(r_o, length).lateral_surface_area() +
               Cylinder(r_i, length).lateral_surface_area())

        return lat + end * 2

    def segment_extrados(self, nominalradius, angle):

        """
        Returns the extrados dimension of a tube segment of
        a specified angle where those segments would form a
        tube bend with a specified nominal radius.

        Arguments:
        nominalradius -- the nominal radius of the bend
        angle -- the segment angle, in radians
        """

        return self._segment_dimension("extra", nominalradius, angle)

    def segment_intrados(self, nominalradius, angle):

        """
        Returns the intrados dimension of a tube segment of
        a specified angle where those segments would form a
        tube bend with a specified nominal radius.

        Arguments:
        nominalradius -- the nominal radius of the bend
        angle -- the segment angle, in radians
        """

        return self._segment_dimension("inner", nominalradius, angle)

    def segment_mean_length(self, nominalradius, angle):

        """
        Returns the mean length dimension of a tube segment of
        a specified angle where those segments would form a
        tube bend with a specified nominal radius.

        Arguments:
        nominalradius -- the nominal radius of the bend
        angle -- the segment angle, in radians
        """

        return self._segment_dimension("mean", nominalradius, angle)

    def _segment_dimension(self, dim, nominalradius, angle):

        """
        Returns a specified dimension of a tube segment of
        a specified angle where those segments would form a
        tube bend with a specified nominal radius.

        Arguments:
        dim -- the dimension to return, either "extra", "inner", or "mean"
        nominalradius -- the nominal radius of the bend
        angle -- the segment angle, in radians
        """

        if dim == "extra":
            rad = nominalradius + self._radius_outer
        elif dim == "inner":
            rad = nominalradius - self._radius_outer
        else:
            rad = nominalradius

        return rad * tan(angle / 2) * 2

materials.py

"""
Materials classes.

Library Version 1.0

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/
"""


from geometry.conversions import convert


class _Material():

    """
    Base material class
    """

    def __init__(self):
        self._specific_gravity = 1

    def weight(self, quantity=1, volumeunits="cc", weightunits="grams"):

        """
        Returns the weight of a specified volume of the material.

        Arguments:
        quantity -- the volume
        volumeunits -- units in which 'quantity' is stated
        weightunits -- units for which to return the weight

        For example, weight(35, "in3", "pounds") will return the weight,
        in pounds, of 35 cubic inches of the material.
        """

        cc_quant = convert(quantity, volumeunits, "cc")
        w_grams = cc_quant * self._specific_gravity
        return convert(w_grams, "grams", weightunits)

    def specific_gravity(self):

        """
        Returns the specific gravity of the material.
        """

        return self._specific_gravity


class DefaultMaterial(_Material):

    """
    Default material class
    """

    def __init__(self):
        _Material.__init__(self)
        self._specific_gravity = 1


class Water(_Material):

    """
    Water material class
    """

    def __init__(self):
        _Material.__init__(self)
        self._specific_gravity = 1


class _Metal(_Material):

    """
    Generic metal class
    """

    def __init__(self):
        _Material.__init__(self)


class MildSteel(_Metal):

    """
    Steel material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 7.861093


class Aluminium(_Metal):

    """
    Aluminium material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 2.6989


class Copper(_Metal):

    """
    Copper material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 8.96


class Gold(_Metal):

    """
    Gold material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 19.32


class Silver(_Metal):

    """
    Silver material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 10.5


class Iron(_Metal):

    """
    Iron material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 7.894


class Lead(_Metal):

    """
    Lead material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 11.35


class Tin(_Metal):

    """
    Tin material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 7.31


class Zinc(_Metal):

    """
    Zinc material class
    """

    def __init__(self):
        _Metal.__init__(self)
        self._specific_gravity = 7.135

conversions.py

"""
Conversions functions.

Library Version 1.0

Copyright 2013 Paul Griffiths
Email: mail@paulgriffiths.net

Distributed under the terms of the GNU General Public License.
http://www.gnu.org/licenses/
"""

# Set up constants for calculations. Centimetres,
# square centimetres, and cubic centimetres are the
# reference measures for length, area, and volume
# respectively.

# Metric lengths

_CM = 1
_MM = _CM / 10.0
_M = _CM * 100
_KM = _M * 1000
_NMI = _M * 1852

# Imperial lengths

_IN = _CM * 2.54
_PT = _IN / 72.0
_PI = _IN / 6.0
_FT = _IN * 12
_YD = _FT * 3
_FTM = _YD * 2
_FUR = _YD * 220
_MI = _FT * 5280
_LEA = _MI * 3

# Volumes

_CC = _CM ** 3
_L = _CC * 1000
_GAL = _CM * 4546.09
_CI = _IN ** 3
_USGAL = _CI * 231

# Weights

_G = 1
_KG = _G * 1000
_MG = _G / 1000.0
_LB = _KG / 2.2046213
_O = _LB / 16.0
_GRAIN = _O / 437.5

# Set up dictionaries of relative scales

_LENGTHS = {"point": _PT,
            "points": _PT,
            "pt": _PT,
            "pts": _PT,
            "pica": _PI,
            "picas": _PI,
            "pi": _PI,
            "mm": _MM,
            "millimetre": _MM,
            "millimetres": _MM,
            "millimeter": _MM,
            "millimeters": _MM,
            "cm": _CM,
            "centimetre": _CM,
            "centimetres": _CM,
            "centimeter": _CM,
            "centimeters": _CM,
            "inch": _IN,
            "inches": _IN,
            "in": _IN,
            "foot": _FT,
            "feet": _FT,
            "ft": _FT,
            "yard": _YD,
            "yards": _YD,
            "yd": _YD,
            "m": _M,
            "metre": _M,
            "metres": _M,
            "meter": _M,
            "meters": _M,
            "fathom": _FTM,
            "fathoms": _FTM,
            "ftm": _FTM,
            "furlong": _FUR,
            "furlongs": _FUR,
            "fur": _FUR,
            "km": _KM,
            "kilometre": _KM,
            "kilometres": _KM,
            "kilometer": _KM,
            "kilometers": _KM,
            "mile": _MI,
            "miles": _MI,
            "mi": _MI,
            "nm": _NMI,
            "nmi": _NMI,
            "nautmile": _NMI,
            "nautmiles": _NMI,
            "nauticalmile": _NMI,
            "nauticalmiles": _NMI,
            "league": _LEA,
            "leagues": _LEA,
            "lea": _LEA}

_AREAS = {"point2": _PT ** 2,
          "points2": _PT ** 2,
          "pt2": _PT ** 2,
          "pts2": _PT ** 2,
          "pica2": _PI ** 2,
          "picas2": _PI ** 2,
          "pi2": _PI ** 2,
          "mm2": _MM ** 2,
          "millimetre2": _MM ** 2,
          "millimetres2": _MM ** 2,
          "millimeter2": _MM ** 2,
          "millimeters2": _MM ** 2,
          "cm2": _CM ** 2,
          "centimetre2": _CM ** 2,
          "centimetres2": _CM ** 2,
          "centimeter2": _CM ** 2,
          "centimeters2": _CM ** 2,
          "inch2": _IN ** 2,
          "inches2": _IN ** 2,
          "in2": _IN ** 2,
          "foot2": _FT ** 2,
          "feet2": _FT ** 2,
          "ft2": _FT ** 2,
          "yard2": _YD ** 2,
          "yards2": _YD ** 2,
          "yd2": _YD ** 2,
          "m2": _M ** 2,
          "metre2": _M ** 2,
          "metres2": _M ** 2,
          "meter2": _M ** 2,
          "meters2": _M ** 2,
          "centiare": _M ** 2,
          "ca": _M ** 2,
          "fathom2": _FTM ** 2,
          "fathoms2": _FTM ** 2,
          "ftm2": _FTM ** 2,
          "acre": _FT ** 2 * 43560,
          "acres": _FT ** 2 * 43560,
          "ac": _FT ** 2 * 43560,
          "are": _M ** 2 * 100,
          "ares": _M ** 2 * 100,
          "a": _M ** 2 * 100,
          "decare": _M ** 2 * 1000,
          "decares": _M ** 2 * 1000,
          "daa": _M ** 2 * 1000,
          "hectare": _M ** 2 * 10000,
          "hectares": _M ** 2 * 10000,
          "ha": _M ** 2 * 10000,
          "furlong2": _FUR ** 2,
          "furlongs2": _FUR ** 2,
          "fur2": _FUR ** 2,
          "km2": _KM ** 2,
          "kilometre2": _KM ** 2,
          "kilometres2": _KM ** 2,
          "kilometer2": _KM ** 2,
          "kilometers2": _KM ** 2,
          "mile2": _MI ** 2,
          "miles2": _MI ** 2,
          "mi2": _MI ** 2,
          "nmi2": _NMI ** 2,
          "nautmile2": _NMI ** 2,
          "nautmiles2": _NMI ** 2,
          "nauticalmile2": _NMI ** 2,
          "nauticalmiles2": _NMI ** 2,
          "league2": _LEA ** 2,
          "leagues2": _LEA ** 2,
          "lea2": _LEA ** 2}

_VOLUMES = {"point3": _PT ** 3,
            "points3": _PT ** 3,
            "pt3": _PT ** 3,
            "pts3": _PT ** 3,
            "pica3": _PI ** 3,
            "picas3": _PI ** 3,
            "pi3": _PI ** 3,
            "mm3": _MM ** 3,
            "millimetre3": _MM ** 3,
            "millimetres3": _MM ** 3,
            "millimeter3": _MM ** 3,
            "millimeters3": _MM ** 3,
            "ml": _CC,
            "millilitre": _CC,
            "millilitres": _CC,
            "milliliter": _CC,
            "milliliters": _CC,
            "cm3": _CC,
            "cc": _CC,
            "centimetre3": _CC,
            "centimetres3": _CC,
            "centimeter3": _CC,
            "centimeters3": _CC,
            "inch3": _CI,
            "inches3": _CI,
            "in3": _CI,
            "uspint": _USGAL / 8.0,
            "uspints": _USGAL / 8.0,
            "uspt": _USGAL / 8.0,
            "uspts": _USGAL / 8.0,
            "usquart": _USGAL / 4.0,
            "usquarts": _USGAL / 4.0,
            "usqt": _USGAL / 4.0,
            "usqts": _USGAL / 4.0,
            "usgallon": _USGAL,
            "usgallons": _USGAL,
            "usgal": _USGAL,
            "usgals": _USGAL,
            "pint": _GAL / 8.0,
            "pints": _GAL / 8.0,
            "pt": _GAL / 8.0,
            "pts": _GAL / 8.0,
            "quart": _GAL / 4.0,
            "quarts": _GAL / 4.0,
            "qt": _GAL / 4.0,
            "qts": _GAL / 4.0,
            "gallon": _GAL,
            "gallons": _GAL,
            "gal": _GAL,
            "gals": _GAL,
            "l": _L,
            "litre": _L,
            "litres": _L,
            "liter": _L,
            "liters": _L,
            "foot3": _FT ** 3,
            "feet3": _FT ** 3,
            "ft3": _FT ** 3,
            "yard3": _YD ** 3,
            "yards3": _YD ** 3,
            "yd3": _YD ** 3,
            "m3": _M ** 3,
            "metre3": _M ** 3,
            "metres3": _M ** 3,
            "meter3": _M ** 3,
            "meters3": _M ** 3,
            "fathom3": _FTM ** 3,
            "fathoms3": _FTM ** 3,
            "ftm3": _FTM ** 3,
            "furlong3": _FUR ** 3,
            "furlongs3": _FUR ** 3,
            "fur3": _FUR ** 3,
            "km3": _KM ** 3,
            "kilometre3": _KM ** 3,
            "kilometres3": _KM ** 3,
            "kilometer3": _KM ** 3,
            "kilometers3": _KM ** 3,
            "mile3": _MI ** 3,
            "miles3": _MI ** 3,
            "mi3": _MI ** 3,
            "nmi3": _NMI ** 3,
            "nautmile3": _NMI ** 3,
            "nautmiles3": _NMI ** 3,
            "nauticalmile3": _NMI ** 3,
            "nauticalmiles3": _NMI ** 3,
            "league3": _LEA ** 3,
            "leagues3": _LEA ** 3,
            "lea3": _LEA ** 3}

_WEIGHTS = {"gram": _G,
            "grams": _G,
            "g": _G,
            "decigram": _G / 10.0,
            "decigrams": _G / 10.0,
            "dg": _G / 10.0,
            "centigram": _G / 100.0,
            "centigrams": _G / 100.0,
            "cg": _G / 100.0,
            "milligram": _MG,
            "milligrams": _MG,
            "mg": _MG,
            "decagram": _G * 10,
            "decagrams": _G * 10,
            "dag": _G * 10,
            "hectogram": _G * 100,
            "hectograms": _G * 100,
            "hg": _G * 100,
            "kilogram": _KG,
            "kilograms": _KG,
            "kg": _KG,
            "tonne": _KG * 1000,
            "megagram": _KG * 1000,
            "t": _KG * 1000,
            "grain": _GRAIN,
            "grains": _GRAIN,
            "gr": _GRAIN,
            "dram": _O / 16.0,
            "drams": _O / 16.0,
            "dr": _O / 16.0,
            "ounce": _O,
            "ounces": _O,
            "oz": _O,
            "pound": _LB,
            "pounds": _LB,
            "lb": _LB,
            "lbs": _LB,
            "hundredweight": _LB * 100,
            "cwt": _LB * 100,
            "ton": _LB * 2000}

# Set up collection of quantities for conversion functions

_COLLECTIONS = [_LENGTHS, _AREAS, _VOLUMES, _WEIGHTS]


# Define custom exceptions

class InvalidUnit(Exception):

    """
    Exception for unrecognized unit.
    """

    pass


class InvalidLengthUnit(InvalidUnit):

    """
    Exception for unrecognized length unit.
    """

    pass


class InvalidAreaUnit(InvalidUnit):

    """
    Exception for unrecognized area unit.
    """

    pass


class InvalidVolumeUnit(InvalidUnit):

    """
    Exception for unrecognized volume unit.
    """

    pass


class InvalidWeightUnit(InvalidUnit):

    """
    Exception for unrecognized weight unit.
    """

    pass


class IncompatibleUnits(Exception):

    """
    Exception for incompatible units (e.g. comparing miles to gallons).
    """

    pass


# Define functions

def validate_length(unit):

    """
    Checks if a length unit is recognized.

    Arguments:
    unit -- string containing the length unit to check
    """

    if not unit in _LENGTHS:
        raise InvalidLengthUnit(unit)


def validate_area(unit):

    """
    Checks if an area unit is recognized.

    Arguments:
    unit -- string containing the area unit to check
    """

    if not unit in _AREAS:
        raise InvalidAreaUnit(unit)


def validate_volume(unit):

    """
    Checks if a volume unit is recognized.

    Arguments:
    unit -- string containing the volume unit to check
    """

    if not unit in _VOLUMES:
        raise InvalidVolumeUnit(unit)


def validate_weight(unit):

    """
    Checks if a weight unit is recognized.

    Arguments:
    unit -- string containing the weight unit to check
    """

    if not unit in _WEIGHTS:
        raise InvalidWeightUnit(unit)


def validate_unit(unit):

    """
    Checks if a unit is recognized.

    Arguments:
    unit -- string containing the unit to check
    """

    for coll in _COLLECTIONS:
        if unit in coll:
            return

    raise InvalidUnit(unit)


def validate_compatible_units(unit1, unit2):

    """
    Checks if two units are of a consistent type.

    Arguments:
    unit1, unit2 -- strings containing the units to check
    """

    # Check whether both units are valid, first...

    for unit in [unit1, unit2]:
        validate_unit(unit)

    # ...then check whether they're compatible.

    for coll in _COLLECTIONS:
        if unit1 in coll and unit2 in coll:
            return

    raise IncompatibleUnits("{0} and {1}".format(unit1, unit2))


def conversion_factor(unitfrom, unitto):

    """
    Returns a conversion factor for two units.

    Arguments:
    unitfrom -- string containing the unit from which to convert
    unitto -- string containing the unit to which to convert
    """

    unitfrom = unitfrom.lower()        # To make case-insensitive
    unitto = unitto.lower()            # To make case-insensitive

    validate_compatible_units(unitfrom, unitto)

    for coll in _COLLECTIONS:
        if unitfrom in coll and unitto in coll:
            return coll[unitfrom] / float(coll[unitto])


def convert(quantity, unitfrom, unitto):

    """
    Returns a converted quantity.

    Arguments:
    quantity -- quantity to convert, in 'unitfrom' units
    unitfrom -- string containing the unit from which to convert
    unitto -- string containing the unit to which to convert
    """

    return quantity * conversion_factor(unitfrom, unitto)