Program 5A: Integrator

Write a program to perform a numerical integration.

Given Requirements

 

Requirements: Write a program to numerically integrate a function using Simpson's rule and write the function for the normal distribution. The program should be designed to integrate using various supplied functions... You will need this program to calculate the values of the various statistical distributions used in later program assignments and in the analysis of your PSP data.

Testing: Thoroughly test the program. Include a test to calculate the probability values of the normal distribution integral from -infinity to x=2.5, from -infinity to x=0.2, and from -infinity to x=-1.1. The results should be approximately 0.9938, 0.5793, and 0.1357 respectively. Include in your test report a table of results in the format in Table D10.

 
--[Humphrey95] 

Table 5-2. Test Results Format -- Program 5A

Test Value -- XExpected ResultsActual Results
2.50.9938 
0.20.5793 
-1.10.1357 

Planning

Requirements

The requirements are fairly straightforward, although up to some interpretation. The problem statement in the book says nothing about how numbers are input or the results are verified.

With that in mind, I'll keep on my current trend of reading numbers from standard input until an EOF is encountered. That should keep things fairly simple. After each line of input, the program will output to standard output both the number and the result.

Of course, "infinity" is a concept abhorrent to computers. There are some clever ways of getting around the "infinity" concept for symmetrical distributions like the normal distribution: since the integral over the entire function is 1, integrating from -infinity to 2.5 is the same as adding the integral from 0 to 2.5 to 0.5. But that's very specific to distributions like the normal distribution, etc, which are in fact symmetrical about 0 and integrate to 1, so I don't want to build that kind of functionality into the integrator proper; it'll be handled by an indirection object of some sort. Output will show the number input, followed by the normal integral from -infinity to the input, thus:

-infinity to 2.5: 0.9938

Size Estimate

Ooog. Once again I'm confused by the PROBE method. After a quick conceptual design, I decided on four new objects: a single_variable_equation (very small math object), a normal_distribution_base_equation medium math object), a normal_input_parser (medium i/o object), and a simpson_integrator. Combined with guesses of numbers of methods, this came to about 72 new LOC. Running all this through the PROBE process resulted in an estimate of 160 new/changed LOC (after program 4a ballooned from my guess of 49 to over 129 LOC, the process has ballooned expectations a bit).

It's worth noting that my linear regression parameters as taken from my original estimates compared to actual results were so unacceptable according to the PROBE script that I had to just take averages rather than actually using linear regression. I don't feel comfortable with that, but I agree that programs 2a and 3a have very much skewed things.

Adding to the confusion is the system of naming things and trying to figure out which variables are fed into which equations. PROBE comes up with estimated new/changed LOC, but I have to come up with a new/changed LOC estimate to feed to PROBE. Which of these gets fed into the linear regression calculations for the next estimation session-- mine, or PROBE's? I say mine. Why? Because PROBE is trying to form an actual estimate based on my guesses; if I feed the results back into itself, PROBE will try and guess based of my guesses and its guesses combined, treating the result as one of my guesses. This is fairly unacceptable. For the future, then, I'll be placing my guess of new and changed LOC as the x-value in future PROBE estimations, with the actual new/changed LOC as the y-value.

Resource estimate

I have similar difficulty with choosing parameters for the time estimation; my results have been odd enough so far that I was forced to choose average values again. Here, I am asked to relate estimated new/changed LOC to actual time; in the future, I'll be relating PROBE's size estimates to actual time spent. Note the difference; for size estimations I'll relate my guesses to actual size; for time estimations I'll relate PROBE's estimates to actual time. That ought to sort things out a bit.

With PROBE's estimate of 160 new/changed LOC, my best guess at time (1.168 minutes/LOC) means about 186 minutes for program 5A.

Development

Design

To encapsulate the concept of a single-variable function, I'll create a very simple base class, single_variable_function, with one feature : at( x: double ). Calling at with a value will return the value of the equation, evaluated at the given value. This will be used as a base for both the normal_distribution_base class (the function which is integrated to get our desired value) and the normal_distribution_from_negative_infinity class (long name, but descriptive; its at function returns the value of the normal integral evaluated from negative infinity to the given value, handling the "infinity workaround" described in the planning section).

The bulk of that work will be done by the simpson_integrator class, which encapsulates the Simpson integration logic and can integrate any single_variable_function we throw at it.

To handle the i/o, our brave simple_input_parser gets reused once again, to form the normal_distribution_input_parser, which takes a series of doubles from standard input and prints the resulting values to standard output.

Code

No surprises here. I did make a few very minor changes from the original design, most having to do with the abstraction of the normal_input_parser into a more generic single_function_input_parser, in case I have to reuse this with different functions.

simple_input_parser

This handy class remains the same as it ever has.

main.cpp

/*
*/

#include <fstream>
#include <iostream>
#include "string.h"

#ifndef SINGLE_FUNCTION_INPUT_PARSER_H
#include "single_function_input_parser.h"
#endif
#ifndef NORMAL_DISTRIBUTION_INTEGRAL_H
#include "normal_distribution_integral.h"
#endif

istream *
input_stream_from_args( int arg_count, const char** arg_vector )
{
  istream* Result = NULL;
  if ( arg_count == 1 )
    {
      Result = &cin;
    }
  else 
    {
      const char* help_text = "PSP exercise 5A: Integrate the normal distribution from\nnegative infinity to the values given from standard input\nUsage: \n\tpsp_5a\n\n";
      cout << help_text;
    }
  return Result;
}

int main( int arg_count, const char** arg_vector )
{
  //get the input stream, or print the help text as appropriate
  istream* input_stream = input_stream_from_args( arg_count, arg_vector );
  if ( input_stream != NULL ) 
    {
      single_function_input_parser parser;
      parser.set_input_stream( input_stream );
      normal_distribution_integral f;
      parser.set_function( &f );
      parser.parse_until_eof();
    }
}

/*
*/

single_variable_function

/*
*/
#ifndef SINGLE_VARIABLE_FUNCTION_H
#define SINGLE_VARIABLE_FUNCTION_H

class single_variable_function
{
 public:
  //the value of the function, evaluated at x
  virtual double at( double x ) const = 0;
};

#endif

/*
*/

normal_function_base

/*
*/

#ifndef NORMAL_FUNCTION_BASE_H
#define NORMAL_FUNCTION_BASE_H

#ifndef SINGLE_VARIABLE_FUNCTION_H
#include "single_variable_function.h"
#endif

class normal_function_base : public single_variable_function
{
 public:
  virtual double at( double x ) const;
};


#endif

/*
*/
/*
*/

#include "normal_function_base.h"
#include <math.h>

double
normal_function_base::at( double x ) const
{
  const double base_value = 1 / ( sqrt( M_PI * 2 ) );
  return base_value * exp( -(x * x ) / 2 );
}

/*
*/

normal_distribution_integral

#ifndef NORMAL_DISTRIBUTION_INTEGRAL_H
#define NORMAL_DISTRIBUTION_INTEGRAL_H

#ifndef NORMAL_FUNCTION_BASE_H
#include "normal_function_base.h"
#endif
#ifndef SIMPSON_INTEGRATOR_H
#include "simpson_integrator.h"
#endif

class normal_distribution_integral : public single_variable_function
{
 public:
  virtual double at( double x ) const;
 protected:
  simpson_integrator homer;
  normal_function_base base;
};

#endif
#include "normal_distribution_integral.h"

#ifndef CONTRACT_H
#include "contract.h"
#endif

double
normal_distribution_integral::at( double x ) const
{
  double Result = 0;
  if ( x < 0 )
    {
      Result = 0.5 - homer.integral( base, 0, 0-x );
    }
  else if ( x > 0 )
    {
      Result = 0.5 + homer.integral( base, 0, x );
    }
  else if ( x == 0 )
    {
      Result = 0.5;
    }
  else
    {
      CHECK( false );
    }
  return Result;
}

simpson_integrator

/*
*/

#ifndef SIMPSON_INTEGRATOR_H
#define SIMPSON_INTEGRATOR_H

#ifndef SINGLE_VARIABLE_FUNCTION_H
#include "single_variable_function.h"
#endif

class simpson_integrator
{
 public:
  //sets the acceptable error in the computation
  void set_acceptable_error( double new_error );
  //sets the starting number of segments
  void set_starting_segment_count( int new_starting_segment_count );
  //returns the simpson integral of the given single variable function between the two limits
  double integral( const single_variable_function& f, double lower_limit, double upper_limit ) const;
  //constructor
  simpson_integrator::simpson_integrator( void );
 protected:
  double acceptable_error;
  int starting_segment_count;
  double single_pass( const single_variable_function& f, double lower_limit, double upper_limit, int segment_count ) const;
  bool is_odd( int x ) const;
};

#endif
/*
*/
/*
*/

#include "simpson_integrator.h"

#ifndef CONTRACT_H
#include "contract.h"
#endif

void
simpson_integrator::set_acceptable_error( double new_error )
{
  REQUIRE( new_error > 0 );
  acceptable_error = new_error;
}

void
simpson_integrator::set_starting_segment_count( int new_starting_segment_count )
{
  REQUIRE( new_starting_segment_count > 0 );
  REQUIRE( !is_odd( new_starting_segment_count ) );
  starting_segment_count = new_starting_segment_count;
}

simpson_integrator::simpson_integrator( void ) :
acceptable_error( 0.00001 ),
starting_segment_count( 20 )
{
}

double
simpson_integrator::integral( const single_variable_function& f,
			      double lower_limit, double upper_limit ) const
{
  double Result = 0;
  double previous_result = 0;
  double last_error = 0;
  int segment_count = starting_segment_count;
  bool result_has_been_calculated = false;
  while ( !result_has_been_calculated 
	  || ( last_error > acceptable_error ) )
    {
      previous_result = Result;
      Result = single_pass( f, lower_limit, upper_limit, segment_count );
      last_error = fabs( Result - previous_result );
      //cout << "Segments: " << segment_count << "; error: " << last_error << "\n";
      segment_count *= 2;
      result_has_been_calculated = true;
    }
  return Result;
}


double
simpson_integrator::single_pass( const single_variable_function& f,
				 double lower_limit, double upper_limit,
				 int segment_count ) const
{
  double Result = 0;
  const double segment_width = ( upper_limit - lower_limit ) / static_cast<double>(segment_count);
  const double third_width = segment_width / static_cast< double >(3);
  Result = f.at( lower_limit ) * third_width;
  //cout << "From " << lower_limit << " to " << upper_limit << "\n";
  //cout << "Width " << segment_width << "\n";
  //cout << "Term 0: " << Result << "\n";
  for ( int i = 1; i < segment_count; ++i )
    {
      const double xi = lower_limit + static_cast< double >( i ) * segment_width;
      const double segment_value = f.at( xi ) * third_width;
      //cout << "Xi: " << xi << " ";
      //cout << "F: " << f.at( lower_limit + static_cast<double>(i) * segment_width ) << "  ";
      if ( is_odd(i) )
	{
	  Result += 4 * segment_value;
	  //cout << "Term " << i << ": " << 4 * segment_value << "\n";
	}
      else
	{
	  Result += 2 * segment_value;
	  //cout << "Term " << i << ": " << 2 * segment_value << "\n";
	}
    }
  //cout << "f( upper ): " << f.at( upper_limit ) << "\n";
  Result += f.at( upper_limit ) * third_width;
  CHECK( fabs( upper_limit - ( lower_limit + segment_count * segment_width ) ) < acceptable_error );
  return Result;
}

bool
simpson_integrator::is_odd( int x ) const 
{
  return ( x & 1 );
}
/*
*/

single_function_input_parser

/*
*/
#ifndef SINGLE_FUNCTION_INPUT_PARSER_H
#define SINGLE_FUNCTION_INPUT_PARSER_H

#ifndef SIMPLE_INPUT_PARSER_H
#include "simple_input_parser.h"
#endif
#ifndef SINGLE_VARIABLE_FUNCTION_H
#include "single_variable_function.h"
#endif

class single_function_input_parser : public simple_input_parser
{
 public:
  virtual void parse_last_line( void );
  void set_function( single_variable_function* const new_function );
  single_function_input_parser( void );
 protected:
  single_variable_function* f;
};




#endif

/*
*/
/*
*/
#include "single_function_input_parser.h"

#ifndef CONTRACT_H
#include "contract.h"
#endif

void 
single_function_input_parser::parse_last_line( void )
{
  REQUIRE( f != NULL );
  char* conversion_end = NULL;
  double new_x = strtod( last_line().c_str(), &conversion_end );
  if ( conversion_end == last_line().data() )
    {
      cerr << "Not a double : " << last_line() << "\n";
    }
  else
    {
      cout << new_x << ": " << f->at( new_x ) << "\n";
    }
}

void 
single_function_input_parser::set_function( single_variable_function* const new_function )
{
  f = new_function;
}

single_function_input_parser::single_function_input_parser( void ):
f( NULL )
{
}

/*
*/

main.e

class MAIN

creation
   make
   
feature { ANY }
   
   make is
      local
	 parser : SINGLE_FUNCTION_INPUT_PARSER
	 f : NORMAL_DISTRIBUTION_INTEGRAL
      do
	 !!parser
	 !!f.make
	 parser.set_f( f )
	 parser.set_input( io )
	 parser.parse_until_eof
      end
   
end-- MAIN

single_variable_function.e

deferred class SINGLE_VARIABLE_FUNCTION
   --represents a calculable function

feature { ANY }
   
   at( x: DOUBLE ) : DOUBLE is
      --value of the function evaluated at x
      deferred
    end
	 
end

normal_function_base.e

class NORMAL_FUNCTION_BASE
   --base function for evaluating the normal distribution integral
   inherit
      SINGLE_VARIABLE_FUNCTION
      redefine
	 at
	 end
	 
feature {ANY}
   
   base_value : DOUBLE is 
      once
	 Result := 1 / ( 2.0 * Pi ).sqrt
      end
   
   at( x :DOUBLE ) : DOUBLE is
	 --value of the function at x
      do
	 Result := base_value * ( - (x*x)/2 ).exp
      end
   
end

   

normal_distribution_integral.e

class NORMAL_DISTRIBUTION_INTEGRAL
--calculates the integral of the normal distribution

inherit
   SINGLE_VARIABLE_FUNCTION   
   redefine
      at
      end
   
   
creation
   make
   
feature {ANY}
   
   f : NORMAL_FUNCTION_BASE
   
   homer : SIMPSON_INTEGRATOR
   
   make is
      do
	 !!f
	 !!homer.make
      end
   
   at( x : DOUBLE ) : DOUBLE is
	 --integral of the normal distribution from -infinity to x
      do
	 if ( x < 0 ) then
	    Result := 0.5 - homer.integral( f, 0, -x )
	 elseif ( x > 0 ) then
	    Result := 0.5 + homer.integral( f, 0, x )
	 elseif ( x = 0 ) then
	    Result := 0.5
	 end
      end
   
end

	    

simpson_integrator.e

class SIMPSON_INTEGRATOR
   --integrates a SINGLE_VARIABLE_FUNCTION over a range
   
creation
   make
   
feature { ANY }
   
   acceptable_error : DOUBLE
   
   set_acceptable_error( new_error : DOUBLE ) is
      require
	 acceptable_error > 0.0
      do
	 acceptable_error := new_error
      end
	 
   starting_segment_count : INTEGER
   
   set_starting_segment_count( new_starting_segment_count : INTEGER ) is
      require
	 new_starting_segment_count > 0
	 new_starting_segment_count.even
      do
	 starting_segment_count := new_starting_segment_count;
      end
   
   make is
      --set initial variables
      do
	 acceptable_error := 0.0001
	 starting_segment_count := 20
      end
   
   integral( f : SINGLE_VARIABLE_FUNCTION; lower_limit, upper_limit : DOUBLE ) : DOUBLE is
      --integral of f from lower_limit to upper_limit
      local
	 previous_result : DOUBLE
	 last_error : DOUBLE
	 result_has_been_calculated : BOOLEAN
	 segment_count : INTEGER
      do
	 from
	    previous_result := 0
	    last_error := 0
	    segment_count := starting_segment_count
	    result_has_been_calculated := false
	 until
	    result_has_been_calculated and ( last_error < acceptable_error )
	 loop
	    previous_result := Result
	    Result := single_pass( f, lower_limit, upper_limit, segment_count )
	    last_error := ( Result - previous_result ).abs
	    result_has_been_calculated := true
	    segment_count := segment_count * 2
	 end
      end
	 
   single_pass ( f : SINGLE_VARIABLE_FUNCTION; lower_limit, upper_limit: DOUBLE; segment_count : INTEGER ): DOUBLE is
      -- calculates a single pass of the algorithm
      local
	 i : INTEGER
	 segment_width : DOUBLE
	 segment_value : DOUBLE
	 third_width : DOUBLE
	 xi : DOUBLE
      do
	 segment_width := ( upper_limit - lower_limit ) / segment_count.to_double
	 third_width := segment_width / 3.0
	 check
	    ( lower_limit + segment_count.to_double * segment_width - upper_limit ).abs < acceptable_error
	 end
	 Result := f.at( lower_limit ) * third_width;
	 from
	    i := 1
	 until
	    i = segment_count
	 loop
	    xi := lower_limit + i.to_double * segment_width;
	    segment_value := f.at( xi ) * third_width
	    if i.odd then
	       Result := Result + 4.0 * segment_value
	    else
	       Result := Result + 2.0 * segment_value
	    end
	    i := i + 1
	 end
	 Result := Result + f.at( upper_limit ) * third_width
      end
   
   
end

      

single_function_input_parser.e

class SINGLE_FUNCTION_INPUT_PARSER
--inputs numbers and calculates a given SINGLE_VARIABLE_FUNCTION 
--at each
   
inherit
   SIMPLE_INPUT_PARSER
      redefine
	 parse_last_line
      end
   
feature { ANY }
   
   f : SINGLE_VARIABLE_FUNCTION
   
   set_f( new_f : SINGLE_VARIABLE_FUNCTION ) is
      do
	 f := new_f
      end
   
   parse_last_line is
	 --reads last line as a double and calculates f at that value
      require
	 f /= void
      local
	 new_x : DOUBLE
      do
	 if ( last_line.is_double or last_line.is_integer ) then
	    new_x := double_from_string( last_line )
	    std_output.put_double( new_x ) 
	    std_output.put_string( ": " )
	    std_output.put_double( f.at( new_x ) )
	    std_output.put_new_line
	 else
	    std_error.put_string( "Not a double: " + last_line + "%N" )
	 end
      end

   double_from_string ( string : STRING ) : DOUBLE is
      require
	 string.is_double or string.is_integer
      do
	 if string.is_double then
	    Result := string.to_double
	 elseif string.is_integer then
	    Result := string.to_integer.to_double
	 end
      end
   
end

Compile

No surprises here, except my continual problems: forgetting to update loop variables in Eiffel, forgetting to instantiate variables in Eiffel, forgetting to match up header definitions and implementations in C++

Test

Although this revealed some problems, overall the design worked fine. Results are below:

Table 5-3. Test Results Format -- Program 5A

Test Value -- XExpected ResultsActual Results: C++Actual Results : Eiffel
2.50.99380.993790.993790
0.20.57930.579260.579260
-1.10.13570.1356660.135666

Postmortem

PSP1.1 Project Plan Summary

Table 5-4. Project Plan Summary

Student:Victor B. PutzDate:000106
Program:Numeric integratorProgram#5A
Instructor:WellsLanguage:C++
SummaryPlanActualTo date
Loc/Hour 503448
Planned time186 186
Actual time 182182
CPI (cost/performance index)  1.02
%reused223131
Program SizePlanActualTo date
Base2525 
Deleted05 
Modified51 
Added155103 
Reused5255142
Total New and Changed160104790
Total LOC2321781144
Total new/reused000
Time in Phase (min):PlanActualTo DateTo Date%
Planning266718318
Design192410310
Code504226827
Compile1113656
Test652531231
Postmortem1511778
Total1861821008100
Defects Injected ActualTo DateTo Date %
Plan 000
Design 42525
Code 157069
Compile 033
Test 033
Total development 19101100
Defects Removed ActualTo DateTo Date %
Planning 000
Design 000
Code 32221
Compile 144949
Test 23030
Total development 19101100
After Development 00 
Eiffel code/compile/test
Time in Phase (min)ActualTo DateTo Date %
Code3114646
Compile137624
Test69530
Total50317100
Defects InjectedActualTo DateTo Date %
Design046
Code135892
Compile000
Test012
Total1363100
Defects RemovedActualTo DateTo Date %
Code012
Compile113962
Test22336
Total1363100

Time Recording Log

Table 5-5. Time Recording Log

Student:5ADate:000108
  Program:Victor Putz
StartStopInterruption TimeDelta timePhaseComments
000108 11:29:59000108 12:58:052167plan 
000108 13:10:06000108 13:34:09024design 
000108 13:54:41000108 14:37:21042code 
000108 14:39:39000108 14:53:06013compile 
000108 14:53:10000108 15:18:55025test 
000108 16:42:05000108 16:53:14011postmortem 
      

Table 5-6. Time Recording Log

Student:Victor PutzDate:000108
  Program:5a
StartStopInterruption TimeDelta timePhaseComments
000108 15:19:51000108 15:51:15031code 
000108 15:51:17000108 16:03:51012compile 
000108 16:04:00000108 16:10:1406test 
      

Defect Reporting Logs

Table 5-7. Defect Recording Log

Student:5ADate:000108
  Program:Victor Putz
Defect foundTypeReasonPhase InjectedPhase RemovedFix timeComments
000108 14:08:47mdigdesigncode9added separate feature for single_pass of simpson procedure
000108 14:18:26mdigdesigncode0added is_odd feature
000108 14:29:02mdigdesigncode1didn't design to handle different functions. Probably unnecessary, but it's more modular
000108 14:39:50syomcodecompile0forgot to change header inclusion
000108 14:40:31sytycodecompile0Typo in string constant
000108 14:40:58syomcodecompile0Forgot to add const to implementation
000108 14:42:04iucmcodecompile0Incorrect order of arguments
000108 14:43:25sytycodecompile0forgot to add const to implementation
000108 14:43:50wncmcodecompile0wrong name of a variable
000108 14:44:49wtigcodecompile0tried to "constify" a member and then modify it
000108 14:45:48sytycodecompile0forgot to add "single_function_input_parser::" to constructor
000108 14:46:42mdomdesigncompile0forgot to assign a normal integral to the input parser
000108 14:49:02sytycodecompile0Missed comma in constructor list
000108 14:49:26syomcodecompile0missed more consts in implementation
000108 14:50:10sytycodecompile0attempted eiffel-style assignment (:=) in feature
000108 14:50:44sytycodecompile0misspelled variable name
000108 14:51:22wtcmcodecompile1used "double" as argument in is_odd; meant to use int
000108 14:54:09waigcodetest18check was for double equality; should have used difference < margin
000108 15:13:26waomcodetest3some confusion in boolean check conditions to continue the simpson process
       

Table 5-8. Defect Recording Log

Student:Victor PutzDate:000108
  Program:5a
Defect foundTypeReasonPhase InjectedPhase RemovedFix timeComments
000108 15:51:40sytycodecompile0forgot "end" at end of class
000108 15:52:09maomcodecompile0forgot to instantiate the parser
000108 15:53:01maomcodecompile0forgot to create a function and assign it to the parser
000108 15:53:57sytycodecompile0put inherit and creation clauses in wrong order
000108 15:54:15sytycodecompile0used c-style == to check equality
000108 15:55:23macmcodecompile2forgot to instantiate f and call its creation clause
000108 15:59:00syomcodecompile0forgot to declare a return type for integral
000108 16:00:14maomcodecompile0forgot to call make on new object
000108 16:00:59sytycodecompile0forgot to put class name in all caps
000108 16:01:40syomcodecompile0incorrectly declared once function
000108 16:03:28syomcodecompile0forgot to add third_width to local declaration
000108 16:04:11maomcodetest1forgot to increment loop index... again...
000108 16:08:22maomcodetest1forgot to update has_been_calculated flag