www/includes/formulas.php

changeset 85
ca7a37586551
parent 83
85521c6e0022
child 87
7f1d0abe5571
equal deleted inserted replaced
84:3e5e87f1818d 85:ca7a37586551
107 107
108 return array($R[$i],$G[$i],$B[$i]); 108 return array($R[$i],$G[$i],$B[$i]);
109 } 109 }
110 110
111 111
112
113 function sg_to_plato($sg) {
114 if ($sg > 0.5)
115 return 259 - 259 / $sg;
116 return 0;
117 }
118
119
120
121 function plato_to_sg($plato) {
122 if ($plato < 259)
123 return 259 / (259 - $plato);
124 return 1.000;
125 }
126
127
128
129 /*
130
131 Brouwhulp data.pas
132
133 Function THop.FlavourContribution : double; //in % * concentration in g/l
134 var bt, vol : double;
135 begin
136 bt:= FTime.Value;
137 vol:= 0;
138 if FRecipe <> NIL then vol:= FRecipe.BatchSize.Value;
139 if FUse = huFirstWort then Result:= 0.15 * FAmount.Value * 1000 //assume 15% flavourcontribution for fwh
140 else if bt > 50 then Result:= 0.10 * FAmount.Value * 1000 //assume 10% flavourcontribution as a minimum
141 else
142 begin
143 Result:= 15.25 / (6 * sqrt(2 * PI)) * Exp(-0.5*Power((bt-21)/6, 2))
144 * FAmount.Value * 1000;
145 if result < 0.10 * FAmount.Value * 1000 then
146 Result:= 0.10 * FAmount.Value * 1000 //assume 10% flavourcontribution as a minimum
147 end;
148 if vol > 0 then Result:= Result / vol;
149 end;
150
151 Function THop.AromaContribution : double; //in % * concentration in g/l
152 var bt, vol : double;
153 begin
154 bt:= FTime.Value;
155 vol:= 0;
156 if FRecipe <> NIL then vol:= FRecipe.BatchSize.Value;
157 if bt > 20 then Result:= 0
158 else if bt > 7.5 then
159 Result:= 10.03 / (4 * sqrt(2 * PI)) * Exp(-0.5*Power((bt-7.5)/4, 2))
160 * FAmount.Value * 1000
161 else if FUse = huBoil then Result:= FAmount.Value * 1000
162 else if FUse = huAroma then Result:= 1.2 * FAmount.Value * 1000
163 else if FUse = huWhirlpool then Result:= 1.2 * FAmount.Value * 1000
164 else if FUse = huDryHop then Result:= 1.33 * FAmount.Value * 1000;
165 if vol > 0 then Result:= Result / vol;
166 end;
167
168
169 Procedure TFermentable.SetpHParameters(force : boolean);
170 var x, ebc : double;
171 begin
172 if Between(FDIpH.Value, -0.01, 0.01) or Between(FAcidTo57.Value, -0.1, 0.1) or force then
173 begin
174 ebc:= SRMtoEBC(FColor.Value);
175 case FGrainType of
176 gtBase, gtKilned:
177 begin
178 FDIpH.Value:= -0.0132 * ebc + 5.7605;
179 x:= 0.4278 * ebc - 1.8106;
180 FAcidTo57.Value:= x;
181 end;
182 gtRoast:
183 begin
184 FDIpH.Value:= 0.00018 * ebc + 4.558;
185 FAcidTo57.Value:= -0.0176 * ebc + 60.04;
186 end;
187 gtCrystal:
188 begin
189 FDIpH.Value:= -0.0019 * ebc + 5.2175;
190 FAcidTo57.Value:= 0.132 * ebc + 14.277;
191 end;
192 gtSour:
193 begin
194 FDIpH.Value:= 3.44;
195 FAcidTo57.Value:= 337;
196 end;
197 gtSpecial: //this could be anything. Assume for now it is a non-acidulated base or kilned malt
198 begin
199 FDIpH.Value:= -0.0132 * ebc + 5.7605;
200 FAcidTo57.Value:= 0.4278 * ebc - 1.8106;
201 end;
202 end;
203 end;
204 //known parameters should be filled in here
205 if FSupplier.Value = 'Weyermann' then
206 begin
207 if FName.Value = 'Vienna mout' then
208 begin
209 FDIpH.Value:= 5.65;
210 FAcidTo57.Value:= 1.6;
211 end;
212 if FName.Value = 'Münchner I' then
213 begin
214 FDIpH.Value:= 5.44;
215 FAcidTo57.Value:= 8.4;
216 end;
217 if FName.Value = 'Münchner II' then
218 begin
219 FDIpH.Value:= 5.54;
220 FAcidTo57.Value:= 5.6;
221 end;
222 if FName.Value = 'Caramunich I' then
223 begin
224 FDIpH.Value:= 5.1;
225 FAcidTo57.Value:= 22.4;
226 end;
227 if FName.Value = 'Caramunich II' then
228 begin
229 FDIpH.Value:= 4.71;
230 FAcidTo57.Value:= 49;
231 end;
232 if FName.Value = 'Caramunich III' then
233 begin
234 FDIpH.Value:= 4.92;
235 FAcidTo57.Value:= 31.2;
236 end;
237 if FName.Value = 'Cara-aroma' then
238 begin
239 FDIpH.Value:= 4.48;
240 FAcidTo57.Value:= 74.4;
241 end;
242 if FName.Value = 'Carafa I' then
243 begin
244 FDIpH.Value:= 4.71;
245 FAcidTo57.Value:= 42;
246 end;
247 if FName.Value = 'Carafa III' then
248 begin
249 FDIpH.Value:= 4.81;
250 FAcidTo57.Value:= 35.4;
251 end;
252 if FName.Value = 'Carafa II' then
253 begin
254 FDIpH.Value:= 4.76;
255 FAcidTo57.Value:= 38.7;
256 end;
257 if FName.Value = 'Carafa Special I' then
258 begin
259 FDIpH.Value:= 4.73;
260 FAcidTo57.Value:= 46.4;
261 end;
262 if FName.Value = 'Carafa Special II' then
263 begin
264 FDIpH.Value:= 4.78;
265 FAcidTo57.Value:= 42.9;
266 end;
267 if FName.Value = 'Carafa Special III' then
268 begin
269 FDIpH.Value:= 4.83;
270 FAcidTo57.Value:= 38.9;
271 end;
272 if IsInString(FName.Value, 'Zuurmout') then
273 begin
274 FDIpH.Value:= 3.44;
275 FAcidTo57.Value:= 358.2;
276 end;
277 end;
278 end;
279
280
281 function TFermentable.GetExtract: double;
282 begin
283 Result := 0;
284 if FRecipe <> nil then
285 begin
286 Result := FAmount.Value * FYield.Value / 100 * (1 - FMoisture.Value / 100);
287 if FAdded = atMash then
288 Result := Result * FRecipe.Efficiency / 100;
289 end;
290 end;
291
292
293 function TFermentable.GetKolbachIndex: double;
294 begin
295 if FProtein.Value > 0 then
296 Result := FDissolvedProtein.Value / FProtein.Value
297 else
298 Result := 0;
299 end;
300
301
302 Procedure TWater.AddMinerals(Ca, Mg, Na, HCO3, Cl, SO4 : double);
303 begin
304 FCalcium.Add(Ca);
305 FMagnesium.Add(Mg);
306 FSodium.Add(Na);
307 FBicarbonate.Add(HCO3);
308 FChloride.Add(Cl);
309 FSulfate.Add(SO4);
310 end;
311
312 function TWater.GetResidualAlkalinity: double;
313 begin
314 //Result in mg/l as CaCO3
315 Result:= FTotalAlkalinity.Value - (FCalcium.Value / 1.4 + FMagnesium.Value / 1.7);
316 end;
317
318 const
319 Ka1 = 0.0000004445;
320 Ka2 = 0.0000000000468;
321
322 Function PartCO3(pH : double) : double;
323 var H : double;
324 begin
325 H:= Power(10, -pH);
326 Result:= 100 * Ka1 * Ka2 / (H*H + H * Ka1 + Ka1 * Ka2);
327 end;
328
329 Function PartHCO3(pH : double) : double;
330 var H : double;
331 begin
332 H:= Power(10, -pH);
333 Result:= 100 * Ka1 * H / (H*H + H * Ka1 + Ka1 * Ka2);
334 end;
335
336 Function PartH2CO3(pH : double) : double;
337 var H : double;
338 begin
339 H:= Power(10, -pH);
340 Result:= 100 * H * H / (H*H + H * Ka1 + Ka1 * Ka2);
341 end;
342
343 Function Charge(pH : double) : double;
344 begin
345 Result:= (-2 * PartCO3(pH) - PartHCO3(pH));
346 end;
347
348 //Z alkalinity is the amount of acid (in mEq/l) needed to bring water to the target pH (Z pH)
349 Function TWater.ZAlkalinity(pHZ : double) : double; //in mEq/l
350 var CT, DeltaCNaught, DeltaCZ, C43, Cw, Cz : double;
351 begin
352 C43:= Charge(4.3);
353 Cw:= Charge(FpH.Value);
354 Cz:= Charge(pHz);
355 DeltaCNaught:= -C43+Cw;
356 CT:= GetAlkalinity / 50 / DeltaCNaught;
357 DeltaCZ:= -Cz+Cw;
358 Result:= CT * DeltaCZ;
359 end;
360
361 //Z Residual alkalinity is the amount of acid (in mEq/l) needed to bring the water in the mash to the target pH (Z pH)
362 Function TWater.ZRA(pHZ : double) : double; //in mEq/l
363 var Calc, Magn, Z : double;
364 begin
365 Calc:= FCalcium.Value / (MMCa / 2);
366 Magn:= FMagnesium.Value / (MMMg / 2);
367 Z:= ZAlkalinity(pHZ);
368 Result:= Z - (Calc / 3.5 + Magn / 7);
369 end;
370
371 Function TWater.ProtonDeficit(pHZ : double) : double;
372 var i : integer;
373 F : TFermentable;
374 x : double;
375 begin
376 Result:= ZRA(pHZ) * FAmount.Value;
377 //proton deficit for the added malts
378 for i:= 0 to FRecipe.NumFermentables - 1 do
379 begin
380 F:= FRecipe.Fermentable[i];
381 if (F.AddedType = atMash) and (F.GrainType <> gtNone) then
382 begin
383 x:= F.AcidRequired(pHZ) * F.Amount.Value;
384 Result:= Result + x;
385 end;
386 end;
387 end;
388
389 Function TWater.MashpH : double;
390 var n : integer;
391 pd : double;
392 pH, deltapH, deltapd : double;
393 begin
394 Result:= 0;
395 n:= 0;
396 pH:= 5.4;
397 deltapH:= 0.001;
398 deltapd:= 0.1;
399 pd:= ProtonDeficit(pH);
400 while ((pd < -deltapd) or (pd > deltapd)) and (n < 1000) do
401 begin
402 inc(n);
403 if pd < -deltapd then ph:= ph - deltapH
404 else if pd > deltapd then pH:= pH + deltapH;
405 pd:= ProtonDeficit(pH);
406 end;
407 Result:= pH;
408 end;
409
410 Function TWater.MashpH2(PrDef : double) : double;
411 var n : integer;
412 pd : double;
413 pH, deltapH, deltapd : double;
414 begin
415 Result:= 0;
416 n:= 0;
417 pH:= 5.4;
418 deltapH:= 0.001;
419 deltapd:= 0.1;
420 pd:= ProtonDeficit(pH);
421 while ((pd < PrDef-deltapd) or (pd > PrDef + deltapd)) and (n < 1000) do
422 begin
423 inc(n);
424 if pd < PrDef-deltapd then ph:= ph - deltapH
425 else if pd > PrDef+deltapd then pH:= pH + deltapH;
426 pd:= ProtonDeficit(pH);
427 end;
428 Result:= pH;
429 end;
430
431 function TWater.GetAlkalinity: double;
432 begin
433 Result := FBicarbonate.Value / 1.22; //mEq/l
434 end;
435
436 function TWater.GetHardness: double;
437 begin
438 Result := 0.14 * FCalcium.Value - 0.23 * FMagnesium.Value;
439 end;
440
441 function TWater.GetEstPhMash: double;
442 {var
443 pHdemi, S: double;}
444 begin
445 Result:= MashpH;
446 { Result := 0;
447 if FRecipe <> nil then
448 begin
449 pHDemi := FRecipe.pHdemi;
450 S := 0.013 * FRecipe.MashThickness + 0.013;
451 Result := pHDemi + ResidualAlkalinity / 50 * S;
452 end;}
453 end;
454
455
456 function TBeerStyle.GetBUGUMin: double;
457 var
458 B, G: double;
459 begin
460 Result:= 0;
461 if ((FOGMax.Value + FOGMin.Value) > 0) and ((FIBUMax.Value + FIBUMin.Value) > 0) then
462 begin
463 G := (FOGMax.Value - FOGMin.Value) / ((FOGMax.Value + FOGMin.Value) / 2);
464 B := (FIBUMax.Value - FIBUMin.Value) / ((FIBUMax.Value + FIBUMin.Value) / 2);
465 if G > B then
466 Result := ((FIBUMin.Value + FIBUMax.Value) / 2) / (1000 * (FOGMax.Value - 1))
467 else
468 Result := FIBUMin.Value / (1000 * (((FOGMax.Value + FOGMin.Value) / 2) - 1));
469 end;
470 end;
471
472 function TBeerStyle.GetBUGUMax: double;
473 var
474 B, G: double;
475 begin
476 Result:= 0;
477 if ((FOGMax.Value + FOGMin.Value) > 0) and ((FIBUMax.Value + FIBUMin.Value) > 0)
478 and (FOGMin.Value > 1) then
479 begin
480 G := (FOGMax.Value - FOGMin.Value) / ((FOGMax.Value + FOGMin.Value) / 2);
481 B := (FIBUMax.Value - FIBUMin.Value) / ((FIBUMax.Value + FIBUMin.Value) / 2);
482 if G > B then
483 Result := ((FIBUMin.Value + FIBUMax.Value) / 2) / (1000 * (FOGMin.Value - 1))
484 else
485 Result := FIBUMax.Value / (1000 * (((FOGMax.Value + FOGMin.Value) / 2) - 1));
486 end;
487 end;
488
489
490 CalcOG;
491 CalcBitterness;
492
493 // Get concentration of ions in diluted brewwater (1) and target water (2) in mmol/l
494 Ca1 := W.Calcium.Value / MMCa;
495 Ca2 := W2.Calcium.Value / MMCa;
496 Mg1 := W.Magnesium.Value / MMMg;
497 Mg2 := W2.Magnesium.Value / MMMg;
498 Na1 := W.Sodium.Value / MMNa;
499 Na2 := W2.Sodium.Value / MMNa;
500
501 CO31 := W.Bicarbonate.Value / MMHCO3;
502 CO32 := W2.Bicarbonate.Value / MMHCO3;
503 SO41 := W.Sulfate.Value / MMSO4;
504 SO42 := W2.Sulfate.Value / MMSO4;
505 Cl1 := W.Sulfate.Value / MMSO4;
506 Cl2 := W2.Sulfate.Value / MMSO4;
507
508
509 procedure MixWater(W1, W2, Wr: TWater);
510
511 function Mix(V1, V2, C1, C2: double): double;
512 begin
513 if (V1 + V2) > 0 then
514 Result := (V1 * C1 + V2 * C2) / (V1 + V2)
515 else
516 Result := 0;
517 end;
518
519 var
520 vol1, vol2: double;
521 phnew: double;
522 begin
523 vol1 := W1.Amount.Value;
524 vol2 := W2.Amount.Value;
525 if (vol1 + vol2) > 0 then
526 begin
527 Wr.Amount.Value := vol1 + vol2;
528 Wr.Calcium.Value := Mix(vol1, vol2, W1.Calcium.Value, W2.Calcium.Value);
529 Wr.Magnesium.Value := Mix(vol1, vol2, W1.Magnesium.Value, W2.Magnesium.Value);
530 Wr.Sodium.Value := Mix(vol1, vol2, W1.Sodium.Value, W2.Sodium.Value);
531 Wr.Bicarbonate.Value := Mix(vol1, vol2, W1.Bicarbonate.Value, W2.Bicarbonate.Value);
532 Wr.Sulfate.Value := Mix(vol1, vol2, W1.Sulfate.Value, W2.Sulfate.Value);
533 Wr.Chloride.Value := Mix(vol1, vol2, W1.Chloride.Value, W2.Chloride.Value);
534 pHnew := -log10((power(10, -W1.pHWater.Value) * vol1 +
535 power(10, -W2.pHWater.Value) * vol2) / (vol1 + vol2));
536 Wr.pHwater.Value := pHnew;
537 end;
538 end;
539
540
541
542 function TRecipe.CalcColorWort : double;
543 var
544 i: integer;
545 F: TFermentable;
546 c, v: double;
547 begin
548 c := 0;
549 v := FBatchSize.Value;
550 if (v > 0) and (High(FFermentables) >= 0) then
551 begin
552 for i := Low(FFermentables) to High(FFermentables) do
553 begin
554 F := TFermentable(FFermentables[i]);
555 c := c + F.Amount.Value * F.Color.Value / v;
556 end;
557 c := c * 8.34436;
558 case FColorMethod of
559 cmMorey: c := 1.49 * Power(c, 0.69);
560 cmMosher: c := 0.3 * c + 4.7;
561 cmDaniels: c := 0.2 * c + 8.4;
562 end;
563 end;
564 Result:= c;
565 end;
566
567
568 procedure TRecipe.CalcWaterBalance;
569 var
570 i: integer;
571 F: TFermentable;
572 begin
573 FAbsorbedByGrain := 0;
574 for i := Low(FFermentables) to High(FFermentables) do
575 begin
576 F := TFermentable(FFermentables[i]);
577 if (F.FermentableType = ftGrain) or (F.FermentableType = ftAdjunct) then
578 FAbsorbedByGrain := FAbsorbedByGrain + F.Amount.Value;
579 end;
580 FAbsorbedByGrain := FAbsorbedByGrain * Settings.GrainAbsorption.Value;
581
582 if FEquipment.CalcBoilVolume.Value then
583 {FBoilSize.Value := FBatchSize.Value / (1 - (FEquipment.EvapRate.Value / 100) *
584 (FBoilTime.Value / 60));}
585 FBoilSize.Value:= FBatchSize.Value + FEquipment.BoilSize.Value
586 * FEquipment.EvapRate.Value / 100 *
587 (FBoilTime.Value / 60);
588 end;
589
590
591
592 procedure TRecipe.CalcOG;
593 var
594 i, j, k: integer;
595 v, v2, sg, d, tot, tot2, vol, vol1, vol2, sugF, sug, sug2, p, x: double;
596 mass1, mass2 : double;
597 F: TFermentable;
598 begin
599 for j := 1 to 1 do
600 begin
601 sug:= 0;
602 sugf:= 0;
603 sug2:= 0;
604 tot := 0;
605 tot2:= 0;
606 vol:= 0;
607 FEfficiency.Value := GetEfficiency;
608 for i := 0 to NumFermentables - 1 do
609 begin
610 F := TFermentable(Fermentable[i]);
611 if (F.AddedType = atMash) or (F.AddedType = atBoil) then
612 begin
613 d := F.Amount.Value * (F.Yield.Value / 100) * (1 - F.Moisture.Value / 100);
614 if (F.AddedType = atMash) then
615 d := FEfficiency.Value / 100 * d;
616 sugf := sugf + d;
617 tot := tot + F.Amount.Value;
618 end
619 else
620 begin
621 x:= (F.Yield.Value / 100) * (1 - F.Moisture.Value / 100);
622 sug2:= sug2 + F.Amount.Value * x;
623 tot2:= tot2 + F.Amount.Value;
624 tot := tot + F.Amount.Value;
625 vol:= vol + F.Amount.Value / (x * SugarDensity + (1 - x) * 1);
626 end;
627 end;
628 if tot > 0 then
629 for i := 0 to NumFermentables - 1 do
630 begin
631 F := Fermentable[i];
632 F.Percentage.Value := 100 * F.Amount.Value / tot;
633 end;
634
635 if (FEquipment <> NIL) and (FBatchSize.Value > 0) then
636 begin
637 vol1:= FBatchSize.Value - FEquipment.TrubChillerLoss.Value;
638 vol2:= vol1 + FEquipment.TopUpWater.Value + vol;
639 sug:= sugf * vol1 / FBatchSize.Value; //kg
640 sug:= sug + sug2; //kg
641 if vol2 > 0 then
642 sug:= sug / vol2; //kg/l
643 p:= 100 * sug;
644 sg:= PlatoToSG(p);
645 for k:= 1 to 30 do
646 begin
647 if sg > 0 then
648 p := 100 * sug / sg; //deg. Plato
649 sg := PlatoToSG(p);
650 end;
651 FEstOG.Value:= sg;
652 end
653 else if FBatchSize.Value <> 0 then
654 begin
655 p := 100 * sugf / FBatchSize.Value; //deg. Plato
656 sg := PlatoToSG(p);
657 for k:= 1 to 20 do
658 begin
659 if sg > 0 then
660 p := 100 * sugf / (FBatchSize.Value * sg); //deg. Plato
661 sg := PlatoToSG(p);
662 end;
663 FEstOG.Value := sg;
664 end
665 else
666 FEstOG.Value := 1.0;
667 end;
668
669 CalcWaterBalance;
670 end;
671
672
673
674 procedure TRecipe.EstimateFG;
675 var
676 i: integer;
677 percS, percCara, BD, Att, AttBeer, sg: double;
678 Temp, TotTme: double;
679 Y: TYeast;
680 // Eq: TEquipment;
681 begin
682 percS := GetPercSugar;
683 //if PercS > 40 then PercS:= 0;
684 percCara := GetPercCrystalMalt;
685 if percCara > 50 then PercCara:= 0;
686 if (Mash <> nil) and (Mash.MashStep[0] <> nil) then
687 begin
688 BD := Mash.MashStep[0].WaterToGrainRatio;
689 BD:= Max(2, Min(5.5, BD));
690 Temp := Mash.AverageTemperature;
691 Temp:= Max(60, Min(72, Temp));
692 TotTme := Mash.TotalMashTime;
693 TotTme:= Max(20, Min(90, TotTme));
694 end
695 else
696 begin
697 BD := 3.5;
698 Temp := 67;
699 TotTme := 75;
700 end;
701 Y := Yeast[0];
702 if Y <> nil then
703 begin
704 Att := Y.Attenuation.Value;
705 if Att < 30 then Att:= 77;
706 end
707 else
708 Att := 77;
709 AttBeer := 0.00825 * Att + 0.00817 * BD - 0.00684 * Temp + 0.00026 *
710 TotTme - 0.00356 * PercCara + 0.00553 * PercS + 0.547;
711
712 { Eq := nil;
713 if FEquipment <> nil then
714 Eq := TEquipment(Equipments.FindByName(FEquipment.Name.Value));
715 if Eq <> nil then
716 AttBeer2 := Eq.EstimateFG(Att, BD, Temp, TotTme, PercCara, PercS);}
717
718 FEstFG.Value := 1 + (1 - AttBeer) * (FEstOG.Value - 1);
719 CalcOGFermenter;
720 if FOGFermenter.Value > 1.001 then
721 begin
722 sg:= FOGFermenter.Value;
723 FEstFG2.Value := 1 + (1 - AttBeer) * (sg - 1);
724 FEstABV.Value := ABVol(FEstOG.Value, FEstFG.Value);
725 end
726 else if FOG.Value > 1.001 then
727 begin
728 sg:= FOG.Value;
729 FEstFG2.Value := 1 + (1 - AttBeer) * (sg - 1);
730 FEstABV.Value := ABVol(FEstOG.Value, FEstFG.Value);
731 end
732 else
733 begin
734 FEstFG2.Value := 1 + (1 - AttBeer) * (FEstOG.Value - 1);
735 FEstABV.Value := ABVol(FEstOG.Value, FEstFG.Value);
736 end;
737 end;
738
739 Procedure TRecipe.CalcCalories;
740 var sug, alc, org, fig : double;
741 begin
742 if FOGFermenter.Value > 1.001 then org:= FOGFermenter.Value
743 else if FOG.Value > 1.001 then org:= FOG.Value
744 else org:= 0;
745 if FFG.Value > 0.999 then fig:= FFG.Value
746 else if FEstFG.Value > 1.000 then fig:= FEstFG.Value
747 else if FEstFG2.Value > 1.000 then fig:= FEstFG2.Value
748 else fig:= 0;
749 if (org > 0) and (fig > 0) then
750 begin
751 alc:= 1881.22 * fig * (org - fig) / (1.775 - org);
752 sug:= 3550 * fig * (0.1808 * org + 0.8192 * fig - 1.0004);
753 FCalories.Value:= (alc + sug) / (12 * 0.0295735296);
754 end
755 else FCalories.Value:= 0;
756 end;
757
758
759
760
761 */
762
112 ?> 763 ?>

mercurial