Thursday, June 20, 2024

The 3 Types of Woodstock Yield Tables (plus one)

Introduction

Currently, I'm working on a strategic planning model for a research forest in the U.S. Forests held by land-grant universities is subject to scrutiny from many interested parties: faculty, students, neighboring landowners, etc. In this case, we need to recognize ongoing research projects, recreational use, biodiversity, fire resilience and other metrics. Representing these metrics in the model required the use of all Woodstock yield types, so I thought this would be a good opportunity to review them with you.

Three Types of Yield Tables

Anyone who has taken Remsoft’s Modeling Fundamentals course was taught the three types of yield tables: age-dependent, time-dependent and complex. Let’s review each one.

Age-Dependent Yields

Yield tables that represent the development of stands over time are usually age-dependent. For a given age, the yield table shows things like product volume, diameter, dominant height, basal area, and other stand parameters. The yield table header starts with *Y followed by the development type mask.

*Y ? ? ? ? ? ? ? ? EX ? 00 00 ? ? ? ? 010111 010111  
_AGE yiTPA  yiBA yiBAO yxTHT yxQMD yxSDI yxCCF   yiTCF
   6   374   135    23    57   8.1   268   192   3.075
   7   359   153    26    67   8.8   295   209   3.995
   8   345   171    30    73   9.5   320   226   4.936
   9   332   189    34    80  10.2   343   241   5.899
  10   319   205    38    87  10.8   364   255   6.843

Of course, this assumes that you are practicing even-aged management, and that you know the age of your stands fairly precisely. Otherwise, you should be using a time-dependent yield table.

Time-Dependent Yields

Yield tables that represent values that depend on planning periods rather than age are time-dependent. Prices and costs that are expected to change over time (in real terms) are associated with particular planning periods, and the yield header starts with *YT followed by the development type mask.

*YT ? ? ? ? ? ? ? ? ? ? ? ? ? ? CA ? ? ?
y$CC 1 327
y$UI 1 377
y$UG 1 377
y$UV 1 285
y$OR 1 388

*YT ? ? ? ? ? ? ? ? ? ? ? ? ? ? GR ? ? ?
y$CC 1 189
y$UI 1 280
y$UG 1 222
y$UV 1 195
y$OR 1 388

Complex Yields

A complex yield table is really just a way to created new yield components based on previously-defined yield components. For example, you can sum individual product volumes to produce a total volume coefficient. Complex yield headers start with *YC.

Woodstock provides a number of built-in functions to make complex yields easier to define and to speed up processing. For example, I can define a complex yield coefficient that sums all volume coefficients (MBF/ac) that occur in the same period. I can also define cost coefficients that occur in the same period that depend on harvest volume ($/MBF). If I use this method to trigger total costs, I can reduce the matrix generation time. In theory, I should also be able to define a harvest cost coefficient by multiplying product harvest volume by logging cost.

*YC ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
yiDF _SUM(yiDF0,yiDF4,yiDF8,yiDF2,yiDF3)
ycCC _MULTIPLY(yiMBF,y$CC)
ycSW _MULTIPLY(yhMBF,y$CC)
ycSC _MULTIPLY(yhMBF,y$T2)

Since most people define costs and prices as time-dependent yields, the example above may or may not work. If you define stumpage prices as time-dependent, but they are static (no price appreciation), the syntax above works fine. But if you have price appreciation, Woodstock will throw the following error:

Error: 386, line number(2539), in file C:\Work\Model.yld 
       yrPLP _MULTIPLY(yplp,yplp$)
Incorrect use of internal functions. You cannot mix Age-dependent and Time-dependent yield curves. 

There is a work-around if you're willing to use older code:

*YC .MASK()
yrPWD yPWD * y$PWD

If you have a lot of yield tables to process, the interpreted code is much slower than the functions like _MULTIPLY( ) that are implemented using dynamic link libraries (DLLs). But that may be better than rethinking your entire model.

Sequence-Dependent Yields?

I know we are becoming an endangered species, but those of us old enough to have used FORPLAN may remember that yields could be one of age-, time- or sequence-dependent. By sequence-dependent, we mean that a yield is triggered after some other event. For example, suppose that I want to model siltation and have it triggered for a few periods after a harvest operation. The siltation does not depend on the age of the trees harvested, and while the amount of silt produced may change with time, it is not associated with any particular planning period either. It is truly sequence-dependent.

Woodstock doesn’t explicitly offer sequence-dependent yields and you may think you are out of luck modeling something like siltation. However, you can incorporate yield tables within prescriptions in the REGIMES section. For example, in the research forest model, we have a group selection prescription that consists of periodic harvests. We also have a yield table that shows changes in the presence of pollinator insects following harvest that we link directly in the prescription table.

*PRESCRIPTION rxUG01 Start removals in period 1
*OPERABLE .MASK(_TH1(F),_TH4(UGRP),_TH17(U01)) _AGE >= 1
  _RXPERIOD _ACTION _ENTRY  yxPOL
      0       aGS  _INITIAL   4.0    
      1        -       -      3.5     
      2        -       -      2.5 
      3        -       -      1.0
      4       aGS      -      4.0  
      5        -       -      3.5
     ...

I have defined the prescription rxUG01. The action aGS (group selection harvest), boosts pollinator levels to 4.0 (on a scale of 0 to 5). One period (5 years) later, pollinator levels drop to 3.5. As the canopy closes on the group selection harvest area, pollinators drop more quickly. However, following another harvest, pollinator levels are boosted once again. While you could probably construct age- or time-dependent yields to represent these, they would quickly become unwieldy if the return period between harvests is not constant (say 15 years, then 10 years and then 20 years).

If the yield table containing yxPOL is present in the YIELDS section within a time-dependent yield table, it will also be treated as time-dependent in REGIMES. That means it will be subject to the same rules for other time-dependent yields, including the restriction on mixing types using yield functions. I don’t know how yield tables in the REGIMES section are represented in the Woodstock code (as age- or time-dependent) if the yield component is not present in the YIELDS section.

Confused by Regimes and Sequence-Dependent Yields? Contact Me!

If you are interested in modeling innovative silviculture or other uncommon formulations, give me a shout! I have lots of experience modeling different types of silviculture from around the world and I'm sure I can help you devise an efficient formulation.

Friday, May 24, 2024

Getting Good Graphs From Woodstock

Introduction

I’m always surprised when I learn that clients do not use the Woodstock graphics section to produce on-screen graphs of their solutions. Frankly, I can’t conceive of doing an analysis without monitoring standing inventory, harvest levels, operable inventory and silvicultural activity areas. If nothing else, a graph will quickly indicate if you forgot to generate a new schedule after imposing an even-flow constraint or something similar. Seeing how inventory is changing can provide insights into why harvest levels don't seem to be what you expect.

That being said, the default color choices that Remsoft provides are at best, meh. People who know me know that I despise the “mustard” color, which to me looks more like baby poop. They also know that I don’t think much of 3D charts (which are hard to interpret), line charts with all sorts of markers, and graphs with too many outputs graphed simultaneously. There are good resources on how to display data, but Woodstock doesn’t enforce any of these rules. It is up to you to make something informative and visually pleasing.

About the Graphics Colors

Unfortunately, you cannot redefine Woodstock’s standard color codes to more pleasing shades. These are, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, SCARLET, EMERALD, NAVY, MUSTARD, PEACH, ORANGE, BROWN, PURPLE, GREY, and BLACK. However, you can tell Woodstock that you want to use a custom color instead of one of the standard colors.

Right-click the graph and bring up the Properties dialog. Select the output you want to change and click the More (…) button. You can choose one of the standard Windows colors, or you can define your own by clicking in the rainbow box, or by specifying HSL percentages (hue, saturation, luminence) or RGB decimal values.

How to customize Woodstock graph colors

Once you’ve selected and saved your custom color, Woodstock will save your choice in the Graphics section using the _RGB() function, which uses decimal numbers instead of hex.

 *LINES
 OICE 1 1000 _RGB(203,24,29) _SOLID "Cedar "
 OIPW 1 1000 _RGB(107,174,214) _SOLID "Ponderosa & white pines"
 OIDF 1 1000 _RGB(35,139,69) _SOLID "Douglas-fir, larch, spruce"
 OIWW 1 1000 _RGB(204,204,204) _SOLID "Whitewoods "
 OILP 1 1000 _RGB(33,113,181) _SOLID "Lodgepole pine"

That is all well and good, but if you save a bunch of custom colors, how do you use them in your next modeling project? You could copy and paste from your graphics section to inherit the colors, but the better approach is to edit the Woodstock palette.ini file and save them.

The WKPalette.ini File

In your Remsoft installation folder (e.g., C:\Remsoft\Ver2023.02.01), you will find a text file called wkpalette.ini. It is just a text file with a bunch of colors defined by hexadecimal RGB values. For example, #000000 is the RGB code for black, where red = 00, green = 00 and blue = 00. White would be #FFFFFF, with all three colors set to the maximum 255 (FF in hex). All other colors are some combination of RGB values ranging from black (00) to white (FF). Greys are easy because they are all the same value for red, green and blue (i.e., #969696 is a mid grey).

If you have decided on colors previously, you can save them as a new palette in the wkpalette.ini. First, save the old file as wkpalette.old, just in case you muck things up. Then open the file in any text editor (Notepad, Woodstock)

[Palette:_Default]
#000000
#808080
#FF8000
#804000
#0000FF
#000080
#FFFF00
#C2C200
#8000FF
#800080
#C20000
#008000
#21A161
#FF80C2
#FFC080
3FF0000

There are multiple palettes defined in the file, which can be selected from the Options menu of the Graphics window. 


How to choose alternative palettes

The best source I know of for choosing pleasing but contrasting color combination in your maps is the ColorBrewer 2.0 website. Designed for GIS professionals, it has numerous options for displaying different kinds of data on choropleth maps. But the same concepts also apply to graphs. 

For a project I am currently working on, one of the team members is red-green color blind. After researching some websites on the subject, I selected a color palette that should allow anyone to discern differences in the data.

[Palette: ColorBlind]
#000000
#E69F00
#648FFF
#009E73
#F0E442
#0072B2
#D55E00
#CC79A7
#332288
#117733
#44AA99
#88CCEE
#DDCC77
#CC6677
#AA4499
#882255

Results

The colors in the new palette (right) are softer than the Remsoft defaults (left):



Alternative palettes: Remsoft Default (L) vs Custom Color Blind (R)

Summary

In addition to colors, you can also choose different fonts. The standard Tahoma fonts look good, but for those who like a fixed pitch font, Consolas is also nice. I use it as the text font in the Woodstock editor. I believe it displays better on screen than the default Courier New. When I am teaching new analysts, it is important to be able to distinguish spaces and underscores, and that's hard to do with Courier font. 

NOTE: If you change the palette file, you'll also have to change the palette setting in your Woodstock graphs if you want the graph colors to change. And remember, colors are just randomly chosen when you first define graphs. You still have to manually change them if you find the same colors repeated, etc. Have fun!

Interested in How to Better Present Results? Contact Me!

Model diagnostics are very important which is why I think using graphs is mandatory. With graphs, you can immediately see if constraints are being respected and if your outputs are calculating properly. I can lead a custom session for your staff on how to do QA and model testing right!

Wednesday, May 15, 2024

Errors of Omission and Commission

Introduction

Recently, I've been working on a modeling project with a lot of interesting silviculture. In addition to the usual suspects (clearcutting, commercial thinning), this model includes group selection, individual tree selection and variable retention harvesting. Devising the conceptual model for this project was challenging (something I'll talk about in another post), but keeping track of the many different yield tables was equally so. Specifically, it was a challenge to know that I was associating the correct yield table with each stand, AND that I wasn't inadvertently missing tables. So, in this post, I’m going to review errors of omission and commission and highlight a feature in the Woodstock editor that can be a big help.

Errors of Omission

You are committing an error of omission in your model when you reference a cost coefficient that doesn’t exist. For example, if you have stumpage prices by district, but you forgot to include one district in the YIELDS section, Woodstock assumes a value of 0 for prices in the missing district. Normally, that will just generate the familiar Warning 74 (yield not found, assume 0), despite the fact that it is a major problem.

Luckily, you can avoid errors of omission fairly easily by following a simple methodology based on this idea: never rely on Woodstock to assume 0 for you:

Define all yield sets with the same yield components (columns). If a product doesn’t exist in a stand, then print a column of zeros. Yes, Woodstock will complain (Warning 328) that you don’t need that column. You can mute that error code in the CONTROL section.

If you include non-productive areas in your model, assign them an explicit yield set as well. You just need a single row of zeros under each yield component heading for AGE or CP = 1.

If you do this, you should be able to check for missing yields using any of your defined yield components (e.g., ytmvt) using the following report:

*TARGET yield_check.dbf
ytmvt

Open the report file, and sort the YldNumber column in ascending order. If there are blanks in the YldNumber column, the development types shown do not have yield tables. Woodstock has assumed 0 for you. 



Excerpt of Yield_Check report

How does it work?

Internally, Woodstock maintains a list of YldNumbers (corresponding to each yield component) to which each development type points. If a development type does not point to a yield component, it triggers the warning 74.

By ensuring that every development type points to an instance of every yield component, there should be no gaps in the yield report. Yes, it does require redundant information that must be stored in memory, but the trade-off is knowing that you are not missing any real yield information.

The DevTypes you should be most concerned about are existing types that have never been treated. These ALL should have matching yield tables or else your initial inventory numbers will be off.

You may also have missing yields for specific timing choices of an action, even though all existing untreated stands are accounted for. This can arise when you have specified a general operability statement, but some conditions do not have yields associated with them. You can apply very specific DevType masks on multiple lines to account for every single stand, but this may not be worth the effort to account for them all. If the number of missing yield tables is small and only represents a small subset of some specific timing choices, all may be fine. When Woodstock evaluates those timing choices, the 0 coefficients associated with them should cause those timing choices to never be chosen.

Errors of Commission

You are committing an error of commission when you point to wrong information in your model. For example, this can occur if you associate the wrong price coefficient in your output definition, or if Woodstock matches an incorrect yield table with a development type. Unfortunately, errors of commission are much harder to find than errors of omission. There is no easy "Wrong_Yield" report you can use to identify them.

Avoiding these types of errors requires diligence and a methodical approach to model development. You should use the *GROUP keyword in your OUTPUTS section to develop all your outputs. Just like an outline in a Word document, you can use *GROUP to identify all the potential outputs you will need. Then, within each *GROUP, systematically work through all the variations on outputs that required. Copy and paste operations often result in overlooked edits that lead to errors of commission. Automation tools like Integrator or Python are better at ensuring that you are enumerating all the possible combinations with the right yield component.

CTRL-M - a handy tool

My favorite newer feature of the Woodstock editor is the CTRL-M (mask search) function. This feature works very much like the CTRL-F (Find) function but it isn’t literal like CTRL-F. For example, if you search a file for the string ‘CH00018 ? ? ? ? UT’, Woodstock will look only for strings where there is a single space between question marks. It will not find this string where there are two spaces between the question mark and UT : ‘CH00018 ? ? ? ? UT’. Unless you are very consistent in spacing, CTRL-F can be of limited utility if you’re trying to find a matching yield table for a particular development type.

If you open the CRTL-M dialog, you can enter a fully defined development type mask, with or without global and aggregate attributes. When you click Find, the Woodstock editor searches for masks that match, including global attributes. For example, suppose you input the following string into the CTRL-M dialog:

'FEE NONE NONE FC Y CSP CH00018 FE N WH I UT 250 ZR6024 1 LBC'. 

In your YIELDS section, CTRL-M will locate this yield set header:

*Y ? ? ? ? ? ? CH00018 ? ? ? ? UT ? ? ? ? 

and this one,

*Y ? ? ?      ? ? ?   CH00018 ? ?         ? ? UT ? ? ? ?

and also this one,

*Y ? ? ? ? ? ? agGRP8 ? ? ? ? ? ? ? ? ?

In the first instance, the mask search matches to global attributes (?) in all themes except TH7 and TH12. The second instance is exactly the same, but spacing does not matter, as it would in the Find function. With the third instance, the mask search also locates yield headers with valid aggregate attributes - the aggregate agGRP8 includes CH00018.

A Potential Use Case for CTRL-M

Let's assume that you’ve noticed you are getting incorrect volumes reported, and you’ve identified one of the development types that is yielding wrong values. Using CTRL-M, you start searching your YIELDS section for matching yields. You expect to find the correct yield in the bottom third of the file, but instead you find a match almost immediately. When you search again, you find the yield set you were expecting. You have a problem! Remember, Woodstock only searches for yield information until it finds a first match; later matches are ignored.

Now that you’ve found the problem yield table, the question is, why is it there? Did your yield generation process include old inventory information that produced the errant yield table? Was this just temporary code used earlier in the development process? Regardless of how it got there, it can be easily removed. However, if you have an automated process or you rely on contractors to provide the data, you need to make sure that future yield updates do not create the problem once again.

Is Your Model Driving You Crazy? Contact Me!

Finding errors of omission and commission can be difficult in a large complex model. If you’d like a new set of eyes to look over your model(s), give me a shout to schedule a model audit Afterwards, we can schedule an on-site training review with your staff to review recommendations for avoiding problems in the future.

If you have a topic that you'd like me to address in a future post, send me a message. I'm always more interested in helping with client issues than ones that I think might be of interest.

Monday, April 29, 2024

Regimes - Why Aren't People Using Them?

Introduction

If memory serves me correctly, Remsoft introduced the Regimes module back in 2009. At first, it did not generate a lot of interest, despite criticisms from some circles that Model I was better than Model II. It was an extra module to license, and frankly, it wasn't the easiest syntax to wrap your head around. But times have changed!

Regimes is now part of the base Woodstock license. And while the syntax isn't the easiest thing to master, I've come to appreciate how Ugo was able to incorporate Model I structure into Woodstock's inherent Model II framework. As a user of FORPLAN, I remember how meshing Model I and Model II in a single model resulted in a very clumsy interface. It was no surprise that few people used FORPLAN v.2. But it has been 15 years since Woodstock Regimes was released, and yet I still don't see a lot of clients using it. This poll I conducted confirms that:

Poll conducted week of April 22, 2024

Why Use Regimes?

In Woodstock, when you specify an action, you provide the operability criteria (development type + condition) that allows the interpreter to generate a decision variable for each DevType-condition combination. If there are 5 operable DevTypes and 5 conditions (e.g. age) when each DevType can be harvested, the interpreter will generate 25 decision variables. The beauty of Regimes is that it allows you to create decision variables for a sequence of events, rather just a single activity. 

So, for what kinds of forest management decisions are Regimes suited? I've seen many models in the US South where commercial thinning is always followed by a fertilization one year later. While the timing of the thinning is a decision, the fertilization is not. Before Regimes, modeling CT + fert required inventory outputs with somewhat unintuitive DevType masks to make it work and report the fertilization acres in the correct period:

*SOURCE .MASK(_THx(14)) @AGE(16) _INVENT _AREA
; add 1 to age because of age counter

Additionally, by relying on an inventory output, it makes your model run slower.

*REGIME rThin
*OPERABLE rThin
   .MASK(_THx(00)) _AGE >= 13 AND _AGE <= 18

 *PRESCRIPTION rxTF (thin and fert)
   _RXPERIOD _ACTION   _ENTRY 
       0       aTH    _INITIAL
       1       aFT     _FINAL

Using a regime and prescription, reporting acres treated relies only on action-based outputs and so your model runs faster.

What About Reforestation?

The poll numbers are interesting for a few reasons.

Two-thirds of the respondents have specific planting actions in their models. From a conceptual modeling perspective, this implies that there are alternatives for each harvested area (timing, species, planting density, etc.).

No one simply attributes reforestation acres and costs to harvest actions. I know there are many Woodstock users in Canada, and few of them use 1-year planning periods. Again, this implies reforestation alternatives for each harvested area.

Roughly one-third of the respondents are using regimes to model reforestation. (Disclosure: respondents work for client organizations for which I developed models recently).

Over the last 20 years, I've witnessed a shift from silvicultural evaluation models to highly prescriptive models. By this I mean that 20 years ago, a lot of the models I built evaluated multiple planting densities for each harvested area. As a result, there needed to be multiple planting actions to represent the alternatives. And because planting was a choice, there needed to be constraints to enforce reforestation policies.

These days, however, while there may be different reforestation prescriptions across the forest, there is generally only one prescription for each harvest area. And in that case, there really isn't a need to have separate decision variables for planting. Instead, one or prescriptions in a reforestation regime that begins with final harvest is all that is required. Fewer decision variables = faster matrix generation and solution. And an added bonus is that the reforestation constraints may not be required either.

So Why Haven't Things Changed?

While I was at Remsoft, we always taught reforestation as a distinct activity from harvesting in the Modeling Fundamentals course. I suspect that many of the analysts we taught just replicated that conceptual modeling framework in the models they developed. And my experience has been that organizations rarely revamp models once developed.

Because models are rarely scrapped and rebuilt, I suspect most analysts these days inherited models built by some predecessor (who probably has now left the organization). There is never time to redesign a model so the same structure persists, with annual updates to the AREAS file (to account for depletions) and yield tables.

Ready for a Change? Contact Me!

If your models haven't been updated in a while, maybe it is time to rethink things. Together, we can design a new conceptual framework that is efficient and easier to debug if problems arise. And if you'd like to apply some targeted training (for say, Regimes or conceptual modeling) we can do that too!

Sunday, April 21, 2024

What To Do When Things Go Wrong

Introduction

I’ve been using Woodstock for a long time… a very, VERY long time. So, you might think I never get stumped or encounter oddball results. And you would be wrong. One of the things I miss most about working in a team environment is getting help from a colleague on a Woodstock model that is giving me grief. I recall working on a model build project for a client. The land base was relatively small but there were lots of details. This model required the use of the ALLOCATION and REGIMES modules, and the OUTPUTS section was quite extensive. A second set of eyes could have saved me hours of frustration.

The model tracked a lot of silvicultural costs, and many of those costs occurred in the same planning period. Since this was a new model build, I could define my own outputs rather than using inherited code. I did not want to simply create outputs for each one and then sum them because all those nodes will slow down matrix generation. Instead, I relied on summary coefficients in the YIELDS section to streamline the OUTPUTS section. The resulting matrix generation was very quick. As a test, I compared the values of the client summary outputs to my new outputs based on summary yield coefficients. Not surprisingly, I got the same answers and so I sent off the initial model to the client for review. I got back a simple question.

Why is the Objective Function Value Different than the Calculated NPV?

Sure enough, the objective function value (ordREV – ocdCOST) from the solver was about $5000 higher than my calculated NPV. I reviewed all of my outputs and verified that each one was triggered by the correct action. I also made sure that the yield coefficients were also correct. However, I did not find any errors and my attempts using various rewrites of the output specifications yielded no new insights. I was stumped. So I thought back on my process to see where things stopped working.

I ran one-line AREAS files to test various regime combinations, and these all produced consistent results between the objective function and output calculation. Next, I wrote a constraint to force the choice of a different prescription, and that too worked as expected. It was only when I incorporated a forced activity in the LPSCHEDULE that the results differed. The actions were carried out properly but deliveries in the AO Detailed report looked odd – all of the forced activities that were part of REGIMES had no development types associated with them.

I sent my simplified model off to Remsoft technical support and within a day I received back an email from Ugo. It turns out that there was a bug in Woodstock. I had created the LPSCHEDULE section using the Spatial Woodstock menu option and it wrote the LPSCHEDULE in the newer “Treatments” syntax:

.MASK() Age Area RegimeCode(Period,_RX(RxCode),Transition#)

Woodstock did not handle deliveries properly from the LPSCHEDULE, hence the different objective function values. As a workaround, Ugo told me that if I used the older “Stanley” syntax, the LPSCHEDULE would be processed properly:

.MASK() Age Area RegimeCode Period _RX(RxCode)

Ugo also told me that the bug had been fixed in the subsequent release. Problem solved! But the experience taught me a few important lessons.

Lessons Learned

Don’t waste a lot of time on something that seems to be eluding you. If you have a colleague you can ask for help, do it. Often, they can spot a problem in code that your brain refuses to see. It is also better than thinking you’ve gone insane.

Check ALL your work. I should have spotted the objective function issue before sending it to the client. But I assumed that the model was working properly because it had worked fine before I added any constraints (i.e., LPSCHEDULE).

Don’t assume it is necessarily your fault - software bugs are a fact of life. Once I identified the issue with my model, I created a small instance of the model that replicated the problem and sent it off to Remsoft technical support with a detailed explanation of the problem. That will get you faster solutions than if you just send a large, complicated model with no explanations as to what the problem is or what you have tried.

Do You Need a Second Pair of Eyes? Contact Me!

If your model is misbehaving (calculation errors, memory errors, etc.) then by all means contact Remsoft tech support. Otherwise, if your model is slow, or you want to implement something new and you are not sure how to do it, give me a shout. There are several alternative engagements we could undertake to keep you and your team productive.

Monday, April 15, 2024

Improve Model Efficiency - Part 3

Think Carefully About Outputs

When I am contacted about potential work, the problem is often related to slow Woodstock model performance. While performance can be related to either matrix generation and solution, or schedule execution and report writing, most of the concern is for the former. In both cases, however, the root cause is usually the same: the OUTPUTS section. Poor performance can be due to a number of factors, including:

  1. the type of outputs that are defined,
  2. the number of outputs in the model, and
  3. the level of detail required in reports.

Let’s consider each of these factors.

Output Types

All Woodstock modelers know that there are two main types of outputs: action-based and inventory-based. Action-based outputs are those that are triggered by an action you’ve defined. During matrix generation, Woodstock enumerates the timing choices associated actions, and the total number of output coefficients going into the matrix is simply the # of timing choices times the number of yield coefficients to be reported.

With inventory-based outputs, there is no action trigger. Rather, the matrix generation enumerates every development type class that potentially exists in each planning period, and the total number of output coefficients going into the matrix is the # of development type classes times the number of yield coefficients associated with each development type. While both timing choices and development type classes increase in number as each period is evaluated, there are always fewer timing choices than development type classes. As a result, the execution time devoted to inventory-based outputs will be significantly more than action-based outputs.

Output Nodes

Another factor to consider for both types of outputs is the # nodes associated with them. An output node simply refers to the number of triggering action-yield coefficient combinations needed to report the output properly. Consider this syntax:

*OUTPUT oaCC Clearcut area (ac)   
*SOURCE .MASK() aCC _AREA
*OUTPUT oaTH Thin area (ac)
*SOURCE .MASK() aTH _AREA
*OUTPUT oaHARV Harvest area (ac)
*SOURCE oaCC + oaTH

The node number associated with the first two outputs is 1 because there is just a single triggering action and a single yield coefficient (_AREA = 1). For the output oaHARV, the node number is 2 because you add the two nodes from the summed outputs. But things get much more complicated when you start dealing with prices and costs:

*OUTPUT ocSP Site prep cost ($)
*SOURCE .MASK() rCCPL(aSP) ycSP
… 
*OUTPUT ocPLT Planting cost ($)
*SOURCE .MASK() rCCPL(aPL) ycPLT
…
*OUTPUT ocRLW Woody release ($)
*SOURCE .MASK() rCCPL(aRLW) ycRLW
*OUTPUT ocSILV Silviculture costs ($)
*SOURCE ocSPC + ocSPB + ocSPM + ocPLT + ocINT + ocRLC + ocRLW

Clearly, the number of nodes can be much higher if there are multiple activities contributing to a single output. Harvest revenues are even worse, because they can originate with multiple harvest actions, with multiple volume coefficients and multiple prices. Unfortunately, if you have lots of harvest types and lots of products, your node numbers will be high.

Number Of Outputs

The sheer number of outputs in a model can be large, even without overkill. However, some of the worst offenders of model overkill typically include multiple versions of the same thing. For example, I’ve seen models with harvest volume reported by species, by ownership class, by tract, by age class, etc. These reporting outputs are usually not constrained and therefore have no impact on matrix generation. However, they can bring schedule execution and reporting to a crawl because each output has to calculated independently. Even worse, the same outputs may also appear in multiple reports (_ALL report plus customized reports).

Level of Detail

Most Woodstock modelers are aware of theme-based outputs. These can be very helpful when an output needs to be reported by species, tract, etc., because they reduce the overall number of outputs needed. But there is a tendency among many modelers to include redundant outputs. For example, I’ve seen many instances where an output is reported as a sum of outputs AND as an output of summed yield coefficients:

*OUTPUT oqTOT1 Total clearcut volume (m3)
*SOURCE .MASK() aCC yiPWD + .MASK() aCC yiCNS + .MASK() aCC yiSAW
*OUTPUT oqTOT2 Total clearcut volume (m3)
*SOURCE .MASK() aCC yiTOT

The first version is undesirable because of nodes anyway, but it really serves no purpose unless the sum in the YIELDS section is done incorrectly in the second version.

Tips To Improve Performance

  • Avoid inventory-based outputs wherever possible. If you use an inventory-based output to report pro-rated activities that are hard to predict, consider using REGIMES instead. For example, precommercial thinning is often used in young stands to correct overstocking. However, since we can’t really predict overstocking very well, we may assume that a fixed proportion of stands receive a PCT at a given age:

    *OUTPUT ocPCT PCT cost ($)
    *SOURCE .MASK() @AGE(16) _INVENT ycPCT

    This approach works and, to be honest, it was the only practical way to model PCT before the REGIMES module came along. But now, it is simpler and more efficient to use a Regime and Prescription to convert the inventory-based output to an action-based one:

    *PRESCRIPTION rxCCPL Clearcut, plant, commercial thin 
         _RXPERIOD _ACTION  _ENTRY   yAREA
             0       aCC   _INITIAL   1.00
             0       aSPC      -      1.00
             1       aPLT      -      1.00
             3       aINT      -      1.00
             6       aRLW      -      1.00
            16       aPCT   _FINAL    0.25


    Instead of relying on the age of the DevType to determine if a PCT should be done, the prescription states that PCT is done 15 periods after planting.
     
  • Avoid summary outputs. Where possible, use sums of coefficients in the YIELDS section to perform summary operations.

  • Avoid defining unnecessary outputs. Use theme-based outputs as needed but be judicious. For example, many modelers report on acres by age-class using outputs. This is totally unnecessary. The same information can be had from the built-in _CONDITION report. By design, this report details every development type class in every period. Using a Woodstock table filter, an SQL query or a pivot table in Excel, you can glean the same details without bringing your model execution to a crawl by reporting dozens of inventory-based outputs.

    Also, don’t use a theme-based output where there are many thematic attributes, but you only wish to constrain or report on just a few of them. Instead, define specific outputs for the constrained/reports cases. Otherwise, the matrix generator will inject all yield coefficients associated with each attribute into the matrix, bloating the number of non-zero elements, and slowing down your LP solver.

Want More Advice? Contact Me!

If you need a model audit or some in-house training to brush up your modeling skills, give me a shout. I’ll be happy to help.

Monday, April 1, 2024

Improve Model Efficiency - Part 2

Avoid Unintended Outcomes in Spatial Planning

Today, we will investigate unintended consequences in forest planning models. In a typical project, analysts work with a project team to learn what kinds of silviculture need to be modeled. Once established, the silviculturist and biometrician will establish the operability criteria for those actions. Often, the team will also provide you with a list of stands that they plan to harvest in the first period or two. 

As you construct the model, you soon realize that some of these preplanned activities break the rules. For example, there is a 13-year-old stand scheduled for thinning, despite the rule requiring that commercial thinning be carried out between ages 15 and 20. Despite the frustration it causes, this type of thing happens all the time. It is just a matter of extenuating circumstances – things that models are ill-equipped to handle at face value.

So, how do you handle this as an analyst? If you lower the threshold for thinning to 13 instead of 15, your problem goes away, but a new one takes its place. You will generate many more potential timing choices without corresponding yield tables for them. To avoid that, you can restrict them with an additional volume requirement. But that is somewhat inefficient and violates the intent to thin at ages 15 or older, in general.

Special Actions and Prescriptions

Instead, you can create a new variant of the action, which is only available in the first planning period. There are two ways to ensure that it is only available in period 1. You can use _CP = 1 in the operability statement, or you can use *EXCLUDE in the OPTIMIZE section to exclude the special action from being generated in periods 2 onward.

Of the two approaches, I prefer the first because you don’t need a new action code. If you are precise in your operability statement, only the stand that has been identified for harvest is affected. And there’s the rub: how do you know that you targeted a particular stand (polygon)? Here’s an example:

Stands Layer with Stand-IDs Labeled

Harvest Units Layer with Stand-IDs Labeled

In the harvest units layer, look for harvest units 215 and 225 (lime green), scheduled for the first planning period. You also notice that they are both from the same stand 5977 (red). Technically, they are not eligible for harvest under the standard clearfell operability, so you write a special *OPERABLE statement to allow those blocks to be harvested. Here is the code:

LANDSCAPE
*THEME StandID
5977
5978
5980
5981
5982
6626
6638
6639
*AGGREGATE CCP1
5977 6639

ACTIONS
*ACTION aCC Y Clearcut
*OPERABLE aCC
? ? ? ? ? ? ? ? ? CCP1 _CP = 1

Sure enough, the units 215 and 225 are harvested in period 1. But you’ve also generated harvest decision variables for units 10 and 1416, because they are also part of the same stand. This is not what was intended – only units 215 and 225 were supposed to be harvested.

Understanding Forced Choices

Remember that Woodstock does not schedule based on geographic information. It only sees pools of area that are eligible to be harvested. Even using an LPSchedule file does not necessarily mean that a particular polygon is harvested. True, the specified area associated with the development type is treated, regardless of any *OPERABLE statement. But technically, the solution only guarantees that the right number of acres is harvested. If you truly want to guarantee a particular polygon is harvested, you need to identify it within the landscape section somehow.

From our example above, a simple fix is to include a theme that identifies forced choices (e.g., CC for clearcuts, TH for commercial thins, NA for not applicable). Then, you could explicitly write the operable statement as follows:

LANDSCAPE
*THEME StandID
5977
5978
5980
5981
5982
6626
6638
6639 
*THEME Planned Activities
CC
TH
NA

ACTIONS
*ACTION aCC Y Clearcut
*OPERABLE aCC
? ? ? ? ? ? ? ? ? 5977 CC _CP = 1

Because the last thematic attribute is CC for harvest units 215 and 225, they are eligible for harvest. The other parts of stand 5977 do not have CC in the last theme and are not eligible for harvest. Since no other polygons have the combination of standID = 5977 and planned activity = CC, you know that only those explicit polygons are harvested.

Another approach might be to include harvest units as a theme. While this will work, you are dramatically increasing the number of development types. Unless you have a reason to include ALL planning units, this may be overkill. Remember, your goal in modeling is efficiency!

Summary

The main takeaway from this example is to be precise in your operability specifications to avoid unintended consequences. Don’t be misled by the idea that a stand-based or unit-based harvest model is necessarily equivalent to particular polygons. Unless there is a 1:1 correspondence between polygons and development type classes, there may be alternative optimal solutions that include other polygons. When you map out the solution, it may not meet your expectations.

Struggling With Spatial Planning Issues? Contact Me!

If you’d like a new set of eyes to look over your model(s), give me a shout to schedule a model audit or an on-site training review. We can use your own models to ensure concepts are relevant and suggestions are immediately useful to your team.










What is the Big-M Method, and Why Should I Care?

The  Big-M method  is a common modeling technique used in  Mixed Integer Programming (MIP)  to represent  logical conditions  or  implicatio...