#Copyright 2009 Brian Jaress
#brian_jaress@gna.org
#This file is part of Jest.
#
#Jest is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 3 of the License, or
#(at your option) any later version.
#
#Jest is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program. If not, see <http://www.gnu.org/licenses/>.
"""COCOMO II estimation
This is an implementation of the COCOMO II estimation formulas for
software projects. The formulas relate the size of the project, in
lines of code, to the time it takes and number of people.
The formulas also take into account ratings for different situational
and individual factors, such as how well the team gets along. These
ratings are optional, and the default of 'normal' is usually
appropriate.
"""
from operator import mul
from itertools import chain
__all__ = ['Capabilities', 'RATING_LIST', 'AREA_TABLE', 'TIME_UNIT_LIST']
RATING_LIST = [
'extra-easy',
'very-easy',
'easy',
'normal',
'hard',
'very-hard',
'extra-hard'
]
RATING_TABLE = dict((rating, idx) for idx, rating
in enumerate(RATING_LIST))
def look_up(table, area, rating):
"""Find the constant associated with a particular rating in a
particular area."""
return table[area][0][RATING_TABLE[rating]]
def look_up_all(table, area_ratings):
"""Look up ratings in multiple areas at once."""
return [look_up(table, area, area_ratings.get(area, 'normal'))
for area in table.keys()]
#Areas that affect the exponent
CRITICAL_AREA_TABLE = {
'precedent': ([0, 1.24, 2.48, 3.72, 4.96, 6.20, 6.20],
'Similar past work, existing studies of the problem, etc.'),
'flexibility': ([0, 1.01, 2.03, 3.04, 4.05, 5.07, 5.07],
'Flexibility of the requirements'),
'risk': ([0, 1.41, 2.83, 4.24, 5.65, 7.07, 7.07],
'How well the risks have been studied and planned for'),
'team': ([0, 1.10, 2.19, 3.29, 4.38, 5.48, 5.48],
'How well everyone involved gets along'),
'process': ([0, 1.56, 3.12, 4.68, 6.24, 7.80, 7.80],
"The quality of your official development policies.")
}
#Areas that affect the coefficient
RELEVANT_AREA_TABLE = {
'reliability': ([0.82, 0.82, 0.92, 1.00, 1.10, 1.26, 1.26],
'Required reliability of the program'),
'data': ([0.90, 0.90, 0.90, 1.00, 1.14, 1.28, 1.28],
'Size of dataset'),
'documentation': ([0.81, 0.81, 0.91, 1.00, 1.11, 1.23, 1.23],
'Percentage of time spent writing documentation'),
'complexity': ([0.73, 0.73, 0.87, 1.00, 1.17, 1.34, 1.74],
'Complexity of the problem'),
'reusability': ([0.95, 0.95, 0.95, 1.00, 1.07, 1.15, 1.24],
'Required reusability of the code'),
'speed': ([1.00, 1.00, 1.00, 1.00, 1.11, 1.29, 1.63],
'How fast the program has to be'),
'memory': ([1.00, 1.00, 1.00, 1.00, 1.05, 1.17, 1.46],
'How tight the memory usage requirements are'),
'platform-volatility': ([0.87, 0.87, 0.87, 1.00, 1.15, 1.30, 1.30],
'Rate of change in the platform under the program'),
'analyst': ([0.71, 0.71, 0.85, 1.00, 1.19, 1.42, 1.42],
'Analyst skill level, not including experience'),
'application-experience': ([0.81, 0.81, 0.88, 1.00, 1.10, 1.22, 1.22],
'Team experience with similar applications'),
'programmer': ([0.76, 0.76, 0.88, 1.00, 1.15, 1.34, 1.34],
'Programmer skill level, not including experience'),
'platform-experience': ([0.85, 0.85, 0.91, 1.00, 1.09, 1.19, 1.19],
'Team experience with platform'),
'language-experience': ([0.84, 0.84, 0.91, 1.00, 1.09, 1.20, 1.20],
'Language and tool experience'),
'turnover': ([0.81, 0.81, 0.90, 1.00, 1.12, 1.29, 1.29],
'People leaving'),
'tools': ([0.78, 0.78, 0.90, 1.00, 1.09, 1.17, 1.17],
'Tool and language capability'),
'collaboration': ([0.80, 0.86, 0.93, 1.00, 1.09, 1.22, 1.22],
'Physical proximity and communication methods of team members')
}
AREA_TABLE = dict((key, val[1]) for key, val in chain(
CRITICAL_AREA_TABLE.iteritems(), RELEVANT_AREA_TABLE.iteritems()))
#COCOMO II constants
CRITICAL_MIN = .91
CRITICAL_CALIBRATION = 0.01
RELEVANT_CALIBRATION = 2.94
TIME_COEFFICIENT = 3.67
TIME_EXP_MIN = 0.28
TIME_EXP_CALIBRATION = 0.2
def seq(init, *funcs):
"""Simple function composition.
Functions will be applied in order from left to right."""
return reduce(lambda x, f: f(x), funcs, init)
class Capabilities():
"""The ability of your team to do work.
We don't include schedule pressure. The results are what you'd
get from standard COCOMO II using a schedule of "Nominal" (normal).
"""
def __init__(self, **area_ratings):
"""Store aggregates of the ratings."""
self.exp = CRITICAL_MIN + CRITICAL_CALIBRATION * sum(
look_up_all(CRITICAL_AREA_TABLE, area_ratings))
self.coef = RELEVANT_CALIBRATION * reduce(mul,
look_up_all(RELEVANT_AREA_TABLE, area_ratings),
1.0)
self.time_exp = TIME_EXP_MIN + TIME_EXP_CALIBRATION * (
self.exp - CRITICAL_MIN)
def lines_to_time(self, lines):
"""Typical COCOMO II calculation."""
return seq(lines,
self._lines_to_effort,
self._effort_to_time)
def time_to_lines(self, months):
"""Default calculation (inverse of typical)."""
return seq(months,
self._time_to_effort,
self._effort_to_lines)
def lines_to_people(self, lines):
"""Typical COCOMO II staff estimation."""
return seq(lines,
self._lines_to_effort,
self._effort_to_people)
def time_to_people(self, months):
"""Default staff estimation."""
return seq(months,
self._time_to_effort,
self._effort_to_people)
def _lines_to_effort(self, lines):
"""COCOMO II effort formula."""
ksloc = lines / (10. ** 3)
return self.coef * ksloc ** self.exp
def _effort_to_lines(self, person_months):
"""Reverse of COCOMO II effort formula."""
ksloc = (person_months / self.coef)**(1/self.exp)
return ksloc * (10. ** 3)
def _effort_to_time(self, person_months):
"""COCOMO II time formula."""
return TIME_COEFFICIENT * person_months ** self.time_exp
def _time_to_effort(self, months):
"""Reverse of COCOMO II time formula."""
return (months / TIME_COEFFICIENT)**(1/self.time_exp)
def _effort_to_people(self, person_months):
"""COCOMO II staff formula."""
try:
return person_months / self._effort_to_time(person_months)
except (ZeroDivisionError):
#If nothing needs to be done, no one needs to do it.
return 0
|