Objects in this document may appear out of sequence if not viewed with Internet Explorer

ORMSware NMOD Primer: Chapter 19


Reinforcing concepts through PrimerSig's feedback file

In this chapter we take you through execution feedback file of PrimerSig model to demonstrate use and execution of the following:

  • Loading data from Excel tables
  • Temporal arcs (signals)
  • Duration of signal traversals
  • Conceptual clock
  • Adding to default merging logic at Convergence nodes
  • Results from PrimerSig model's CurveBuild mode
  • Turning off execution feedback
  • Optimization of Real type variables using Golden Section Search

Close all other open windows.

Click Figure 13 again to open PrimerSigVSD window (VSD window) showing PrimerSig model diagram. Also open the following windows:

PrimerSigNET.TXT 
PrimerSigADD.TXT 
PrimerSigNMOD.TXT (PrimerSig's execution feedback file)

As mentioned in Chapter 14 (SignalTargets/temporal arcs property of network objects), we have made a few changes to model logic since the previous (Primer) version of the model.

Figure 13-Repeat

We have added Merging property block to the code block of [5] and we have inserted SignalTarget properties in [2], [5], [6], and [8]. If you do not recall these changes, it will be useful to take a quick peek at their code blocks in PrimerSigNET.TXT window.

Loading data from Excel tables

Go to PrimerSigNMOD.TXT window (feedback window). Skip everything at the top of the file and find the beginning of model execution feedback by locating Start of model execution feedback...

A few lines below that you will find Loading table from Excel. This is NMOD's response to the analyst's GetTable instruction in n[6] to get CostPerPieceInputs table from Excel file PrimerSigTables.HTM. To see this instruction, look in code block for object 6 in PrimerSigNET.TXT window (NET window).

Continuing with the feedback, notice that NMOD searches in file 21 (PrimerSigTables.HTM) for table tag {Table.D}CostPerPieceInputs. Upon finding the tag, NMOD looks in the same row of the spreadsheet in columns B and C for row and column border sizes of the table. As you can see in the image of PrimerSigTables.HTM below (in cells b2 and c2), row border size is 0 and column borders size is 1. Look for NMOD's feedback upon finding row and column border sizes. Using this information NMOD proceeds to locate contents of the table.

Figure 20

Follow along, skipping messages that do not make sense to you, if any, until you see 1 column(s) in {Table.D}CostPerPieceInputs. At this point NMOD has figured out how many columns this table has. Now it proceeds to find the number of rows in the table, while also storing away entries from the table. When it comes to table tag {Table.D}CostPerPieceInputs again, NMOD knows how many rows there are in the table.

In the next section in the feedback file NMOD summarizes what it found. Since the GetTable call in n[6] did not specify lower bound(s) of row or column numbers, NMOD has assumed that the start row number of the table is 1 and that the start column number of the table is 1. In the feedback message Table limits: ( 1 : 3 , 1 : 1 ), NMOD tells us that row numbers of the table range from 1 to 3 and column numbers range from 1 to 1. It then allocates memory for the table and lists table entries by rows (5.000000000000000, 5.000000000000000, and 40.00000000000000 in this case).

LaborCosts table has two columns. We recommend that you follow along as above to "watch" NMOD load that table.

Temporal arcs

Locate S2-T0-C0 finished node 6... below Event 1 mark in feedback window. If you recall our explanation of when signals are dispatched, you can see in the feedback file that signals from n[6] are being dispatched as soon as n[6] is finished. In the message S3-T0-C0 Normal signal 0 CostPerPiece~.[6]Initializations~~~.[2]PiecesPerHour NMOD is telling us that it has created a temporal arc from Initializations to PiecesPerHour in the CostPerPiece network.

The surrogate carrying the signal along temporal arc CostPerPiece~.[6]Initializations~~~.[2]PiecesPerHour is S3-T0-C0. Notice that the sequence number of the temporal arc is 0. Temporal arcs will always have 0 for their sequence numbers, since there is no such actual arc present in any network. They exist only in property code blocks in terms of execution instructions. No memory is allocated for temporal arcs the same way memory is allocated for visible logical arcs.

Messages below the Event 2 mark show arrival of the above signal at n[2]PiecesPerHour (from n[6]Intitializations). It triggers [2] immediately. From this point on execution continues as if the surrogate had reached [2] through an actual arc.

Let us now look at the execution of signal below the Event 3 mark. Notice that traces of signal executions are delineated with two records containing {~~~ and }~~~ symbols. Go to code block for [2] in NET window and examine its SignalTarget property. The logic in this property permits execution of signal to [1] only for the first customer. Since there is no other path to [1] in the network, this signal condition ensures that [1]WearParameters executes only once.

Since [1] will execute only for the first customer, there will be no surrogates arriving at [3] through a[9] after the first customer. But, [3] is a Convergence node with two AND arcs coming into it (see VSD window). When a surrogate arrives at [3] through a[8] after the first customer (i.e. customers 2, 3,...), unless and until another surrogate of the same customer arrives along an AND arc to [3], [3] will never execute for that customer.

To make sure that [3] executes for all customers after the first one, we can send a signal (for each customer) to any of the AND arcs leading into [3] so that its AND incidents count will be satisfied. To demonstrate that one can create a temporal arc between two actual arcs, we chose to send this signal from a[8]. Because we chose [8], we could not let the target of the signal be a[8] itself as it would cause an infinite loop (since each signal arriving at [8] will send another signal to [8]).

Note: We can, of course, avoid infinite loop by coding the signal surrogate with a flag and testing for the flag in [8]'s SignalTarget property. But, since a[9] has no active property definitions, sending a signal to it is an easier way to handle this particular convergence. If we were not keen on demonstrating arc-arc temporal connection, we could have sent a signal to [8] from [2] instead. There are always many ways to handle these kinds of situations as we explained in Chapter 14 (SignalTargets/temporal arcs property of network objects).

If you look at the code block for [8] in NET window, you will see that for all customers with ID > 1, a signal will be dispatched to a[9].

To see how this works, let us pick up the events stream at Event 15 mark. By this time, the second customer has arrived (we will look at that customer's arrival into the system in another section of this chapter). We are picking up at Event 15 mark to show that, unlike below the Event 3 mark, no signal is sent from [2] to [1]WearParameters for the second customer. Notice below Event 15 mark that when n[2] is finished for Customer 2, NMOD went on to dispatch surrogates along [12] to n[4] and [8] to n[3] without sending a signal to n[1].

Follow along to Event 16 mark, which is the completion of a[8], and look for feedback block of a signal dispatch as soon as S2-T1-C2 finishes a[8].

S4-T1-C2 Normal signal 0 CostPerPiece~.[8]PiecesPerHour-->>|MachineCostPerHour~~~[9]WearParameters-->>|Machine... indicates that a signal is being dispatched through a temporal connection from the end of arc [12]PiecesPerHour-->>|MachineCostPerHour to the beginning of arc [9]WearParameters-->>|MachineCostPerHour.

Notes

1. Temporal connection established from a network object's SignalTarget property is always from the end of a network object to the beginning of another network object. However, signals can be sent from any property of any object by inserting a {Signal.N} statement in any desired location, where N is the Visio object ID of the desired target object.

2. A signal's sender and target must be on the same page/network. This restriction was included in NMOD to conform to the principle of single entry into any given network, which means that a network is entered at its Start node and that the transfer must come from a referencing Network node.

3. In Chapter 3 (Node and arc notations) we explained why we use Visio object ID to resolve potential ambiguity in arc names. Now that we have discussed temporal arcs, you can see another reason why we made Visio object ID a key part of arc names and used them in node names, too, for notation and content logic consistency. 

Continuing with the feedback line after the signal feedback block, >> << 2 required for convergence, 0 waiting is a message about S2-T1-C2 after it causes the signal dispatch and at the end of a[8] and is totally finished with its arc tasks. Recall that when an arc is finished, NMOD executes its SignalTarget property before it checks anything related to its terminating node. So, NMOD dispatched signal from a[8] as soon as S2-T1-C2 finished a[8], before checking for convergence at n[3], the terminating node of a[8]. Now it is picking up the convergence issue.

Follow along to end of Event 17 mark. Once the signal surrogate S4-T1-C2 arrives at a[9], it executes [9] immediately. Follow along to end of Event 18 mark. You can see below this event how convergence requirement for [3]MachineCostPerHour is satisfied.

Duration of signal traversals

We can increase the duration of signal traversals from the default value of 0 by setting the system property $D.SignalDuration to the desired value prior to a Signal statement. To see how this is done, go to code block for Object 5 in PrimerSig.NET window. Locate code section for the node's SignalTarget property. Examine the code under IF(logBranch)THEN...

As you can see the value of $D.SignalDuration comes from user-supplied model level function dblfSignalDelay in the model's ADD file. Go to PrimerSigADD.TXT window (ADD window) and locate this function. We used this function in this model just for demonstration purposes. All that the function is doing is reading a value we stored in string array strResults during the Trans procedure property of n[5].

If you recall, Trans properties of nodes and arcs are executed before the relevant event is placed in Events queue and SignalTarget properties are executed when those events are finished. So, in SignalTarget property of [5] we are reading the value of #D.PiecesPerHour which we stored in an element of strResults array during Trans property execution. Through the dblfSignalDelay function, that value then gets assigned to $D.SignalDuration prior to the execution of the Signal statement.

Notes

1. Assigning the value of PiecesPerHour to SignalDuration is totally meaningless, of course. It was just a quick way for us to demonstrate the use of user-defined array declared in ADD file, user-defined function provided through ADD file, and setting of Duration of a signal in SignalTarget property, all in one shot.

2. We recommend prefixing names of user-defined functions in ADD file with the name of the KIND (dbl in his case), followed by f to indicate that it is a function.

To see how a signal's Duration works, let us pick up at Event 13 mark in feedback window. Locate S2-T1-C1  finished node 5 CostPerPiece.[5]CostPerPiece.

Recall that [5] is a Departure node and we have seen before how departures are handled. After the message about the departure of customer 1, the next message (@D.PiecesPerHour < &B.PiecesLim,logBranch: 5.000000000000000 40 T) is the result of our custom WRITE statement in SignalTarget property of [5] (see NET window). We inserted the message there so that we will know when looking at execution feedback what the values of @D.PiecesPerHour and &B.PiecesLim are (5 and 40 in this case) and whether the logBranch condition tested above the WRITE statement is True (i.e. whether the model is operating in CurveBuild mode and whether the branch condition for calculating the next point on the curve has evaluated to True).

The next message Duration of signal traversal: 5.000000000000000 is an automatic message NMOD issued while executing the {Signal.2} command in [5]'s SignalTarget property. The value of Signal Duration was set to 5 by the $D.SignalDuration = dblfSignalDelay(bigCustomer) statement prior to the {Signal.2} statement.

Conceptual clock

Conceptual time is a modeling dimension NMOD provides for easy modeling of problems in which calculations have to be performed for events that happen at different points in time (when separating calculations by the time element is important). You will see this in virtually all of the example problems posted at this site.

Conceptual clock is a device for keeping track of conceptual time as NMOD goes through the execution of a model.

In this model we have artificially created a time dimension to demonstrate the use of conceptual time. In the signal feedback block below Event 13 mark notice the message when S3-T0-C0 enters Events queue. It says Entity 3 entering Events queue. Rank value 5.000000000000000.

In Events queue the rank value is the conceptual time at which an entity is scheduled to come out of the queue (i.e. event completion time). Rank value of an entity entering Events queue is calculated by adding the Duration time of the event to the current time on the conceptual clock. Events in Events queue are stored according to the rank value of entities entering the queue, with the earliest event at the head of the queue and the farthest event at the tail of the queue.

The next event in a model is always the event related to the entity at the front of the Events queue. If the event related to the entity at the head of the Events queue is greater than the current time on the conceptual clock, NMOD advances time on the clock when it takes that entity out of the queue. You can see that in the Event 14 mark. Notice that the clock time in the first line of the Event mark is 5.0 while the clock time in Event 13 mark was 0.0.

Skip to Event 24 mark. Follow along to Event 25 mark to see another advancement of conceptual clock (to 15.0).

Before we wrap up this section, let us take another look at SignalTarget property code block of [5] in NET window. Notice the record with the code

     @D.DepartureTime = Time()

Time is a NMOD-provided dbl KIND real/floating point function that analysts can use to fetch the time on conceptual clock. This function has no arguments as you can see from the empty parentheses.

Adding to default merging logic at Convergence nodes

When a customer's surrogates satisfy convergence requirements at a Convergence node, NMOD looks to see if the user has entered any logic in its Merging property. If not, it simply executes the default Merging process. The default process involves just dequeuing all surrogates of the relevant customer waiting in the node's Convergence queue and disposing them. The last surrogate to arrive at the Convergence node survives the merging process and enters the Convergence node.

Let us look at Merging property block of [5] in NET window to see what NMOD does when the analyst has entered logic in a Convergence node's Merging property.

When convergence requirement for that node is met for a given customer, NMOD raises an internal system flag to indicate that it is calling the node's Merging property code for the very first time following the convergence, and then makes the call. You can see in the NET file that we are testing the value of this flag (through NMOD's exposed function MergeInitFlag() to determine when to take care of certain initialization logic we want to execute during the merging process.

In this case, what we are doing while merging threads at [5] is summing up the hourly machine cost and labor cost. These costs are stored in the CostPerHour property of surrogates while they are in [3] and [4], respectively. We are using customer property #D.CostPerHour to store the sum of those costs.

During the first call to the Merging property, before NMOD takes any surrogate out of the node's Convergence queue, merging initialization flag is up and so the first part of the IF-THEN-ELSE block gets executed. Notice that we are asking NMOD to set the value of #D.CostPerHour equal to the surrogate property @D.CostPerHour. But, which surrogate?

Recall that the surrogate that causes dequeuing and disposal of waiting surrogates is the one that arrives last at the node to meet convergence requirement for a given customer. So, the active surrogate during Merging initialization is the surrogate that arrived last, because the other surrogates of the customer are still in the node's Convergence queue. And, therefore, the value of #D.CostPerHour when MergeInitFlag is up will be equal to that of the CostPerHour of the merge triggering surrogate.

Then, holding the triggering surrogate, NMOD locates and dequeues other surrogates of that customer waiting in the node's Convergence queue. Each time it takes a surrogate out of the queue, NMOD calls the Merging procedure property of that node again.

Jumping back in execution feedback file and looking below Event 12 mark, you will see that the triggering surrogate is S2-T1-C1 and it has arrived along a[10]MachineCostPerHour-->>|CostPerPiece. Message Merging initialization logic: CostPerHour sum= 1.81194901 per our WRITE statement means that based on what we know about where S2-T1-C1 came from, this is the machine cost per hour.

After executing Merging property once with merging initialization flag up, NMOD lowers the flag. Then, as mentioned earlier, NMOD proceeds to dequeue current customer's surrogates waiting in the Convergence queue. Each time NMOD dequeues a surrogate, NMOD calls the node's Merging procedure property. Looking at [5]'s code in NET window, you can see that when merging initialization flag is down (i.e. when dequeuing of waiting surrogates is in process), NMOD executes the ELSE part of the IF-THEN-ELSE block we coded up.

In the expression for #D.CostPerHour in the ELSE part of the code, which one is the surrogate whose CostPerHour property is being added to the sum? It has to be whatever surrogate is active at that instant. Obviously, the active surrogate is the surrogate that is just taken out of the Convergence queue.

Notice NMOD's feedback message S3-T1-C1 had arrived through arc 3 CostPerPiece.[7]LaborCostPerHour-->>|CostPerPiece. So, the CostPerHour property being added to #D.CostPerHour is that of S3-T1-C1 (i.e. LaborCostPerHour).

LastMergeOut() is a NMOD-provided logical/binary function which users can use to determine if the active surrogate during a Merging property call by NMOD is the last surrogate of a customer in a Convergence queue. In this case, we are using LastMergeOut to make sure that total cost per piece is written to feedback file only when all surrogates of a customer are out of the Convergence queue, because we only want to write the value when all cost components have been added together (for the sake of illustration).

Key things to remember when writing merging logic are that:

  • During Merging for any given customer NMOD calls Merge property a total of n times where n equals the number of AND incidents that must be detected to trigger convergence for that customer.

  • The first time NMOD calls Merging property during a merge, the active surrogate will be the one that arrived last at the given Convergence node, and the value the MergeInitFlag function returns will be True. This logical function has no arguments.

  • You can examine properties of an active surrogate to decide what actions to take.

  • The second through the nth time NMOD calls Merge property for a customer, the value of the MergeInitFlag function will be False.

  • You can determine if an active surrogate is the nth one by using NMOD-provided logical function LastMergeOut (function has no arguments).

  • You can get the value of the loop index NMOD uses (range = 1 thru n-1) for dequeuing a customer's surrogates from a Convergence queue by using NMOD-provided reg KIND integer function MergeOutCount (function has no arguments).

  • You can get the ID of the active surrogate by using NMOD-provided big KIND integer function MergeOutSur (function has no arguments).

Using the above facts and functions, you should be able to incorporate any desired merging logic into a model.

Alert: One should be careful in storing values in a surrogate property during Merging. Remember that only one surrogate can survive the merging process. Using the functions provided by NMOD, user-defined variables (if necessary), and your own logic, you can actively control which surrogate survives merging, and ensure that the value of any surrogate property you are interested in is properly stored in the surviving surrogate.

Results from PrimerSig model's CurveBuild mode execution

Before looking at results in the feedback file, let us look at the code that stores x-y points for the CostPerPiece curve. Locate Trans property of [5] in NET window. Look at the code in IF(&L.CurveBuild)THEN block. We are storing the result for each customer (i.e. PiecesPerHour point) in the two-dimensional string array strResults. Each row in the array carries the result for one customer. We store the PiecesPerHour value in column 1 and the CostPerPiece result in column 2.

We wanted to write out to feedback file all of the results together at the end of model execution (again, strictly for illustrative purposes, of course). Since SignalTarget property is the last node property to be executed for any given customer, we chose to write the results in the SignalTarget property of the last node in the model, which happens to be [5]. Recall that SignalTarget property executes at the end of node and arc events.

Scan down to SignalTarget property of [5] in NET window. When the condition @D.PiecesPerHour < &B.PiecesLim is no longer True, it is the ELSE-IF part of IF(logBranch)THEN that executes. However, as you can see, the ELSE-IF part will execute only if the model is operating in the CurveBuild mode. If the model is operating in that mode, NMOD writes out the results as specified in the DO construct.

Note: We explained in Chapter 17 (User-defined scalars, arrays, derived types and procedures) why it is okay to use bigCustomer as the upper limit of this DO construct, though its value is set during execution of [5]'s Trans property. If you do not remember this, it may be worthwhile to review the explanation when you are finished with this chapter.

To see the results in the feedback file, go to the feedback window and skip to the very end of the page. Notice that the table columns have headers. Where did these header values get set? If you have not noticed where they were set, go to code block for [6] in NET window. Scan for records where we write to row 0 of strResults (towards bottom of Trans property. Now page up to look at that DO structure in [5] again. Notice that the first loop in the construct writes the 0th row of strResults.

Alternatively, you could do a reference check on strResults, which would produce the following search results:

Searching in c:\ORMSware\PrimerModels\PrimerSig.NET for: strResults
 
   CostPerPiece.[5]CostPerPiece
     {Trans}
         !Write result (x-y pair) to user-defined array strResults
         WRITE(strResults(bigCustomer,1),*)#D.PiecesPerHour !Row=bigCustomer; Col=1
         WRITE(strResults(bigCustomer,2),*)&D.CostPerPiece  !Row=bigCustomer; Col=2
     {SignalTarget}
           !Write row intIndx, columns 1 and 2 of strResults to trace file NMOD.TXT
           WRITE(9,*)(strResults(intIndx,intCol),intCol=1,2)
 
   CostPerPiece.[6]Initializations
     {Trans}
       WRITE(strResults(0,1),'(A)')"Pieces/hour"
       WRITE(strResults(0,2),'(A)')"Cost/piece"

Turning off execution feedback

Model configuration (CFG.htm) files are discussed in Hands-on Tutorial. For now suffice it to know that we can turn off NMOD's execution feedback through each model's CFG.htm spreadsheet or NMOD's general CFG.htm spreadsheet. Of course, during model development it is very helpful to have the feedback switch on while also specifying in the configuration file a limit on the number of events. Feedback gives us essential information for verifying that the model is doing what the analyst intended, while the events limit switch ensures that the trace doesn't create hundreds of megabytes of messages due to overlooked, runaway cycles in the model.

Once a what-if model is fully tested and proven, we may want to use it for optimization discussed in the next section and/or Monte Carlo simulation discussed in Chapter 20 (Reinforcing concepts through PrimerSubNet's feedback file). Both require repeated execution of the what-if model. Since we will already know how the model is working by the time we try optimization search or Monte Carlo simulation, we will want to avoid a torrent of feedback from repeated executions of the model.

In the next section, our feedback file contains very little content, since we turned off the execution feedback switch during the optimization run.

Optimization of real type variables using Golden Section search

Recall that at the end of Chapter 5 (Execution of Network Type nodes) we said that we will show how to use Golden Section search included in NMOD to find optimal answers (widget production rate in this case) that will yield the lowest cost per piece.

Golden Section search is a procedure for finding a real type decision/design/control variable's value which will produce optimal value of a performance variable in the model. In our current model, a decision variable is production rate (pieces per hour). We will explain here how we used Golden Section search to find the production rate that minimizes cost per piece.

Note: NMOD also includes Fibonacci search, the counterpart of Golden Section search for finding best setting for integer type decision/design/control variables in models. Use of this procedure is illustrated in Problem 5 in the Examples section. Other search procedures included in NMOD so far are Gaolseeking and Trivial search. Goalseeking is demonstrated in Problem 3 in the Examples section. Trivial Search is demonstrated in Problem 5 as an easy means for inspiring confidence in other stakeholders that Fibonacci search does indeed find the right answers.

In order to turn a descriptive (what-if) model in effect into a prescriptive (what-should/if-what) model in terms of one decision variable at a time (such as fleet size in the case of Problem 5 and widgets per hour in this case), we have to do the following:

  • Insert two statements into two different locations in a model.

    CALL OptimizationStimulus instruction should go immediately after the statement in the model where the decision variable is set in the what-if model. In PrimerSig model, that location is in n[6]Initializations. Notice in NET window the call we have inserted towards the middle of [6]'s Trans property code block:

    CALL OptimizationStimulus(@D.PiecesPerHour)

    For Golden Section search the stimulus argument must be a real variable of the dbl KIND. We can continue to run the model in what-if mode even after we insert this statement in the model, because this statement becomes benign when we change the model's run mode in the Excel table (in CFG.htm file) containing the parameters for the search.

    CALL OptimizationResponse instruction should go immediately after the statement in the model where the performance/objective variable we want to optimize is calculated. In PrimerSig model, that location is in n[5]CostPerPiece. Notice in NET window the call we have inserted towards the top of [5]'s Trans property code block following the CostPerPiece calculation:

    CALL OptimizationResponse(#D.PiecesPerHour, &D.CostPerPiece, &
    "Pieces per hour, cost per piece:")

    The third (string) argument in the call is optional. If it is present, NMOD will write the result of each search iteration to the standard-out device (monitor in the case of PCs). Whether or not this argument is present NMOD always writes periodic status information to execution feedback file NMOD.TXT.

    For all search procedures in NMOD response argument must be a real variable of the dbl KIND. We can continue to run the model in what-if mode even after we insert this statement in the model, because this statement becomes benign when we run the model in what-if mode.

  • Provide limits and tolerance for the search as shown in {Table.D}PrimerOptPieces in PrimerSigTables.HTM file.

    Final interval between low and high means that starting with the low and high values suggested by the analyst, the search will keep reducing the interval between high and low values systematically until the final interval is less than or equal to the one requested by the analyst. When the search is over, we will know that the desired setting of the control variable lies within the final interval of uncertainty. NMOD does not make an additional probe to find the response associated with the midpoint of this interval. It just makes sure that the true optimal value will be within the required final interval/tolerance from the answer it provides.

  • Create a PGM file for the model as shown below (and explained in Chapter 18):

    PROGRAM PrimerSig
    USE Optimization
    CALL InitOptimization
    CALL GoldenSection("PrimerSigTables.HTM","{Table.D}PrimerOptPieces")
    END PROGRAM PrimerSig

Click here to see the reduced feedback we got when we ran the model in optimization mode with the feedback switch turned off. It may be helpful to search for Search iteration to jump to the start of next iteration (to avoid all of feedback we are not interested in at this point) and then scroll/scan back to view what NMOD has written for current iteration. You will notice that the file does not contain any feedback about events and related activities. Only network and Excel tables loading information and the results of unconditional WRITE statements we inserted in network objects appear in the feedback file.

Click here to see the stimulus and response values written to standard-out device (monitor in the case of PCs) by Golden Section search as it converges on the answer.

Note: We will be using the optimal production rate (24.33 pieces per hour) and cost per piece ($2.31) developed here in the next chapter.

 

Click to go to Chapter 18: Modules and global procedures that make up Model.EXE

Click to go to Chapter 20: Reinforcing concepts through PrimerSubNet's feedback file

Click to go to Introduction: NMOD Primer