|
1 | 1 | #' buckHpr |
2 | 2 | #' |
3 | | -#' Calculate optimal bucking for all stems in a hpr file |
| 3 | +#' Optimal bucking for all stems in an HPR file using \code{buckStem()}. |
4 | 4 | #' |
5 | | -#' @param XMLNode ouput from getXMLNode() |
6 | | -#' @param PriceMatrices list of price matrices for all ProductKeys (getPriceMatrices()) |
7 | | -#' @param ProductData Matrix containing product data (getProductData()) |
8 | | -#' @param StemProfile Stem profiles for all stems in hprfile (getStemProfile()) |
9 | | -#' @param PermittedGrades list with the same lenght of assortments, each element containing the stemgrades allowed in each assortment (getPermittedGrades()) |
10 | | -#' @param SpeciesGroupDefinition getSpeciesGroupDefinition() |
11 | | -#' @return result structure with optimum bucking solution for the stems in the input .hpr file |
12 | | -#' @seealso getPermittedGrades, getPriceMatrices, getProductData |
| 5 | +#' @param XMLNode Output from \code{getXMLNode()}. |
| 6 | +#' @param PriceMatrices List of price matrices for all ProductKeys (from \code{getPriceMatrices()}). |
| 7 | +#' @param ProductData Data frame or data.table with product definitions (from \code{getProductData()}). |
| 8 | +#' @param StemProfile Stem profiles for all stems (from \code{getStemprofile()} / \code{getStemProfile()}). |
| 9 | +#' @param PermittedGrades Named list where names are ProductKeys and each element defines permitted stem grades |
| 10 | +#' (from \code{getPermittedGrades()}). |
| 11 | +#' @param SpeciesGroupDefinition Species group definitions (from \code{getSpeciesGroupDefinition()}). |
| 12 | +#' |
| 13 | +#' @return A \code{data.table} with the bucking solution for all processed stems, row-bound across stems. |
| 14 | +#' |
| 15 | +#' @seealso \code{\link{buckStem}}, \code{\link{getPermittedGrades}}, \code{\link{getPriceMatrices}}, \code{\link{getProductData}} |
13 | 16 | #' @author Lennart Noordermeer \email{lennart.noordermeer@nmbu.no} |
14 | 17 | #' @references Skogforsk 2011. Introduction to StanForD 2010. URL: Skogforsk. https://www.skogforsk.se/contentassets/1a68cdce4af1462ead048b7a5ef1cc06/stanford-2010-introduction-150826.pdf |
15 | 18 | #' @export |
| 19 | +buckHpr <- function(XMLNode, |
| 20 | + PriceMatrices, |
| 21 | + ProductData, |
| 22 | + StemProfile, |
| 23 | + PermittedGrades, |
| 24 | + SpeciesGroupDefinition) { |
16 | 25 |
|
17 | | -buckHpr=function (XMLNode, PriceMatrices, ProductData, StemProfile, PermittedGrades, |
18 | | - SpeciesGroupDefinition) |
19 | | -{ |
20 | | - require(XML) |
21 | | - require(plyr) |
22 | | - require(dplyr) |
23 | | - require(data.table) |
| 26 | + if (!requireNamespace("XML", quietly = TRUE)) stop("Package 'XML' is required.") |
| 27 | + if (!requireNamespace("data.table", quietly = TRUE)) stop("Package 'data.table' is required.") |
24 | 28 |
|
25 | | - grdFinder = function(x) { |
26 | | - unique(StemGrade[idxstart:x]) |
27 | | - } |
28 | | - asoFinder = function(x) { |
29 | | - names(SGKG)[which(colSums(matrix(sapply(SGKG, |
30 | | - FUN = function(X) all(grd[[x]] %in% X)), ncol = length(SGKG))) > |
31 | | - 0)] |
32 | | - } |
33 | | - DiameterValueFinder = function(x) { |
34 | | - DiameterValue[vec[[x]]] |
35 | | - } |
36 | | - Rounder = function(x) { |
37 | | - res = round_any(DV[idx][[x]], 10, floor) |
38 | | - if (sum(idx) > 1) { |
39 | | - res |
40 | | - } |
41 | | - else { |
42 | | - list(res) |
43 | | - } |
44 | | - } |
45 | | - BarkFinder = function(x) { |
46 | | - BarkFunction(DV[idx][[x]], SpeciesGroupKey, SpeciesGroupDefinition, |
47 | | - Top_ob = tab[idx, ][x]$Top_ob, DBH = DBH, LogLength = tab[idx, |
48 | | - ][x]$LogLength) |
49 | | - } |
50 | | - rowFinder = function(x) sum(commercial$LogLength[x] >= |
51 | | - rownames[[x]] %>% as.numeric()) |
52 | | - colFinder = function(x) sum(commercial$topdiam[x] >= |
53 | | - colnames[[x]] %>% as.numeric()) |
54 | | - priceFinder = function(x) lis[[x]][row[x], col[x]] |
55 | | - seqVectozied = Vectorize(seq.default, vectorize.args = c("from", |
56 | | - "to")) |
57 | | - trackTrace = function(res, tt) { |
58 | | - low = min(tt[, "StartPos"]) |
59 | | - while (low > 0) { |
60 | | - id_previous = tt$CumulativeValue[order(tt$StartPos)[1]] - |
61 | | - tt$Value[order(tt$StartPos)[1]] |
62 | | - sub = res[res$StopPos == low, ] |
63 | | - prev = sub[which(near(sub$CumulativeValue, |
64 | | - id_previous)), ] |
65 | | - if (!is.vector(prev)) { |
66 | | - prev = prev[1, ] |
67 | | - } |
68 | | - tt = rbind(tt, prev) |
69 | | - low = min(tt$StartPos) |
70 | | - } |
71 | | - tt = tt[nrow(tt):1, ] |
72 | | - if (is.vector(tt)) { |
73 | | - tt[5] = ifelse(tt[5] == 0, 999999, tt[5]) |
74 | | - } |
75 | | - return(tt) |
| 29 | + ProductData <- ProductData[!is.na(ProductData$ProductName), ] |
| 30 | + |
| 31 | + # Stem order from XML (if available), otherwise from StemProfile |
| 32 | + stems_xml <- XMLNode[["Machine"]][names(XML::xmlSApply(XMLNode[["Machine"]], XML::xmlAttrs)) == "Stem"] |
| 33 | + if (length(stems_xml) > 0) { |
| 34 | + stem_keys <- vapply( |
| 35 | + stems_xml, |
| 36 | + function(s) as.integer(XML::xmlValue(s[["StemKey"]])), |
| 37 | + integer(1) |
| 38 | + ) |
| 39 | + } else { |
| 40 | + stem_keys <- unique(as.integer(StemProfile$StemKey)) |
76 | 41 | } |
77 | 42 |
|
78 | | - stems <- XMLNode[["Machine"]][names(xmlSApply(XMLNode[["Machine"]], |
79 | | - xmlAttrs)) == "Stem"] |
80 | | - pb <- txtProgressBar(min = 0, max = length(stems), style = 3, |
81 | | - width = 50, char = "=") |
82 | | - ProductData <- ProductData[!is.na(ProductData$ProductName),] |
83 | | - result <- list() |
84 | | - for (i in 1:length(stems)) { |
85 | | - StemKey <- SK <- as.integer(xmlValue(stems[[i]][["StemKey"]])) |
86 | | - stem <- StemProfile[StemProfile$StemKey == SK, ] |
87 | | - if (nrow(stem) > 0) { |
88 | | - diameterPosition <- as.numeric(stem$diameterPosition) |
89 | | - DiameterValue <- as.numeric(stem$DiameterValue) |
90 | | - StemGrade <- as.numeric(stem$StemGrade) |
91 | | - SpeciesGroupKey <- unique(stem$SpeciesGroupKey) |
92 | | - ProductKeys <- ProductData$ProductKey |
93 | | - LengthClassLowerLimit <- as.numeric(ProductData$LengthClassLowerLimit) |
94 | | - LengthClassMAX <- as.numeric(ProductData$LengthClassMAX) |
95 | | - DiameterClassLowerLimit <- as.numeric(ProductData$DiameterClassLowerLimit) |
96 | | - DiameterClassMAX <- as.numeric(ProductData$DiameterClassMAX) |
97 | | - VolumeDiameterCategory <- ProductData$VolumeDiameterCategory |
98 | | - DiameterTopPositions <- ProductData$DiameterTopPosition |
99 | | - DBH <- xmlValue(stems[[i]][["SingleTreeProcessedStem"]][["DBH"]]) %>% |
100 | | - as.numeric() |
| 43 | + StemProfileDT <- data.table::as.data.table(StemProfile) |
| 44 | + stems_list <- split(StemProfileDT, StemProfileDT$StemKey) |
101 | 45 |
|
102 | | - SeqStart = round_any(min(LengthClassLowerLimit[LengthClassLowerLimit > |
103 | | - 0]), 10) |
104 | | - SeqStop = ifelse(max(LengthClassMAX) < max(diameterPosition), |
105 | | - max(LengthClassMAX), max(diameterPosition)) |
106 | | - DiameterTopPositions = ProductData$DiameterTopPositions |
107 | | - bult = seq(10, 100, 10) |
108 | | - res = data.table(StartPos = -1, StopPos = 0, Top_ub = NA, |
109 | | - LogLength = NA, ProductKey = NA, Volume = 0, |
110 | | - Value = 0, CumulativeValue = 0) |
111 | | - if (SeqStart < SeqStop) { |
112 | | - SeqAsp = seq(SeqStart, SeqStop, 10) |
113 | | - StartPos = 0 |
114 | | - while (StartPos < max(diameterPosition) - min(LengthClassLowerLimit[LengthClassLowerLimit > |
115 | | - 0])) { |
116 | | - StartPos = sort(res$StopPos[!res$StopPos %in% |
117 | | - res$StartPos])[1] |
| 46 | + pb <- utils::txtProgressBar(min = 0, max = length(stem_keys), style = 3, width = 50, char = "=") |
| 47 | + on.exit(close(pb), add = TRUE) |
118 | 48 |
|
119 | | - if (StartPos == 0) { |
120 | | - StopPos = StartPos + c(bult, SeqAsp) |
121 | | - } else { |
122 | | - StopPos = StartPos + SeqAsp |
123 | | - } |
124 | | - StopPos = StopPos[StopPos <= max(diameterPosition) & |
125 | | - StopPos > 0] |
126 | | - if (length(StopPos) < 1) { |
127 | | - break |
128 | | - } |
129 | | - LogLength = StopPos - StartPos |
130 | | - rotdiam = DiameterValue[which(near(diameterPosition, |
131 | | - StartPos))] |
132 | | - idxstart = as.numeric(which(near(diameterPosition, |
133 | | - StartPos))) |
134 | | - idxstop = as.numeric(match(as.character(StopPos), |
135 | | - as.character(diameterPosition))) |
136 | | - grd = lapply(idxstop, grdFinder) |
137 | | - SGPK = ProductData$ProductKey[ProductData$SpeciesGroupKey == |
138 | | - SpeciesGroupKey[1]] |
139 | | - m = data.table(idxstart, idxstop, StartPos, |
140 | | - StopPos, LogLength, rotdiam) |
141 | | - m = m[m$StopPos <= max(diameterPosition), ] |
142 | | - SGKG = PermittedGrades[as.character(SGPK)] |
143 | | - asos = lapply(1:length(grd), asoFinder) |
144 | | - lapply(1:length(grd), function(x) { |
145 | | - names(SGKG)[which(colSums(matrix(sapply(SGKG, |
146 | | - FUN = function(X) all(grd[[x]] %in% X)), |
147 | | - ncol = length(SGKG))) > 0)] |
148 | | - }) |
149 | | - m$Price = 0 |
150 | | - r = rep(idxstop, len = sum(lengths(asos))) |
151 | | - r = r[order(r)] |
152 | | - tab = data.table(idxstop = r, ProductKey = unlist(asos)) |
153 | | - idx = match(as.character(tab$ProductKey), as.character(ProductData$ProductKey)) |
154 | | - tab = cbind(tab, data.table(DiameterUnderBark = ProductData$DiameterUnderBark[idx], |
155 | | - LengthClassLowerLimit = ProductData$LengthClassLowerLimit[idx], |
156 | | - LengthClassMAX = ProductData$LengthClassMAX[idx], |
157 | | - DiameterClassLowerLimit = ProductData$DiameterClassLowerLimit[idx], |
158 | | - DiameterClassMAX = ProductData$DiameterClassMAX[idx], |
159 | | - VolumeDiameterAdjustment = ProductData$VolumeDiameterAdjustment[idx], |
160 | | - VolumeDiameterCategory = ProductData$VolumeDiameterCategory[idx], |
161 | | - VolumeLengthCategory = ProductData$VolumeLengthCategory[idx], |
162 | | - DiameterTopPosition = as.numeric(ProductData$DiameterTopPositions[idx]))) |
163 | | - tab = merge(m, tab, "idxstop", allow.cartesian = TRUE) |
164 | | - tab$StopPosAdj = round((tab$StopPos - tab$DiameterTopPosition)/10) * |
165 | | - 10 |
166 | | - tab$Top_ob = DiameterValue[match(as.character(tab$StopPosAdj), |
167 | | - as.character(diameterPosition))] |
168 | | - tab$Top_ub = BarkFunction(tab$Top_ob, SpeciesGroupKey, |
169 | | - SpeciesGroupDefinition, Top_ob = Top_ob, |
170 | | - DBH = DBH, LogLength = LogLength) |
171 | | - tab$topdiam = ifelse(tab$DiameterUnderBark, |
172 | | - tab$Top_ub, tab$Top_ob) |
173 | | - tab = tab[tab$LogLength >= tab$LengthClassLowerLimit,] |
174 | | - tab = tab[tab$LogLength <= tab$LengthClassMAX,] |
175 | | - tab = tab[tab$topdiam > tab$DiameterClassLowerLimit,] |
176 | | - tab = tab[tab$rotdiam < tab$DiameterClassMAX,] |
177 | | - if (nrow(tab) > 0){ |
178 | | - commercial = tab[tab$ProductKey != "999999",] |
179 | | - if (nrow(commercial) > 0) { |
180 | | - lis = PriceMatrices[commercial$ProductKey] |
181 | | - rownames = lapply(lis, rownames) |
182 | | - colnames = lapply(lis, colnames) |
183 | | - row = sapply(1:length(commercial$LogLength), |
184 | | - rowFinder) |
185 | | - col = sapply(1:length(commercial$topdiam), |
186 | | - colFinder) |
187 | | - tab$Price[tab$ProductKey != "999999"] = sapply(1:length(lis), |
188 | | - priceFinder) |
189 | | - } |
190 | | - tab$idxstop[tab$VolumeLengthCategory == "Rounded downwards to nearest dm-module"] = match(as.character(round_any((tab$StopPos[tab$VolumeLengthCategory == |
191 | | - "Rounded downwards to nearest dm-module"]), |
192 | | - 10, f = floor)), as.character(diameterPosition)) |
193 | | - WithLengthClass = tab[tab$VolumeLengthCategory == |
194 | | - "Length as defined in LengthClasses" & |
195 | | - tab$ProductKey != "999999", ] |
196 | | - lis = LengthClasses[WithLengthClass$ProductKey] |
197 | | - if (nrow(WithLengthClass) > 0) { |
198 | | - l = 1 |
199 | | - for (l in 1:nrow(WithLengthClass)) { |
200 | | - LengthClass = LengthClasses[[WithLengthClass$ProductKey[l]]] |
201 | | - WithLengthClass$LogLength[l] = round_any(LengthClass[max(which(WithLengthClass$LogLength[l] >= |
202 | | - LengthClass))], 10, f = ceiling) |
203 | | - WithLengthClass$StopPos[l] = WithLengthClass$StartPos[l] + |
204 | | - WithLengthClass$LogLength[l] |
205 | | - WithLengthClass$idxstop[l] = which(diameterPosition == |
206 | | - paste(round_any(WithLengthClass$StopPos[l], |
207 | | - 10, f = ceiling))) |
208 | | - } |
209 | | - tab$LogLength[tab$VolumeLengthCategory == |
210 | | - "Length as defined in LengthClasses"] = WithLengthClass$LogLength |
211 | | - tab$StopPos[tab$VolumeLengthCategory == |
212 | | - "Length as defined in LengthClasses"] = WithLengthClass$StopPos |
213 | | - tab$idxstop[tab$VolumeLengthCategory == |
214 | | - "Length as defined in LengthClasses"] = WithLengthClass$idxstop |
215 | | - } |
216 | | - vec = seqVectozied(from = tab$idxstart, to = tab$idxstop, |
217 | | - by = 1) |
218 | | - DV = sapply(1:length(vec), DiameterValueFinder) |
219 | | - idx = tab$VolumeDiameterAdjustment == "Measured diameter rounded downwards to cm" |
220 | | - if (sum(idx) > 0) { |
221 | | - if (sum(idx) > 1) { |
222 | | - DV[which(idx > 0)] = lapply(1:sum(idx), |
223 | | - function(x) { |
224 | | - as.vector(sapply(1:length(DV[which(idx > |
225 | | - 0)[x]]), Rounder)) |
226 | | - }) |
227 | | - } |
228 | | - else { |
229 | | - DV[idx] = sapply(1:length(DV[idx]), Rounder) |
230 | | - } |
231 | | - } |
232 | | - idx = tab$DiameterUnderBark == T |
233 | | - if (sum(idx) > 0) { |
234 | | - DV[idx] = sapply(1:length(DV[idx]), BarkFinder) |
235 | | - } |
236 | | - RV = relist(unlist(DV)/2, skeleton = as.relistable(DV)) |
237 | | - if (!is.list(RV)) { |
238 | | - RV = list(RV) |
239 | | - } |
240 | | - tab$Volume = -1 |
241 | | - idx = tab$VolumeDiameterCategory == "All diameters (solid volume)" |
242 | | - x = 2 |
243 | | - tab$Volume[idx] = sapply(1:length(RV), function(x) sum(pi * |
244 | | - (unlist(RV[x])^2) * 10)/1e+08)[idx] |
245 | | - idx = tab$VolumeDiameterCategory == "Calculated Norwegian mid" |
246 | | - Dmid = tab$Top_ub + (tab$LogLength/2 * 0.1) + |
247 | | - 0.5 |
248 | | - tab$Volume[idx] = ((((Dmid/100) * (Dmid/100)) * |
249 | | - pi/4 * (tab$LogLength/10)) * 0.001)[idx] |
250 | | - idx = tab$VolumeDiameterCategory == "Top" & |
251 | | - tab$DiameterUnderBark == T |
252 | | - r1 = tab$Top_ub/2 |
253 | | - r2 = (tab$Top_ub + tab$LogLength * 0.01)/2 |
254 | | - tab$Volume[idx] = (((1/3) * pi * (r1^2 + |
255 | | - r2^2 + (r1 * r2)) * tab$LogLength)/1e+08)[idx] |
256 | | - idx = tab$VolumeDiameterCategory == "Top" & |
257 | | - tab$DiameterUnderBark == F |
258 | | - r1 = tab$Top_ob/2 |
259 | | - r2 = (tab$Top_ob + tab$LogLength * 0.01)/2 |
260 | | - tab$Volume[idx] = (((1/3) * pi * (r1^2 + |
261 | | - r2^2 + (r1 * r2)) * tab$LogLength)/1e+08)[idx] |
262 | | - tab$Value = tab$Volume * tab$Price |
263 | | - head(tab) |
264 | | - m = tab[, c("StartPos", "StopPos", "Top_ub", |
265 | | - "LogLength", "ProductKey", "Volume", "Value")] |
266 | | - idx=paste(res$StopPos)==paste(StartPos) |
267 | | - sub = res[idx,] |
| 49 | + result <- vector("list", length(stem_keys)) |
268 | 50 |
|
269 | | - CumulativeValue = ifelse(nrow(sub)>0&any(!is.na(sub$CumulativeValue)), |
270 | | - max(sub$CumulativeValue,na.rm = T), |
271 | | - 0) |
272 | | - m$CumulativeValue = m$Value + CumulativeValue |
273 | | - }else { |
274 | | - m = data.table(StartPos = StartPos, StopPos = StopPos, |
275 | | - Top_ub = NA, LogLength = NA, ProductKey = NA, |
276 | | - Volume = NA, Value = NA, CumulativeValue = NA) |
277 | | - } |
278 | | - res = rbindlist(list(res, m)) |
| 51 | + for (i in seq_along(stem_keys)) { |
| 52 | + SK <- stem_keys[i] |
| 53 | + stem <- stems_list[[as.character(SK)]] |
| 54 | + |
| 55 | + if (!is.null(stem) && nrow(stem) > 0) { |
| 56 | + sol <- tryCatch( |
| 57 | + buckStem( |
| 58 | + stem = stem, |
| 59 | + ProductData = ProductData, |
| 60 | + PriceMatrices = PriceMatrices, |
| 61 | + PermittedGrades = PermittedGrades, |
| 62 | + StemKey = as.integer(SK) |
| 63 | + ), |
| 64 | + error = function(e) { |
| 65 | + warning(sprintf("buckStem failed for StemKey %s: %s", SK, conditionMessage(e)), call. = FALSE) |
| 66 | + NULL |
279 | 67 | } |
280 | | - } |
281 | | - res = res[!is.na(res$LogLength),] |
282 | | - tt = res[which.max(res$CumulativeValue),] |
283 | | - if (nrow(tt) == 1){ |
284 | | - res = trackTrace(res, tt) |
285 | | - }else{ |
286 | | - res = data.table(StartPos = NA, StopPos = NA, |
287 | | - Top_ub = NA, LogLength = 1, ProductKey = NA, |
288 | | - Volume = NA, Value = 0, CumulativeValue = 0) |
289 | | - } |
290 | | - res = cbind(1:nrow(res), res) |
291 | | - colnames(res)[1] = "LogKey" |
292 | | - out <- cbind(rep(StemKey, nrow(res)), res) |
293 | | - colnames(out)[1] <- c("StemKey") |
294 | | - result[[i]] <- out |
| 68 | + ) |
| 69 | + result[[i]] <- sol |
| 70 | + } else { |
| 71 | + result[[i]] <- NULL |
295 | 72 | } |
296 | | - setTxtProgressBar(pb, i) |
297 | | - print(i) |
| 73 | + |
| 74 | + utils::setTxtProgressBar(pb, i) |
298 | 75 | } |
299 | | - result <- do.call(rbind.data.frame, result) |
300 | | - close(pb) |
301 | | - return(result) |
302 | | -} |
303 | 76 |
|
| 77 | + out <- data.table::rbindlist(result, fill = TRUE) |
| 78 | + return(out) |
| 79 | +} |
0 commit comments