CA2415685C - Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler - Google Patents

Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler Download PDF

Info

Publication number
CA2415685C
CA2415685C CA002415685A CA2415685A CA2415685C CA 2415685 C CA2415685 C CA 2415685C CA 002415685 A CA002415685 A CA 002415685A CA 2415685 A CA2415685 A CA 2415685A CA 2415685 C CA2415685 C CA 2415685C
Authority
CA
Canada
Prior art keywords
phosphate
blr
idc
eid
boiler
Prior art date
Legal status (The legal status is an assumption and is not a legal conclusion. Google has not performed a legal analysis and makes no representation as to the accuracy of the status listed.)
Expired - Lifetime
Application number
CA002415685A
Other languages
French (fr)
Other versions
CA2415685A1 (en
Inventor
John C. Gunther
Scott M. Boyette
Eric A. Thungstrom
Norman B. Worrell
Paul R. Burgmayer
Current Assignee (The listed assignees may be inaccurate. Google has not performed a legal analysis and makes no representation or warranty as to the accuracy of the list.)
Suez WTS USA Inc
Original Assignee
BetzDearborn Inc
Priority date (The priority date is an assumption and is not a legal conclusion. Google has not performed a legal analysis and makes no representation as to the accuracy of the date listed.)
Filing date
Publication date
Priority claimed from US08/321,338 external-priority patent/US5696696A/en
Application filed by BetzDearborn Inc filed Critical BetzDearborn Inc
Publication of CA2415685A1 publication Critical patent/CA2415685A1/en
Application granted granted Critical
Publication of CA2415685C publication Critical patent/CA2415685C/en
Anticipated expiration legal-status Critical
Expired - Lifetime legal-status Critical Current

Links

Landscapes

  • Preventing Corrosion Or Incrustation Of Metals (AREA)
  • Control Of Non-Electrical Variables (AREA)

Abstract

A control system for automatically achieving and maintaining a desired sodium/phosphate ratio and phosphate concentration of the boiler water in an industrial. boiler for minimizing corrosion. The system uses an adaptive controller that models the boiler which enables the system to predict boiler pH and phosphate concentrations at any future time given the feed rates, feed concentrations of high and low sodium/phosphate stocks, blowdown rate, mass of the boiler water, initial boiler phosphate concentration and initial pH. Once these future concentrations are determined, the controller then determines which feed rates will return the current boiler water state to, and then maintain the boiler water at, the desired sodium/phosphate congruency ratio in the least amount of time. Where the controller determines that it is not possible to move the current boiler water state to the desired congruency setpoint, the controller determines a feed rate program that will move the current boiler water state to the attainable steady state point closest to the desired setpoint. An alternative embodiment fixes the phosphate concentration by ratioing the phosphate feed with the blowdown rate while continuously monitoring blowdown pH. This arrangement allows for the control of sodium by switching between the high and low ratio sodium/phosphate stocks based on the measured pH with respect to the pH setpoint.

Description

'_~ai5 ~cy._ilca'~ic>rv ~~~ a ii~~~i.-.i~,rml ~pFO iw ~r ~ .,;i vF ~::c:-iaer~d:r~a .:3.~~p?i~at.:i~~r: ,,I=~:,~»C, f:~~_~-~-~ Alig~.;st. ;:~'l, ~~_:';, r''~PPARATUS AND METHOD Ft)R. AL'TOMATICALhY ACHIEVING ATvdD
t'~AINTAIN:I~1G ~OP3~~RUFNT eONTT20I TN AN I?~1DI'STFIt,I~ BOILER
SI?ECIFICATIOP1 FIELD OF THE INVENTION
The invention pertains to automatic: control systems for continuously stirred tank reactors (CSTRs). In particular, the invention pertains to automatic control systems for achievirg and maintaining optimum congruency and phosphate concentration required to minimize corrosion in industrial high pressure boilers.
BACKGROUND OF TNVENTION
Industrial boilers heat up highly purified feedwater to generate steam for power generation, heating, etc.
A natural consequence of steam production is the "cycling up"
in concentration of chemic:a:ls which enter the boiler inadvertently (e. g., acid leaks) or intentionai.ly (e. g., corrosion inhibitors).
A small portion of the boiler water is "blown down" (i.e., removal of concentrated boiler water from the boiler) to keep the concentrations of non-volatile chemicals (i.e., chemicals that do not flow out with the steam but rather remain substantially in the boiler water) at acceptable levels. 'The rate of blowdown is defined by the "cycles of concentration." Tha term "cycles of concentration" is defined as the sum of the steam and blowdown flowrates divided by the bl.owdown flowrate. Cycles in high pressure boilers range from less than 10 to 100 or more. Thus a chemical added at a low ce-ncentration (e.g., 0.5 ppm) in the feedwater can cycle up to fairly high boiler concentrations (e. g., 30 ppm) .
These boilers are susceptible to, among other things, corrosion. To minimize corrosion, one basic type of corrosion control program that is practiced in the United States within these boilers is phosphate control programs. Typically, in phosphate control programs, a sodium phosphate salt is fed into the solution in order to buffer tr:e solution and to maintain that Ph with sodium. The objective of these phosphate control programs is to maintain the measured variables, phosphate and Ph, within certain stated guidelines, which are dependent upon boiler' pressure by controlling the sodium, phosphate and resultant Ph within the boiler water. See "Sodium Phosphate Solutions at Boiler Conditions: Solubility, Phase Equilibria, and Interactions with Magnetite," by G. Economy, A.J. Fanson, Chia-tsun Liu, J.N.
Esposito, and W.T. Lindsay, Jr., Proc. Intl. Water Conf., 1975, pp.
161-173.
If the concentration of sodium within the boiler water (which is given by the pH, i..e., pH is proportional to effective sodium) is divided by the concentration of phosphate within the boiler water, there exists a range of optimum sodium-to-phosphate (Na/PO') ratios that, if achieved and maintained within the boiler water, will minimize corrosion. Where the boiler water is operated and maintained at a Na/P04 ratio that is below 3.0:1 , the boiler is said to be operating with coordinated phosphate/pH control (also known as "captive alkalinity'q) . Where the boiler water is operated and maintained at a Na/P04 ratio that is between 2.2:1 and 2.8:1, the boiler is said to be operating with congruent control. where the boiler is water is operated at a Na/P04 ratio that is above 3.0, a boiler is said to be operating with "equilibrium phosphate control." All. three types of confi rol can be attained and maintained with the instant invention.
With any of these corrosion control programs, the boiler uses phosphate as the major buffering agent. Additionally, sodium and phosphate concentrations are interdependent variables that must either be controlled simultaneously, or one subservient to the other. They cannot be controlled independently.
Furthermore, boiler systems are extremely slow systems because they comprise large volumes. As an example, a 280,000 pound water boiler having a blowdown rate of :,000 pounds/hour takes over three days to remove and replenish the boiler water. Many things can happen during that time that can alter the operator's initial guess at what concentrations should be added to manually correct control problems.
The applicants have found i:.hat conventional control schemes like Proportional Integral Derivative (PID) control are insufficient to provide practical., universally applicable automatic control of this boiler chemistry for a number of reasons. First, setpoint overshoot is a problem when attempting to control pH in a large volume system. Limitations in pumping capacity inherent in a real-life pumping scheme make "integral windup" a serious problem. Integral windup causes a control system to overshoot its setpoint. Overshoot is also a problem in controlling pH with PID
control due to the asymmetric nature of pH control. Although this problem could potentially be avoided using blawdown flow controllers, these devices are expensive and difficult to maintain and calibrate, Second, tuning such a PID loop is very difficult. Although tuning can be done in many ways, the methods generally require one of two sets of conditions be maintained, either of which are difficult to achieve in an c,perating boiler. In one general tuning method, the boiler chemistry must be held constant for multiples of the first order time constant defined by the volume of the boiler divided by its blowdown flow rate. In real-life applications, such a steady-state cannot be established far that length of time due to small perturbations in feedwater contaminants concentrations. In the other general tuning method, the boiler chemistry must be driven out of the region normally considered to be non-corrosive to derive the tuning constants. This negates the beneficial effect of the treatment. Since any c:~ange in blowdown flow rate (a normal part of boiler operations; will render the measured tuning constant invalid, tuning must be repeated for each blowdown flow setting.
There is one reference to sadium/phosphate control in the literature which demonstrates the difficulties of this method and its shortcomings. In "A Practical Approach to Real Time Data Acquisition and Automated chemical Feed at a Fossil Fueled Cycling Duty Station", by C.E. Frederick presented at the Internationa~
Conference on Cycle Chemistry in Fossil Plants, June 4-6. 1991, the boiler system was tuned using a semi-empirical method to a specific boiler, rather than being adaptable to various types and sizes of industrial boilers. Furthermore, the system disclosed in that reference requires the use of phosphate analyzers which are expensive and require frequent re-~~alibratiens and maintenance.
The closest art to automatically controlling the Na/P04 ratio in the water of an industrial boiler is in automated pH Control systems. The control of pH is in itself a difficult ta.-:k, as discussed in U.S. Patent No. 5,132,916 (Gulaian et al.).
The following U.S. Patents disclose examples of automated pH
control systems: 4,053,74:3 (Niemi), 4,239,493 (Niemi et al.), 4,181,951 (Boeke), 5,132,916 (Gulaian), 5,262,963 (Stana), 4,016,079 (Severin), 5,248,577 (Jerome), 4,033,871 (Wall) and 5,057,229 (Schulenberg).
The Niemi patent discloses an automatic system for controlling the pH and other concentration variables in a chemical reactor.
However, use of that system would not be adaptable to an industrial boiler for the following reasons. The system utilizes a method that requires a steady state that is reached rapidly, which, as discussed previously, an ~nduGtrial boiler does not exhibit.
Consequently, the Niemi patent teaches controlling pH by use of a PID controller, which, as discussed previously, would be difficult to use in Corrosion Control phosphate (CCn) programs described above.
The Niemi et al, patent discloses an automatic system for controlling the pH in a continuous flaw vessel. However, this system is also not adaptable to industrial boilers for the following reasons. For boiler systems, the known tuning methods do not apply for the .reasons described above. If the residence time distribution is known, then simulation of tuning methods requires a perfect match of a simulator and reality. The assumptions of linear processes of first order reactions is not applicable.
Therefore, the method listed in Niemi et al, will only work for systems with small perturbations. industrial boilers exhibit larger deviations. Furthermore, Niemi et al. identifies proportional, proportional-integral and proportional-integral-derivative controls along with an adjustable gain controller.
Limitations on feed concentrations versus system volume will make any adjustable gain ineffective when bounded by limitations in a "pumpable region." Finally, the same pumpable limitations will make integral windup a sex-ious problem in a large volume system.
The Boeke patent discloses an automatic control system for the adjustment of pH that ~s desc:ribed using the term "on-off".
However, this is not an ON,iOFF controller. The series of solenoids that actuate flow across different size orifices produce a signal proportional to feedback. The series of solenoids provides proportional response that is discreet within a specific flow window. This is analogous to a stepwise integration of a continuous function.
The Gulaian patent discloses an automatic system for controlling pH and utilizing an estimation for a pH titration curve in the adaptive control of pH. However, this system is also relegated to short residence times and the use of proportional-integral control. Furthermore, the patent does not discuss limitations from integral windup.
The Stana patent discloses an automatic system for controlling a phosphoric acid plant. However, this system does not involve a model of the system but rather teaches a target feed where the system is compensated for its chemical deficit and then placed in steady state. The algorithm utilized by the system contains predetermined constants that are unique to a particular phosphoric acid plant, and are therefore, not readily adaptable to a variety of phosphoric acid plants (e. g., different plant volumes would require that new constants be calculated and inserted into the algorithm). Moreover, this system controls only sulfuric acid feed and does not try to control two interdependent variables.
The Severin patent discloses an automatic chlorine and pH
control apparatus for swimming pc:ols. The apparatus controls two variables, i.e. , chlorine and pH, under the assumption that the two are not interrelated. A1'~hough ~.hlorine affects pH, chlorine has a minor effect on pH and can be isolated and controlled separately.
This is because in a swvimming pool, chlorine is not the only buffering agent. Its contribution to the pH is masked by the high concentration of anions from the makeup water and atmosphere. This allows the pH to be controlled independent of the chlorine concentration. In contrast, as discussed earlier, a congruent controlled boiler uses phosphate as the major buffering agent, and the pH and phosphate are interdependent variables that must either be controlled simultaneously, or one subservient to the other.
They cannot be controlled independently. In addition, the Severin apparatus also ignores the cycle time of a swimming pool and assumes that the control is constant through the system. It goes not account for lag and residence time effects and probably cycles up and down drastically when in operation. Finally, the pH control range is anticipated as narrow, anc3 works on the assumption that pH
is linear in the chosen range.
The Wall patent discloses a system for continuously monitoring and controlling the pH and free halogen in swimming pool water.
Although this patent mentions the concept of two-sided control (i.e., monitoring whether pH or halogen or both fall within or without predetermined ranges), the control of the pH of swimming pools and the control of pH in a boiler are not interchangeable, as described above.
The Schulenberg patent discloses an automatic system treatment of cooling circuit water. Although this system describes on/off pH
control of a single component to provide one sided control and the system adds other components according to vaporous loss, the chemistry is different fro.~n that of a b oiler. The chlorine in the Schulenberg patent is not the major buffer, and no attempt is made to maintain the COZ alkalinity. In addition, this system cannot control two interdependent variables. The corrosion inhibitor and the pH are not interdependent as are the phosphate (similar to a corrosion inhibitor) and pH..
The Jerome patent discloses a reactant concentration control method and apparatus for precipitation reactions. The system does base feed one reagent and adjusts the second. However, the method and apparatus assume that the system is near steady state at all times. The calculations are:Lineari.zed and performed incrementally to make the calculations simpler. The model used in the method/apparztus is not a true continuously stirred tank r.~sctor (as is the model for industrial boilers).
U.S. Patent No. 5,11,716 (Muccitelli), which is owned by the same Assignee of the present patent application discloses a method of reducing corrosion in a boiler using coordinated phosphate control. However, this method calls for the administering of particular hydroxyethyl piperazines in specific ratios with phosphate, i.e., there is no automatic apparatus nor methods disclosed of canducting this feed.
Two other references which discuss coordinated phosphate control axe: Justification and Engineering Desiqn for the On-Line Monitorind,and Automation,_of a Congruent Fhosphate/pH Program, by Michael E. Rogers, Ian 'Jernappen and Stephen Porter, Paper No. 413, The NACE Annual Conference and Corrosion Show 1992;. Expert System Helps Fine-Tune Boiler-Water Chernistr , by Leyon O. Bretsel and Lon C. Brouse, Power Magazine 1987. In the former reference, although a proposal is discussed for controlling phosphate feed to the feedwater while controlling conductivity in the boiler water, there is no disclosure of any automatic simultaneous control of phosphate and congruency (Na/P04 ratio). With regard to the latter reference, although there is a discussion of providing the operator with chemical feed adjustments, there is no real-time, automatic control system that is disclosed for controlling the chemical pumps in order to control congruencyN
Therefore the prior art does not disclose an effective method for controlling two interdependent and non-volatile chemicals, e.g., sodium and phosphate, in a system that is rarely at steady state that is auto-tuning and does not require control of the blowdown flow. None of the above cited art have devised an apparatus nor a method for achieving an automatic: coordinated sodium/phosphate control system for a variety of industrial boilers without the need far on-line phosphate analyzers.
OBJECTS OF TFiE INVENTION
Accordingly, it is the general abject of this invention to provide an automatic system for coordinated, equilibrium and congruent sodium/phosphate control of an industrial boiler which improves upon and overcomes the disadvantages of the prior art.
It is another object of this invention to provide an automatic system for sodium/phospY~ate c:;ntral that requires no tuning procedure.
It is still anotY~er object of the preferred embodiment of this invention to provide an automatic system for sodium/phosphate control that can be implemented universally, that is easily adapted to any particular boiler system.
It is still yet a further object of this invention to provide an automatic system for sodium/phosphate control where feed pumps are the only means available for control and, in particular, where blowdown controllers tied to the automatic system are not available.
It S.s yet another obje<.a of this invention to function as an advisory system, instructing the operator what to do, or to directly control congruency.
It is still yet another object of this invention to control any number of chemicals used in controlling congruency, e.g., polymer or chelant feeds as wel= as sodium and phosphate.
It is still yet a further object of this invention to control chemical concentrations when precipitation or volatilization is occurring.
It is still even a further object of this invention to control more than one boiler system simultaneously.
It is another object ~.af this invention to provide an efficient method to achieve the congruency control (targets region while minimizing the time spent outside of this control region.
It is another objective of this invention to, once within the target region, provide an efficient method to achieve the congruent control ratio and phosprat2 setpoints and to remain within the target region.

It is still another object of this invention to provide an information database of the congruency control system for use in diagnostics.
It is yet a further objects of this invention to provide an alternative means of determining blowdown without having to utilize expensive blowdown measurement equipment.
It is still yet a further object of this invention to provide an alternative means of determining the contribution to boiler water pH from ionic feedwater contaminant ingresses without having to use chemical analyzers and feedwater flow meters.
It is still a further object of this invention to provide a controller having a well-defined response for those situations where controllers using conventional general purpose equation solvers would simply Conclude that there is no possible response.
It is further object :~f this invention to provide a chemical feed system which minimizes dead time while maximizing controllability.
It is yet another object of a second embodiment of this invention to provide an automa tic congruent control system that utilizes a pumping scheme which fixes the phosphate concentration in the boiler water while permitting the sodium/phosphate ratio to be controlled.
SLJMMt~RY OF 'I HE INtiENTION
These and other obje~:ts of the instant invention are achieved by providing an automaticv control system for controlling at least two interdependent chemicals in the fluid of a continuously stirred tank reactor system having an effluent flow. The control system comprises input means for receipt of fluid parameters and control means responsive to the input means. The control means uses non-proportional control for ,:automatically achieving and maintaining a setpoint of the at least two W terdependent chemicals in the fluid without controlling the effluent flow.
In addition, a second embodiment is provided for controlling the sodium-to-phosphate ratio <ind the phosphate concentration o.f a boiler fluid in an industrial boiler having a blowdown flow. The system comprises input means for receipt of a boiler fluid parameter any? a parameter zndicat:ive of the phosphate conc~.ntra-tion. The system further comprises control means responsive to the input means for automatically achieving and maintaining a predetermined fixed phosphate concentration of the boiler fluid and for automatically achieving and maintaining a predetermined desired sodium-to-phosphate ratio of the boiler fluid without controlling the blowdown flow.
DESCRIPTION OF THE DRAWINGS
Other objects and many of the attendant advantages of this invention will be readily appreciated as the same becomes better understood by reference to the following detailed description when considered in connection °.~ith the accompanying drawings wherein:
Fig. 1 is a block c;iagz-am of the Model Adaptive Corrosion Control (MACC) system;
Fig. 2 is a block diagram of the input/output of the MACC
controller;

Fig. 3 is a diagram showing the boiler water influents/effluents used by she MACC controller boiler model;
Fig. 4 is a time line diagram of the MACC feed program;
Fig. 5 is a diagram of the congruency line and target region;
Fig. 5 is the boiler state space diagram;
Fig. 7 is the boiler state space diagram depicting various feed programs;
Fig. 8 is a time domain diagram depicting the minimization of the first stage period.
Fig. 9 is a diagram depicting two feed rate trajectories having similar time periads within the boiler state space diagram;
Fig. 10 is the boiler state space diagram depicting the shortest time feed programs;
Fig. 10A is a diagram depicting a portion of the candidate shortest-time feed rate trajectories;
Fig. lOB is a diagram depicting the remainder of the candidate shortest-time feed rate trajectories;
Fig. 11 is the boiler state space diagram for stage 2 and stage 3 of the feed program;
Fig. 12 is a blacl~ diagram of an alternative embodiment, the ON/OFF control system; and Fig. 13 is a diagram depicting the ON/OFF control system operation around the pH setpoint at a fixed phosphate concentration.

DETAILED DESCRIPTION OF THE PREFERRED EMBODIMENT
Unless otherwise specified, all references to sodium or sodium-to-phosphate ratio (Na/P04) refer to that sodium which interacts with the phosphate to maintain the. boiler water so as to inhibit corrosion. This is also referred to as "effective sodium"
or the "effective sodium-to-phosphate ratio."
Referring now in detail to the various figures of the drawing wherein like reference characters refer to like parts, there is shown at 20 in Fig. l, the preferred embodiment of the model adaptive congruent controller system 20 of the present invention (hereinafter known as the MACC system) . Generally, the MACC system 20 uses a model of an industrial bailer to predict the feed rates of particular mixtures cf two chemicals, e.g., sodium and phosphate, to achieve and maintain an acceptable range of sodium-to-phosphate congruencies and phosphate concentrations where these congruencies are defined by the sodium-to-phosphate ratio being between high and low limits defined by the boiler operating conditions, and then cheeks itself against its prediction and adapts its model to impz~ove its control. The check is provided for by a laboratory pH and POr analysis, rather than with the use of any on-line pH analyzers or P~~4 analyzers. Hereinafter, the targeted range of congruency and phosphate is referred to as the target region and the specific congruency and phosphate desired is referred to as the setpo.int, as Bill be discussed in detail later.
The MACC system 20 i~ arranged to control water treatment chemicals to be introduced into an industrial boiler 22 by way c;f the feedwater 24 to the boiler 22. The boiler 22 has an effluent flow (hereinafter known as blowdown flow 26) and a blowdown valve 28. The MACC system 20 does not control thF~ blowdown flow 26 via the blowdown valve 28. In fact, one of the distinguishing features of the MACC system 20 over conventional boiler fluid control systems is that the blowdown flow z6 varies independently of the MACC system 20.
The MACC system 20 basically comprises a first chemical feedstream 30A, a second feedstream 308, a ~~ontrol means 32 and an input means 34. The first chemical feedstream 30A and the second chemical feedstream 308 are each connected to the feedwater line 24. The first chemical feedstream 30A is arranged to deliver a first fluid treatment material or chemical, e.g., sodium phosphate mixture having a particular sodium-to-phosphate ratio, to the water in the boiler 22. Similarly, the second chemical feedstream 30B is arranged to deliver a second fluid treatment material or chemical, e.g., a sodium phosphate mixture having another particular sodium-to-phosphate ratio, to the water in the boiler 22. It is important to note at this juncture tt:at the sodium-to-phosphate ratio in the first chemical feedstream BOA must be different from the sodium-to-phosphate ratio in the second chemical feedstream 30B. The particular sodium-to-phosphate ratio (Na/P04) determines a particular pH (high or low) for 'that flrid treatment material.
Sodium and phosphate are non-volatile chemicals in that they do not flow out with the steam but rather only leave the boiler water through the blowdown flow. Furthermore, these chemicals are interdependent in that they both must be controlled in order to achieve and maintain the desired sodium-to-phosphate ratio in the boiler water.
Each feedstream 30A and 30B includes electrically driven pumps 34A and 34B which are coupled to the outlet of respective chemical feed tanks 36A and 36B, via respective draw down assemblies 38A and 38B. The chemical feed tanks 36A and 36B contain high-pH and low-pH sodium phosphate fluid treatment materials, respectively. The draw down assemblies 38A and 38B act as accumulators for enabling precise control of the pumps 34A and 34B by the control means 32.
The control means 32 i.s arranged to precisely control the two chemical feedstreams 30A and 30B by controlling the operation of the pumps 34A and 34B i.n order to achieve and maintain a predetermined desired sodium-to-ahosphate ratio in the boiler water.
The control means 32 basically comprises a computer-based control unit 42 such as that provided by Betz Laboratories, Inc.
under the mark SMARTSCAN PLus and associated software/firmware 44 and a monitorjkeyboard 46. The MACC system software 48 for effecting the operation o~ the control means 32 is set forth in Appendix A, along with an intev-face portion 45t the interface portion 45 interfaces the :>~LARTSCAPd Plus software/firmware 44 with the I~iACC system software a8. fhe control means 32 also includes a feed pump controller 50 am the associated draw down assemblies 38A
and 38B.

The feed pump controller 5c~ ~3nd the associated draw down assemblies 38A and 38H are constructed in accordance with the teachings of U.S. Patent tJo. 4,897,797, assigned to the same assignee as this invention, namely Betz Laboratories, Inc., and whose disclosure is incorporated by reference herein.
As will be discussed in detail below, the feed pump controller 50 receives the optimum feed rates for both pumps 34A and 34B from the M.ACC software 48 via the computer 42.
The input means 40 basically comprises a lab analysis of a sample from the blowdown flow 26. The lab analysis provides, among other boiler water parameters, measured pH and measured PO' values which are then manually entered into the MACC software 48 via the computer 42.
The controller 52 of the MACC system 20 resides in the MACC
software 48. As shown in Fig. 2, the controller a2 takes the external inputs of time, pH, Pn~ concentration and temperature, which are all measured using samples taken from the boiler blowdown line 26. The MACC system 20 is designed to perform well using only the once a day or once a shift sampling rates typical of manual congruent control. The operator makes these measurements on a blowdown sample, and then enters the results into the controller 52. By contrast, conventional control algorithms, such as PID, would require the much shorter- sampling intervals typically associated with on-line pH and phosphate measurements. The controller 52 then utilizing the model equations, discussed below, provides the external outputs of the feed rate (fa) of chemical pump 34A and the feed rate ,fb) of chemical pump 34B to the feed pump controller 50 for precise:Ly controlling pumps 34A an3 34B at their respective feed rates. These contraller 52-determined-feed rates are also fed back .xs input to the controller 52, for calculating future feed rates. A third external output, a status code (e. g., for setting alarms, error reporting, etc.) is included in the MACC software 48.
As mentioned earlier., the controller 52 contains model equations tr3t enable the MACC. system 20 to predict boiler pH and phosphate concentration at any future time, t, given the feed rates, feed concentrations, blowdown flow rate, mass of the boiler water, initial boiler phosphate concentration and initial pH..
The MACC controller 52 uses these equations, as will be discussed in more detail below, by first running the model equations backward for parameter estimation (i.e., to update the boiler model) and then running the equations forward to determine the optimal feed program.
Since future concentrations of Na and P04 depend upon blowdown rate, the model equations are used along with the previous and current phosphate samples, elapsed time between samples, and feed rates over the period t.a back-calculate the blowdown rate, B, required to account for the change in boiler phosphate concentration observed. This eliminates the need fer a direct blowdown flow measurement apparatus which is typically a costly device. Similarly, the last twa pH samples, elapsed time between samples, feed rates and t~.ne estimated blowdown rate (B) are used, in conjunction with the model., to back-calculate a "feedwater contaminant ingress (FCI)"', L, into the boiler. This FCI
represents a generic acid/base flow into the boiler that accounts for the difference between the pH that should be in the boiler, according to the model, and the measured pH. This flow represents the sum of sodium ions, and other positively charged ions, and negatively charged ions in the feedwater 24 (Fig. 1). Thus, it is possible that the sum of these ions can have a positive or negative sign associated with it. again, this eliminates the need for a chemical analyzer and flow meter in the boiler feedwater stream.
Once the blowdown rate, B, and the feedwater contaminant ingress, L, are estimated, the madel equations are run.forward. In principle, all possible feed programs can be plugged into the model and the future boiler sodiurn/phosphate ratio and phosphate concentration predicted by the model compared with their control ranges and setpoints. A~= described below, more efficient and robust methods of determining this optimal feed program are employed. The optimal feed program, which will remain in effect until the next operator sample is entered, is the one that most rapidly brings the boiler Pta/PO' ratio and phosphate concentration into their control limits (i.e.,. the target region, as will be discussed later) , and ono.e ~.~ithin the control limits, to the setpoint.
If a situation arose Juch that the chemical feed pumps 34A and s4B did not have the required pumping capacity to counteract an unusually large FCI , it ~;ould be impossible to bring the system :' 1 into the control range. In that situation, the operator would be informed of the problem and the MACC controller 52 would deliver as much treatment as it could to counteract the FCI. In general, whenever a situation arises where the MACC controller 52 determines that the target region 60 (or setpoint 58, as will be described Later) cannot be reached, the MACC controller 52 sets the feed rates, fa and fn, so that the distance between the model predicted steady-state boiler Na and PO4 concentrations and the target region 60 (or setpoint 58} is minimizQd.
The model equations are based on the fact that the MACC
controller 52 assumes that only :a odium and phosphate are prey ent in the boiler 22 (Fig. 3), Since both sodium and phosphate are non-volatile, the boiler steaming rate does not impact upon sodium or phosphate mass balance around the boiler 22, and thus does not appear in Fig. 3. The FCI, L, is idealized as a constant moles/hr flow rate into the boiler 22. The mass, M, of the boiler is assumed constant. Furthermore, hereinafter use of the term "P04"
refers to total phosphate concentration, i.e., the sum of [H3P04]
phosphoric acid, and the charged forms of P04: [HZP04~ ] monobasic phosphate, [HPO4-] dibasic phosphate and [P04~] tribasic phosphate.
To that end and based on the assumption that the boiler water concentrations are uniform (good mixing), the first model equation is given as:
M*d(P04)/dt .= fa*P04a + fb*P04b w P04*B (Equation #1}
where, ~P04=boiler (hence blowdown) phosphate concentration, moles/kg;
~fa=feed rate (kg/hr) for chemical pump ~34A;

~fb=feed rate (kg/hr) for chemical pump 34B;
~P048=feed concentration (moles/kg) for phosphate from tank 36A;
~P04e feed concentration (moles/kg) for phosphate from tank 36B;
~blowdown rate, in kgs/hr, B; and ~mass of boiler, in kgs, M.
Equation #1 states that the rate of change of the total phosphate in the boiler 22 equals the net flow rate of phosphate into/out of the boiler 22.
Solution of this first order differential equation under the assumption that M, fa, P04a, fb, P04h and B remain fixed, yields the following chemical concentration functions:
PO'(t) - ((fg*P04a + fb*P04b),''B)*(1-e~~tr,~) + P04(0)*e~-t,~r~
(Equation #2) Na(t) - ( (fe*Naa + fb*Nab +L} j B) * (1-ewc~'~) + Na(0) *e~-c~'~
(Equation #3) where ~r = first order time constant = M/B;
~Naa= feed concentration (molen/!~g) for sodium from tank 36A;
~Nab= feed concentration (moles/kg) for sodium from tank 36B;
~L= FCI (moles/hr) into boiler 22;
~P04(0)=initial phosphate concentration (moles/kg); and ~Na(0)=initial sodium conrentratian.
Equation #3 follows by analogy with Equation #2 since the sodium and phosphate flows into/out of the boiler 22 are exactly analogous, except for the addit~_on of the FCI, L, (also assumed constant) into the boiler 22.
It is apparent that unlike phosphate, which is directly measured by the operator, ~he sodium concentration is not directly measured. Therefore, the sodium concen'~ration must be computed indirectly using measured pEi corrected to 25°C and measured PO'.
Using the previous assumption that only sodium and. phosphate are active in determining congruency, the sodium concentration, Na, can be computed, assuming elect~oneutrality in the boiler watar, using a charge balance equation.
The MACC controller 52 feed program consists of three stages, as shown in Fig. 4. During the first stage, chemical pumps 34A and 34B are held at the constant feed rates, fag, fb~ for a length of time, dt~ so as to bring the Na/P04 ratio and phosphate within their control limits. Similarly, the pumps 34A and 34B are held at faz, fez for time interval, dtz, in order to bring the Na/P04 ratio and phosphate to their setpoints. Finally, once these setpoints ara reached, the steady state feed rates, fa3; f~ are used to maintain them. Each time a new pH, P04 measurement is entered by the operator, the feed program calculates these eight parameters (dt~, dtz, fe~, fb~, faz, fbz, fa3, and f~) , as will be described later.
The goal of the MACC system 20 is to choose the optimal feed program. As shown in Fig. 5, the optimal feed program can be defined as:
1) bringing the current boiler water Na/P04 ratio and phosphate concentrations (point 54) within their control limits as soon as possible (i.e., minimizing dt~).
2 ) once within these control limits, bringing the Na/P04 ratio and the phosphate tc th~a setpoint as soon as possible (minimizing dtz~.
Chemically correct Vongruent control is obtained by choosing a narrow control range around the setpoint for Na/PO~ ratio, and choosing as wide a contrcl range as acceptable for phosphate. This allows the algorithm to focus on getting the critical Na/P04 ratio correct initially, except when phosphate levels are so far off that they present an equally critical control problem. For this reason, by default, t he MACC controller 52 automatically defines a narrow control range around the congruency setpoint, the width of which reflects the Na/P04 ratio variation associated with instrumental and operatir~nal pH variability. In particular, there is shown in Fig. 5, the desired cor~gru~ncy line 56 for sodium and phosphate and the ideal congruency setpoint 58. The pH and phosphate control limits define a target region 60 around the setpoint 58.
The first step in determining the optimal feed program is to estimate the blowdown flow rate, B, and the feedwater contaminant ingress, L. As shown in Fig. 4, the MACC controller 52 stores the previous as well as the currently entered P04 and pH entries, which are referred to as "lastP04, lastpH, P04 and pH", respectively.
Equation #2 is used along with the known feed program between samples (i.e. , the previously estimated f,j~, fb~, etc. ) to back-calculate the blowdown flaw rate, B. A blowdown flow rate is estimated and then Equation #2 is applied to each of the three feed stages in turn in order to predict what the concentration would have been had the estimated blowdown flow rate been the actual blowdown flow rate. In particular, Equation #2 is sequentially solved as follows:
P041=( (fad*P04a+fb~*P04b),~B) * r, 1-2~-~co,~)+laStPO4*e~'dc»T>
(Equation 2A) P042=( (fat*P04a+fbz*P04b) /B) *', I-e~'dt2;'>)+PU41*et'dt2~,) (Equation 2B) P043=( (f~*P048+f~*P04b)i'B) * ( l-e~'dr3/ra)+P042*e~-dc3/r>
(Equation 2C) wherein the concentration at the end of stage I is substituted for the initial concentration at the beginning of stage 2, etc.
If the estimated blowdown flaw rate is exactly right, then P043 (the predicted concentration a t the end of stage 3) will be equal to P04, the measured phosphate in the boiler. If P043 is less than P04, the estimated blowdown flow rate, B, must be too high (i.e., it blows out too much phosphate). In other words, the greater the :lowdown estimate, the smaller the predicted phcphate concentration. This uniform P04 vs. blowdown response implies that a particularly simple and reliable method, known as bisection, can be used to find the blowdown estimate that is exactly correct.
As stated earlier, assuming e~lectr~neutrality of the boiler water, the current and preceding phi and Po4 measurements are used in a charge balance equation to calculate Na concentrations, in particular, "lastNa and Na". In a manner. exactly analogous to the manner in which the blowdown was determined via phosphate, the feedwater contaminant :ingre:~s, L, is determined using Equation #3 sequentially and by comparing the Na prec?icted using an estimated L with actual Na estimated from pH and P04.
The root finding process described above can fail in three ways. First, it may turn out that nc re<3souable blowdown flow rate is consistent with the starting and ending phosphate measurements.
For example, if the blowdown valve 28 were completely turned off (B=0), the predicted phosptuate at the end of stage 3 would be at its maximum. A P04 entry greater than this maximum would be inconsistent with the model, since no possible adjustment of the blowdown could match the given data. Similarly, if successive pH
entries that lead to outrageously large (physically impossible) FCIs (e.g,. due to measurement error, data entry errors by the operator, erroneous model parameters, etc.), the MACC controller 52 sets particular error flags (E._INCONSISTENT_P04 or E_INCONSISTENT_PH flags) i.n the MACCr software 48 and then continues to use the previously estimated feed program.
The third way that the root finding can fail is that subjecting lastpH, iastP04, pH, and P04 to small changes consistent with their variability results in relatively large changes in the estimated steady state boiler concentrations. The user can specify both the expected variabil:iøy in t:he inputs, and the acceptable percentage variation in the steady-state concentrations induced by this variability. If the expected variations in pH and phosphate lead to unacceptably large variations in estimated steady-state boiler concentrations, the E~INDETERMINA'rE flag is raised, and MACC
controller 52 continues tc use t=he previously estimated feed program. A common situation that raises the E-INDETERMINATE flag is when the interval between samples is much smaller than the boiler residence time.
Given any feed program (dt~, dt~, dt.3, f~~, fb~, fat, f~,. fa3, and fb3), blowdawn (B) and feedwater contaminant ingress (L), and the initial boiler phosphate concentration and pH, the Ea_uations #2, #3 and the charge balance equa~::ion, c-~n be used to predict Na and P04 concentrations for any time. In principle, all possible feed programs could be calculated and the one feed program that brought the boiler sodium and phosphate concentrations within the target region 60 (and, secondarily, to the setpoint 58) as quickly as possible, would be selected. However, such a brute force approach is impractical if riot impossible. On the other hand, the MACC
controller 52 eliminates the need to use such a brute force method by way of a series of easily solved geometrical problems using a boiler state space diagram, as discussed below.
In its two component forms, the boiler state space (Fig. 6) is visualized as a simple X-Y graph with PO~ (boiler phosphate concentration) on the X axis and Na (boiler sodium concentration) on the Y axis. It is called a state space diagram because each possible state of the model boiler corresponds to a point in the X-Y "space". For any given chemical pump 34A feed rate (fa), chemical pump 34B feed rate (fb), FCI (L), and blowdown flow rate (B), the successive "states" (PO', Na concentrations) that the boiler can assume over time trace out a curve in this state space.
Generally, the boiler state space permits the visualization of both the phosphate exponential (Equation #2) and the sodium exponential (Equation #3) simultaneously depending on their respective current concentrations and the steady state concentrations.
In particular, the curves begin at the point representing the current P0~ and Na boiler c.~n~entrations. Furthermore, according to Equations #2 and #3, the ~oiier ~~oncentr«tions must attain their steady state levels (t=~), namel.y:

P04(t=~) - (fa*P04a + fb*F04b)/B (Equation #4) Na(t~) - (fe*Nae + fig*Nab + L) /B (Equation #5) Multiplication of both sides of the above equations by B shows that they simply state that, in equilibrium, the flow of P04 or Na into the boiler equals the flow out of ..t.
Hence, the curve is defined t5y a straight line between the initial point (PO4(n), tda(0)) a,nd the final point (P04(~), Na(~)).
The reason for this is that in the time domain Equations #2 and #3 the same time constant, r, (M/B), governs the exponential approach to the equilibrium concentration For both phosphate and sodium.
Thus the fraction of the total difference between the starting and final concentrations after any timEa t i ~ the same for both PO~ and Na; changes in "y" (Na) sate always in the same proportion to changes in "x" (PO4) -- the definition of a straight line. Just as its x and y components are, likewise the exponential approach of the point itself (which is the superposition of these "x" and "y"
components) to the equ.ili~rium point in state space is also governed by this same time _constant.
Note that, although the line traced out is straight, the rate at which the point moves along the., line varies. In the beginning the point moves at the fastest rate; as it asymptotically approaches the final equilil,rium point the velocity approaches zero (which corresponds with t:.~e exponential functions in the time domain, :i.e., the derivar_ivn oY each exponential. yields its greatest velocity at the initial state and is zero at the equilibrium point). In fact, the time at which any fraction of the total distance along that line will be traversed is given by using Equations #2 and #4. The following equations provide the time that corresponds to a particular fraction of the total distance to the equilibrium point that remains to be traversed:
t = -(M/B)*ln((P04(t) - P04~~))/(P04(0)-P04(«~))) Equation #6 and using Equations #3 and #5 for sodium, t = -(M/B)*ln((Na(t) - Na(~>)/(Na(0) - Na(~))) Equation #~
These simple, straight line, state space trajectories provide a method of considering the entire range of boiler concentrations that are attainable from a give=_n starting point using a particular set of feed rates.
The possible "end points" of these straight lines (Eauations #4 and #5) are defined by t:he range of the chemical feed pumps 34A
and 34B:
famin <- fa ~= famax w'he.re famin is usually 0;
fin <= fb <= fix where fin is usually 0.
Recasting Equations #4 and =5 into a vector format, and in light of the above constraints, the set of all possible end points (steady state feed rates) forms a parallelogram (i.e., a linear combination of the two feed concentration vectors, fa and fb) in the state space hereinafter known as the pumpable region 62:
P04(~) (P04~,/B) (P04e/Bj (0j f8 + fb + (Equation #8) Na(~) (Naa/B) (Nat~/H) (L/B) This pumpable region 62 is shown in Figure 6. Note that each point in the pumpable region 62 ~zniquely defines the feed rates (fa, f~) associated with the corresponding steady state boiler concentrations, provided only that the concentrctions in chemical pumps 34A and 34B are linearly :independent (in other words, P04e*Nab - P04b*Nae must not be equal to 0), This is an important requirement of the MACC controller 52.
By !:olding the feed rates constant, the boiler state space point is moved along a straight line towards the final, steady-state, concentrations associated with those feed rates. Moreover, the time it takes t.o reach any intermediate point along this state space trajectory is given via Equations #6 and #7. Determining the pumpable region 62 by the MACC controller 52 is known as "scaling the map" and once this is completed, the controller 52 is ready to proceed with determining stage 1 of the feed program.
The objective of stage 1 of the feed program is to bring the boiler water concentrations into the target region 60 as quickly as possible, as discussed earlier with respect to Fig. 5. Thus, it is necessary for the MACC co:~troiler 52 to determine a state space trajectory 64 that begins at the current boiler water concentrations 54, and crosses into the target region 60, as shown in Fig. 6.
However, the only state space trajectories 64 available are those that correspond to actual feed pump rates, in other words, line segments which begin at t:he current point 54 and end in the pumpable region ~52. In general, some of these segments (Fig. 7) will cross into the target region 60, and some will not. In the special case in whic!: none of these line segments cross into the target region 60, the flag E-BOXUNREACHABLE is set, and the MACC
controller 52 uses those feed rates that will drive the current boiler fluid concentrations 54 towards the area of the oumpable region 62 that is closest to the target region 60.
Otherwise, at least one such segment/trajectory 66 crosses into the target region 60. By virtue of Equations #6 and #7, if the end point 68 of this trajectory 66 is not on the perimeter of the pumpable region 62, the time that: it takes to reach the target region 60 can always be reduced by replacing this end point 68 by the point '~0 on the pumpab°..e region's 62 perimeter found by extending the line segment 66 beyond the original end point 68.
Intuitively, by making the distance 66 to the target region 60 a smaller fraction of the entire trajectory 72, the time to reach the target region 60, which depends only on the fraction of the total trajectory that needs to be traversed, is reduced. Kote that any such trajectory 66 will reach the perimeter of the target region 60 before it reaches the interior; thus "'target region" and "target perimeter" are equivalent.
Alternately, when this "endpoint projection in the state space" is viewed in the time domain, by the MACC controller 52 selecting a feed rate ccmbination that will yield equilibrium concentrations beyond the desired equilibrium concentrations, the exponential will reach the desired equilibrium concentrations in a shorter time than if the exact: feed rate combination for the desired equilibrium concentration were uved (Fig. 81.
The next determination that the M.ACC controller 52 makes is to determine just which pumpable region perimeter point will enable the boiler state space point to reach the target region 60 perimeter in the fastest time. To accomplish this, the MACC
controller 52 considers those trajectories that both cross a given edge 74 of the target region's 60 perimeter and end on points in the pumpable region 62 that 1 ie on a lire parallel to that edge 74.
All such trajectories reach the given edge 74 in exactly the same time. To see this, recall the following theorem of Euclid: if two straight lines in a plane are cut by three parallel lines, the corresponding segments are proportional. As shown in Fig. 9, because of this theorem and Equations #6 and #7, the time required to reach the target edge 74 is the same for. any two, and thus all such trajectories.
This time can be minimized by choosing end points within the pumpable region that lie on lines parallel to the target edge 74 that are as far away as possible from the initial point (again, c.f. Equations #6 and #7). This implies that the end point of the shortest time trajectory either lies on a vertex of the pumpable region 62, or on one of the trtijectories that passes through the endpoints of the target edge (Fig. 10). As can be seen in Fig. 10, there are eight line ~egment~5, each having twu collinear trajectories that must be evaluated. The MACC controller 52 exploits this fact by checking each of th.e, no more than 16, such trajectory scena~_-ios, and then u~,i~ig Equation #6 to select the one that reaches the target perimeter in the least time. The 16 candidate trajectory scenarios are established by this analysis being performed on all four target region 60 edges. The 16 candidai.e trajectories caa be more easily seen in Figs. 10A and 10B

where each indicated intersection point defines a particular candidate trajectory; the location of the target region 60 with respect to the pumpable region 62 has been altered for- clarity.
The optimization problem and solution exploits the so-called fundamental theorem of linear programming; the function to be optimized in the linear programming problem--which, though non-linear, has the monotonicity (i.e., either continuously increasing or continuously decreasing) along straight lines in state space that the fundamental theorem of linear programming requires--is Equation #6 (or #7).
Compared to the complexities of stage :l adjustment, stage 2 is easier. As shown in Fig. 11, first, the current point 54 is replaced with the point 68 on the ;.arget region 60 perimeter found via stage 2, which represents where the boiler concentrations will be at the end of stage 1. This new current point and the setpoint 58 determine a line segment 84; the point on the pumpable region's perimeter found by extending this line segment beyond the setpoint 58 must, by the argument given earliery correspond to the feed rates ( faz, f~) that reach the setpoint 58 in the least 'time. If there is no such point, the "~LACC controller 52 sets the E POINTUNRRACHABLE flag and uses the feed rates that correspond to the pumpable point cnithin the ta~.~get region 60 that is closest to the setpoint.
For stage 3, the MAC~:~ controller S? simply feeds at the feed rates that c:~rrespond t.o the setpoint (f3,,, f~). Stage 3 remains in effect until the next sample comes in. The durations of stages 1 and 2 are determined via Equation #6 (or #%)~
For some applications, it may be desirable to modify the MACC
controller 52 algorithms, a~~ discussed below.
Blowdown and FCI Calculation: The blowdawn (B) and FCI (L) estimates are exact: they are the roots of equations that have at most one solution. This could introduce prcblems in boiler systems where the time between pH and P04 samples is much less than the boiler's residence time. Attempting to feed such data into the current MACC controller 52 will lead to a series of E_INDETERMINATE
flags which, in effect, ttErow away the intermediate data points until enough time has elapsed so that a meaningful feed program determination can be made. Therefore, zn boiler systems that utilize such smaller sampling times, it would be more efficient to fit the blowdown and FCI estimates to a series of phosphate and pH
measurements; those blowdown and fCI estimates that came closest to the phosphate and pH sequences in a least squares sense could then be used. This change woul~:.3 dive the MACC system 20 the desirable property that more freguer;t operator sampling would always result in better control.
Introduction of Uead Time: Another way that the MACC system 20 could be improved is by Pxplicitly including dead time into the model. The MACC controller 52 currently assumes that the dead time is zero. Limited simulations suggest that this simplification will not degraue control mucri, provided that dead time i~ a small percentag::. ~f the boiler residence time. However, the impact of dead time depends on many factors; it is lil~ely that at least some of the controlled boilers would experience significantly better MACC control than they would have otherwise obtained had dead time been not been included in the model. On the other hand, conventional congruent control does not explicitly incorporate dead time either.
Optimal Feed I?rogram Determination: the linear programming-related-opti~aization of the MACC controller 52 feed program determination method could be generalized in a number of ways.
Firstly, the setgoint of stage 2 could be replaced with a smaller target region, and then the same computation as in stage 1 could be performed to reach it. This would amount to putting a dead band around the setpoint. In particular, this would entail setting up a new target region around the setpoint and analyzing new feed rate trajectories between the new target region vertices/pumpable region vertices and the new boiler fluid chemical concentrations in a manner similar to that shown in Figs. 10A and 10B. Once the new target region is achieved, the feed rates corresponding to the setpoint would be maintained. Secondly, there is also no reason to have just two such adjustment stages--there could be any number of target regions, each with its corresponding adjustment stage.
Thirdly, there is no need to 1 i mit the appl ication of this feed program optimization to ;;ust two dimensional state spaces. For example, assuming that. polymer measurements were possible, one could introduce a three dimensional state space/target region; if such a system were still limited to two pumps, this would result in a planar pumpable region being embedded in a 3-B space.
Ammonia Correction: Boilers occasionally have ammonia returned to the boiler. Once in ,'.he boiler, ammonia masks the normal relationship between the pH and sodium that is used to control the congruency. Ammonia affects the pH. With ammonia present in the boiler water and with no ammonia correction as part of the MACC
system 20, the sodium concentration calculated from the measured pH
will be higher than is actually correct.
Thus, the following providNS a method for correcting the measured pH for the effect. of ammonia: an analytical determination of the ammonia in the boilE~r wader is provided by any conventional ammonia measuring means (e.g., colorimetric, ion-selective electrode, ion chromatography) to the MPCC controller 52. This ammonia concentration is incorporated into the electroneutrality equation mentioned above, thereby accounting for the ammonia charge contribution and, in particular, subtracting the effect of the ammonia ions from the calculated sodium. tlse of the ammonia correcting method can be implemented 3s an option in the MACC
system 20 that the operator ~~an select so that when the pH and phosphate analysis data ~s inputted into the MACC system 20, the ammonia analysis data can also be entered.
On-Line Input Means: Many boiler operators desire having on-line instrumentation (pH fieters/phosphate analyzers) for boiler water parameters rather than using manual data entry of timse parameters. Although one of the novel aspects of the MACC system 20 is that it eliminates the need to have an-line instrumentation which is susceptible to breakdown and requires frequent calibration, the MACC system 20 i~> easily adaptable for such on-line instrumentation in the input means 40.
Three types of on-line measurements are suggested: (1) on-line blowdown flow measurements coupled with manual pH and phosphate concentration measurements (hereinafter referred to as oLBF) and (2) on-line blowdown flow coupled with on-line pH and/or on-line phosphate (hereinafter referred to as OLBF/pH or OLBF,/P04) and, {3) on-line pH measurement coupled with manual phosphate (hereinafter referred to as OLpH). In the third type, blowdown is estimated rather than measured on-line.
In OLBF, the phosphate concentration and pH of the boiler water would still be manually entered every 8 to 24 hours. Between the P04 and pH entries, alI blowdown flow measurements and the continuously varying feed rates resulting from the MACC system 20 calculations would be stored in arrays. These stored blowdown flow values would comprise appropriately filtered values chosen to average out short-term variation.
As discussed earlier, after each entry of pH and phosphate i5 made, the MACC system 2!) estimates the blowdown flow and FCI.
Using the blowdown flow measurements made over the interval between samples, the MACC controller _'>2 can use the successive manual phosphate entries to estimate a boiler phosphate mass imbalance in a manner analogous to the way in which the FCI is estimated. The boiler phosphate ~ass imbalance represents a measurement of any discrepancy between the expected boiler water phosphate mass and the measured boiler water phosphate mass. The principle difference would be that, for both the FCI anti phosphate imbalance estimates, Equations #2A, #2B, and #2C w~~uld be expanded to included additional stages so as to incorporate changes in the measured blowdawn flow into the model predicaed boiler sodium and phosphate.
The feed program updates for the OLBf method occur much more frequently than for the manual method. with the manual method, this estimate will occur after each entry, every 8 to 24 hours.
With the OLBF method, these feed program updates are made after every blowdown flow measurement using the current blowdown flow and the last set of manually entEred pH and PUS values. Because there are N blowdown flow samples, where N is the number of distinct blowdown samples (and different feed rates) between pH and P04 samples, there will be N feed program updates between pH and P04 samples.
As with the MACC system 20, the optimized feed program would be computed using a state space diagram,, except that the diagram would be scaled (c. f. Equation #8) using the currently measured blowdown rate. The initial phosprate and sodium concentrations to be used would be the boiler model-projected phosphate and sodium concentrations; the expanded form of Equations #2A, #2B and #2C
(see above), incorporating the ~~LBF measurements, would be used to determine these project.ians. fhe resulting calculated feedrates would then be transmitted to tree pumps by the control means 32.

As also discussed earlier, the MACC system 20 uses three stages to reach the set.point 58. Due to the long time between samples, it is possible that all three stages may be reached in one sampling interval. However, using the OLBF method, there could be as many as 3*N stages each time 'the state space and feed program are updated with a new OLBF measurement, three new stages would be generated. In reality, be~:.ause the updates would be so frequent, the system '0 would rarely gzt beyond the current stage before another update arrived.
With OLBF/pH or OLBF/P04, the same steps discussed with OLBF
would be carried cut. Then, using the measured pH, the boiler water sodium concentration would be estimated. If an on-line P04 analyzer is used with the MACC system 20, the boiler water sodium concentration can be calculated as before using the charge balance equation. Otherwise, the last phosphate measurement, the on-line blowdown flow measurements and feed rates (obtained since that last phosphate measurement) are then used, along with the expanded form .of Equations #2A, #2B, and #2C, t.o obtain a model-projected phosphate. Using tht ~~harge balance equation as before, this model-projected phosp:iate arid the on-line temperature corrected pH
measurement are used to estimate a boiler water sodium concentration. In either case, the resulting sequence of boiler phosphate concentratio:~s and/or e:~timated boiler sodium concentrations are used, along with the differential mass balance equations shown below; to estimate PO~ and/or FCIs.
M*(d(Na(t))/dt = L_Na!t) + fa(t)*Naa + fb(t)*Nab -B(t)*Na(t) Fquatzon #9 M*(d(P04(t))/dt = LiP04(t)+fR(t)*P04s + fb(t)*P04b -B(t)*P04(t) Equatzon #10 The above equations are solved for L Na(t) and L_P04(t), and these instantaneous L_Na(t) and L.~PO4(t) estimates are averaged/filtered in such a manner so as to balance the competing goals of responding quickly to FCIs vs. estimating those FCIs accurately. If the FCIs are tracked closely, the upstream series (fa(t), fb(t)) needs to be shifted to incorporate deadtime explicitly; otherwise, it is necessary to use averaged,/fil.tered L_Na(t) and L-P04(t) that are representative of time interval ~ large enough so that the impact of deadtime on these averaged /filtered estimates is negligible.
These averaged/filtered L~Na(t) and L~PO4(t), and blowdown flow(t) are used, along with either the measured or model projected current boiler phosphate and sodium concentrations, to continuously update the MACC system ?0 state space diagram and associated optimized feed program.
With the last option, OLpH, a similar set of calculations are conducted. The MACC system 20 model boiler parameters are updated as before whenever manual phosphate analyses are entered. The on-line pH and model-projected phosphate concentration are used to continuously update the F'~.:I estimate and thereby the MACC system 20 feed program, in a manner analogous to OLBF/pH, but using estimated, rather than measured, blowdown flow rates.
It should be noted that with all of these on-line MACC system 20 options, control of the chemistry is subject to the normal deadtime constraints pre>ent with any automated control scheme.

Multiple Boiler Control: The MACC system 20 is readily adaptable to controlling multiple (e.g., ten) boilers 22 at a customer's facility. ~'o that end, the particular boiler parameters (e.g., boiler size) can be manually entered into the MACC system 20 and the two feedstreams 30A and 30B can be controlled through a solenoid (not shown) that switches the feedstream flows to a particular boiler based on boiler water chemistry, i. e. , the boiler water that is in the lease desirable state will receive the MACC
system 20 congruent control initially.
Note: the definition of the word "amtomatic" as used in this specification with respect to the MACC system 20 refers to all operations, actions, calculations, pumping and other determinations but excluding the manual :removal of the blowdown sample from the blowdown flow, manual insertion of the :sample into the analyzer, and operator entry of the chemical data into the computer 42. In other words, the presence of human intervention in one aspect of the invention does not n:rgate the automatic operation based on a manually-entered in~~ut (e. g., measured phosphate concentration).
In addition, operator response to automatically-generated error messages, alarms, etc. dcmut negate the automatic characteristics of the invention.
As shown in Fig. 12, there is shown a second embodiment of an automatic congruent controller system 220, hereinafter known as the ON/OFF control system 2.'0. As with: the MACC system 20, the ON/OFF
control system 220 cont:.~~ol~: two interdependent variables, namely, phosphate and sodium, t:-ae :utter by way of monitoring the pH.

As stated earlier with respect to the MACC system 20, all subsequent references to sodium and/or to the sodium-to-phosphate ratio (Na/P04) of the boiler water refers to that sodium which interacts with the phosphate to maintain the boiler water so as to inhibit corrosion. This ~s also referred to as the "effective sodium" or the "effective sodium-t:o-phosphate ratio."
The ON/OFF control system 2:0 is arranged to control water treatment chemicals'to be intx-oduced into .3n industrial boiler 222 by way of the feedwater 224 to the boiler 222. The boiler 222 has an effluent flow (hereinafter kn~~wn as blowdown flow 226) and a blowdown valve 228. As stated previously regarding the MACC system 20, the ON/OFF control system 220 does not control the blowdown flow 226 via the blowdown valve 228. In fact, one of the distinguishing features cf the ON/OFF control system 220 over conventional boiler fluid control systems is that the blowdown flow 226 varies independently of the c~N/OFF control system 220.
The system 220 basically comprises a first chemical feedstream 230A, a second r_hemical feedstream 2308, a control means 232 and an input means 240. The first chemical feedstream 230A and the second chemical feedstream 2308 are each connected to the feedwater line 224. The first chemical feedstream 230A is arranged to deliver a first fluid treatment Material or chemical, e.g., a sodium phosphate mixture having a:~ parr ir_ular sodium-to-phosphate ratio and a known pr:osphate concentration, to the water in the boiler 222.
Similarly, the seccnd cht:;icai. feedstream 2308 is arranged to deliver a second fluid t~eatme~nt material or chemical, e.g., a sodium phosphate mixture having a particular sodium-to-phosphate ratio and a known pho;~phate ccncentration to the water in the boiler 222. It is important to rote at this juncture that the sodium-to-phosphate ratio in the first chemical feedstream 230A
must be different from the sodium-to-phosphate ratio in the second chemical feedstream 2308 while the respective known phosphate concentrations in the feedstreams can, in certain circumstances, be identical. The particular sadium-to-phosphate ratio (Na/PO') determines a particular pH fcr that fluid treatment material.
Where the known phosphate concentration ~F04) in both feedstreams is identical, then the different sodium concentrations (Na) in each feedstream determine the pH (hi.gh or low) of each fluid treatment material. The importance of the pH is that the pH of the boiler water, at a fixed phosphate concentration, is indicative of the sodium concentration in thr~ boiler water. Thus, monitoring the pH
of the boiler water provides an effective method of monitoring the sodium-to-phosphate vatic of the boiler water and then in determining which of the two feedstream sodium phosphate mixtures is to be fed to the boiler water to adjust the sodium-to-phosphate ratio of the boiler water, as will be described below. As shown in Fig. 13, by fixing the amount of phosphate in the boiler 222, varying the amount of sodium perrlits the sodium-to-phosphate ratio of the boiler water to be controlled.
Each feedstream 230A .and 230B includes an electrically driven pump 234A and 2348 which are coupled t:o the outlet of respective chemical feed tanks 23E:A and 2368, via respective draw down assemblies 238A and 238B. The chemical feed tanks 236A and 236B
contain high-pH or low-pH sodium phosphate fluid treatment materials, respectively. The draw down assemblies 238A and 238B
act as accumulators for enabling precise control of the pumps 234A
and 234B by the control means 232..
The control means 232 is arranged to precisely control the two chemical feedstreams 230A and 230B by contrclling the operation of the pumps 234A and 234B in order to achieve and maintain a predetermined desired sodium-to-pho:~phate ratio in the boiler water while maintaining a predetermined fixed phosphate concentration (POD setpoint) in the boiler water. In particular, in response to the measurement of the boiler water pH provided by way of input means 240, the control means 232 adjusts the boiler water sodium-to-phosphate ratio by selecting one of the two feedstreams 230A or 230B to pump the appropriate sodium phosphate fluid treatment material while ensuring that the selected feedstr_eam :?30A or 230B
pumps at a rate proport:.oval to they blowdown flow 226 in order to maintain the fixed phosphate concentration.
The control means 232 basica~.ly comprises a computer-based control unit 242 such as that sold by Betz Laboratories, Inc. under the mark SMARTSCAN Plus and <associated :~oftware/firmware 244 and a monitor/keyboard 246. The CN/nFF c-entrol system software 248 and SirIARTSCAPI Plus software 244 far effecting the operation of the control means 232 is set forth :Ln Appendi.x B. The control means 232 also includes a feed pump controller 250 and the associated draw down assemblies 238A and 2388.

The feed pump controller 250 and the associated draw down assemblies 238A and 238B are constructed in accordance with the teachings of U.S. Patent No. 4,897,797, assigned to the same assignee as this invention, namely Betz Laboratories, Inc., and whose disclosure is incorporated by reference herein.
As will be discussed in detail below, the ON/OFF control system software 248 evaluates the boiler water pH and compares that value to the pH setpoint that is entered into the computer 242 via the keyboard 246 by the operator. If the pH is above or below the pH setpoint, the ON/OFF control system software 248, via the computer 242 software/firmware, will command the feed pump controller 250 to drive the appropriate pump 234A or 2348 to correctly feed the precise amount of chemicals that will achieve and maintain the desired phosphate concentration and the desired sodium-to-phosphate ratio.
The input means 240 basically comprises a blowdown flowmeter 252, a pH meter 254 and an optional phosphate analyzer 256. The blowdown flowmeter 252 measures thf~ blowdown rate of the boiler 222 and provides a corresponding electrical signal on line 258 directly to the feed pump controller 250. As will be described later, the feed pump controller 250 uses this signal to ensure that whichever feedstream 230A or 230B s delivering its respective chemical mixture to the boiler 2:?2, thEa chemical mixture xeea is m proportion to the blowdown flow 226. In particular, the ON/OFF
control system software X48 implements this proportion by the following equations:

Lo_Na_flow = P04_setpoir~t * (BD flow)/(Lo PO4 conc)(Equation #11) Hi_Na_flow = P04_setpoir,t * (BL~4flow)/(Hi_PO4_conc)(Equation #12) where, Hi_Na_flow: pump 234A pumping rate;
Lo_Na_flow: pump 234B pumping rate;
P04_setpoint: predetermined fixed phosphate concentration BD flow: current blowdown flow measurement or exponential moving average of past n flow values;
Hi_P04_conc: known phosphate concentration in tank 236A;
Lo'P04'conc: known phosphate aoncE:ntration in tank 2:368;
The pH meter 254 monitors the pH of the blowdown flow 226 and provides a corresponding eiectrical signal on line 262 to the computer 242 for processing by the ON;!OFF control system software 248. As is also well known, the pH of the blowdown flow 226 is indicative of the boiler water pH.
A cooling coil 264 is disposed between the blowdown flow 226 and the pH meter 254, as well as the optional phosphate analyzer 256 (if used), to protect these apparatus from damage due to the high temperature of the blowdown flow 226.
Control in this ON/OF'F control system 220 resides in two places. The first area of control is by way of a maintenance means that maintains a fixed concentration of a chemical, e.g., phosphate, in the boiler water. In particular, the maintenance means for maintaining a fixed phosphate concentration in the boiler water comprises the feed pump controller :?50, the two feedstreams 230A and 2308 and the bl.owdown flcwmeter 252. Since the phosphate concentration i.-~ each chemical fef.d tank 236A and 236B is known and the predetermined fixed phosphate concentration (P04 setpoint) has been entered into the comf.~uter 242 by the operator, the feed pump controller 250 uses the :~igr.al 258 to control the pumps 234A and 2348 so that a precise amount of phosphate is delivered to the boiler water to feed the amount of phosphate needed to maintain the phosphate setpoint in steady state, in accordance with the previously discussed algorithm. In accordance with one preferred aspect of this invention, the ON/OFF control system 220 operates to ensure equal usage of tanks 236A and 2368, i.e., an even distribution of tank amount is used to avoid emptying one tank faster than the other.
The second area of control i;s through the switching between high-pH and low-pH sodium phosphate mixtures of the first chemical feedstream 230A and the second chemical feedstream 2308. This action is conducted by the control means 232. In particular, the ON/OFF control system software 24E~ may use, but is not required to use, an exponential moving average (EMA) of the pH (@ 25°C) to minimize electronic noise from the pH meter 254 in providing the boiler water pH value. A pH adjustment is made at predetermined intervals, e.g., every 30 minutes. If the EMA pH value is above the pH setpoint, as determined by a comparator means in the ON/OFF
control system software 248, the software 248 commands the feed pump controller 250, via the computer 242, to control the pump 2348 to feed the law-pH sodium phosphate mixture i.n the second feedstream 2308 at the ~aroportioned rate (Equation #11) while turning off the other pump 23~A in the first chemical feedstream 230A, thereby driving thbollsar water pH value down to the pH
setpoint. If the EMA pH value is below or equal to the pH setpoint as determined by the same comparator means in the ON/OFF control system software 248, software 248 commands the feed pump controller 250, via the computer 242, to control the pump 234A to feed the high-pH sodium phosphate mixture in the first feedstream 230A at the proportioned rate (Equation #1.2) while turning off the other pump 2348 in the second chemical feedstream 2308, thereby driving the boiler water pH value up t« the pH setpoint. In either situation, whichever feedstream is delivering its particular sodium phosphate mixture, the feed pump co,ntrollcr 250 maintains the fixed phosphate concentration in the boiler watAr by ratioing the active feedstream 230A or 230B to the bl.owdown rate. This alternate "on"
and "off" feed control brings the boiler water sodium-to-phosphate ratio to the desired sodium--to-phosphate ratio at the predetermined fixed phosphate concentration and maintains that ratio at that phosphate concentration. :In parti~~ular, tt,,e ON/OFF control system software 248 implements thi:~ switching by the following commands:
IF(pH > pH-setpoint) THEN
Lo_Na_flow = P04_setpoint * (BD_flow)/(LO-P04 conc) Hi_Na_flow = 0 ELSE
Lo_Na_flow = 0 Hi_Na_flow = P04-setpoint * (BD_flow)/(Hi_P04 cone) where, pH: current pH value of boiler water or exponential weighted moving average of n past pH values;
pH_setpoint: pH setpo:i.nt Should the first chemical feedstream 230A and the second feedstream 2308 ever be utilized ;simultaneously so that both are delivering their respective sodium phosphate mixtures to the boiler water, rather than in alternation as in the ON/OFF control system 220, then the sum of the feedstream _rates (Hi Na_flow + Lo Na_flow) would have to be in proportion to the blowdown flow 226 in order to maintain a fixed amount of phosphate in 'the boiler water.
The measurement of the blowdown pH can be a single measurement or can be the average pH value of a plurality of pH measurements.
In the ON/OFF control mode, it. is assumed that the blowdown flow rate accurately maps the changes in boiler phosphate concentrations. It is posJible that the expected and actual phosphate concentrations can start to diverse due to calibration problems, pump leaks, or actual leaks in the boiler. In that case, some means of monitoring the concentration and informinS the operator that action to corre<~t the boiler phosphate mass imba Lance is desirable. One exemplary method «f detecting the mass imbalance entails use of a phosphate analyzer 256. To that end, the analyzer 256 monitors the phosphate concentration in the boiler water and provides an electrical signal on line 260 to the feed pump controller 250 which, in turn, provides this electrical signal to the computer 242. The computer 242 provides the phosphate concentration to the ON/OFF control system software 248. The 0N/OFF control system software 248 calculates the statistical variation in the desired phosphatE=. concentration over time and alerts the operator to a boiler water phosphate mass imbalance. 1s an alternative to using the phospuate analyzer 256, the blowdown phosphate concentration ~~oula be manually measured and the measured phosphate concentration can be entered into the computer 242 at various time intervals.

Without further elaboration, the foregoing will so fully illustrate our invention that others may, by applying current or future knowledge, readily adopt the same for use under various conditions of service.
~1 /* tnacc4.h: external interface for the mace4.c module */
/* M~ ?9, 1994: added nh3, lastnh3, and b4lastnh3 to support ammonia co__ection to the pH -- JCG */
# include <float.h~ /* uses only FLT,MAX */
# define E ENGINEERI1~1G UNITS :1. /* 0=research units, 1=engineering units*/
# define E NAN (-l.Oe3~? /* Not a number */
# define E_MINMINP04 (1.0e-3/94.97? /* 1 ppm in moles/kg */
# define E MAXMAXP04 (1.0e-1/94.97) /* 100 ppm in moles/kg */
# define E MINMINNAP04RATI0 2.2 # define E_MAXMAXNAP04RATI0 2.8 # define E MAXHISECT:1:ONS 100 /* aprox. 1 part in 1.26765e+30 */
# define E NV 4 /~~ number of vertexes in feasable region, V */
# define E NW 4 ;* number of vertexes in target region, W */
typedef enum e_logica~l { E FALSE=0, EJTRUE=1 } E LOGICAL;
typedef struct a vec:t=or { dou:~le po4; double na; } E VECTOR;
typedef enum e-initst:atus { !* e_init() errors/alarms */
E_INITOK=0, E _MISSING T, E_MISSING FAMIN, E MISSINC3 FAMAX, E__MISSING FADEF, E _MISSINC3 P04A, E MISSING RATIOA, E_MISSING FBMIN, E MISSING FBMAX, E MISSING FBDEF, E MISSINC3 P04B, E_~MISSIN(i RATIOH, E_MISSING P04, E MISSING PH, E ~MISSINC3 M, E MISSIN(J BDTEMP, E MISSING P04SETPO::NT, E~MISSING MINP04, E_MISSING MAXP04, E MISSING RATIOSET~?OINT, E BDMAX LT BDMIN, E~_NALEAKMAX LT NALF'sAKMIN, E FAMAX LE FAMIN, E_FBMAX LErFBMIN, E MINP04_L~ MINMIN~?04 , E~MAXP04 GT MAXMAXP04, E_P04SETPOINT LT MINP04, E_~P04 SETPOINT GT MAXP04 , E P04A LT ZERO, E__P04 B LT ZERO , E MINRATIO LT MINM:CNRATIO, E~MAXRATIO GT MAXMAXRATIO, E~_RATIO_LTrMINRATIO, E RATIO GT MAXRATIO, E~__RAT I OA EQ RAT I OB ,.
E _MISSING S$GA, E MISSING SPGB, E II~fITSTATUS;

t~~~edef enum a updatestatus { /* a updates) errors/alarms */
E UPDATEOK=0, E_~ ~'7ETERMINATE=:L, h L..X UNREACHABLE=2, E_OUTOFBOX TOOLONG=4, E_POINT UNREACHABLE=8, I._BOX UNMAINTAINABLE=16, E POINT UNMAINTAI1VABLE=32, E SAMPLRINTERVAL 'r00LONG=64 , E~_NOTUPDATED=128, E INCONSISTENT PO~~=256, ~ INCONSISTENT PH:=512 E-UPDATESTATUS;
typedef struct a /*
boiler { public:
*/

double t; /* time since start of run */

double famin, /* pump a =>
mininum feed rate */

famax, !* maximum feed rate */

fadef, /* .
default (initial) feed rate */

po4a, /* total (as ortho-P04) concentration*/

ratioa, /* Na/P04 ratio */

spga; /* specific gravity */

double fbmin, /* pump b =>
mininum feed rate */

fbmax, l* maximum feed rate */

fbdef, !* default (initial) feed rate */

po4b, ,%* total ti /* P04 b (as ~rtho-P04) concentration*/

ra Na/P04 o ratio , spgb; /* specific gravity */

double m; /* boiler water mass */

double bdtemp; /* boiler blowdown temperature */

double po4, /* boiler blowdown po4 =>
concentration */

dpo4, /* variability */

po4setpoint,/* setpoint */

minpo4, /* ~
lower control limit */

maxpo4; /* upper control limit */

double nh3; /* boi.l~er blowdown ammonia concentration */

double ph, /* boiler blowdown pH
=>
at temperature=bdtemp */

dph, /* variability */

phsetpoint, /* setpoint (nh3=O.O;bdtemp=25C)*/

dphsetpoint;/* 1/2 setpoint control range */

/* (control limits =
phsetpoint +/-dphsetpoint)*/

double napo4ratio, /*
alternative to phsetpoint */

minnapo4ratio, /*
alternative to phsetpoint-dphsetpoint */

maxnapo4ratio; /*
alternative to phsetpoint+dphsetpoint */

double max_sample"interval;
/* if time between samples is longer than*/

i*
this, an updatestatus code is flagged */

double max_outofboxadj ustment;
/*
if we cannot get into control ranges on */

~ *
pH
and within this time, an */

/*
updatestatus code is flagged */

double max relativeerr or;
/*
largest allowed variation in estimated */

/* steady-state ntrations conce induced by the given variabilities, dpH
and */

/* dP04 ERMINATE
before status an E_INDET ~s flagged *j double beps, j*
bisection (root fording) epsilon */

bdmin, /*
lower bound for blowdown bisection */

bdmax, /*
upper bound for blowdown bisection*/

naleakmin, j*
lower bound for Na leak bisection */

naleakmax; i*
upper bound far Na leak bisection */

/* (final rootfinding due to truncation errors will be */
errors on /* bep s*(~bdmax~ ~bdmin~) beps*(~naleakmin~+~naleakmax~) */
+ and double fdt; /* f eed delta time (due t:o hardware truncation):
*/

/* m inimum tame interval between pump changes */

/* private: */
double lastt, lastbdtemp, lastpo4, lastnh3, lastph; /*last inputs... */
dog ' b4lastt,b4lastbdtemp,b4lastpo4,b41astnh3, b4lastph;/* & before last */
dou.,~e bd, 1; /* blowdown, leak estimates */
double lastbd, lastl; /* prev blowdown, leak estimates */
double dt[2], fb[~:1, fa[3]; !* current feed program */
double lastdt [2] , :Lastfb [3] , lastfa [3] ; /* previous feed program */
E__VECTOR vV[E NV+ll, vW[E NW+1]; /* pumpable, target regions */
E VECTOR vlastV[E IW+1], vlastW[E NW+1]; /* last pumpable, target regions */
E-_-_VECTOR vF[3]; -- /* steady-state concentrations of the feed program */
E VECTOR vlastF[3:; /* last steady-state concentrations of the feed program*/
E UPDATESTATUS updatestatus; /* identifier for a update() status*/
E__UPDATESTATUS lasr_updatestatus; /* previous updatestatus */
E__INITSTATUS initstatus; /* identifier for a init() errors*/
E LOGICAL undoable; ;'* can last e,update be undone ? */
ENBOILER;
const E BOILER *e undefined boiler(void) E INITSTATUS a init(E BOILER *blr);
double a afeedZE BONER *blr);
double e-bfeed(E BOILER *blr);
E UPDATE3'TATUS a update(E BOILER *blr);
E LOGICAL a undo~E BOILER *blr);
double e-hmp2po4(double hmp);
double e~o4model(E BOILER *blr);
double e~hmodel(E BOILER *blr);
double a nasetpoint(E BOILER *blrl;
double e-congruenoyratio(E BOILER *blr);
double e-fixNAN(doub:Le num5er, double forNAN);

/* ma"~,c4.c: module f:or model based coordinated phosphate/ph control */
/*
April 4, 1994 added lines near the end of e_update() to force feed rates within specified pumping lower and upper bounds.
Roundoff error sometimes resulted in pump rates of -l.Oe-10 rather than 0.0; such negative numbers can often spook equipment which is sometimes set up to ignore such "meaningless" values - JCG
April 10, 1994: fixed a parentheses error near the bottom of a init(). Error was harmless for most usages but not for tfie way I was using a init() in MACC4SSP. Error was on line the began blr->b3 = fixNAN(...).
Also removed two local variables that were never used in a update() - JCG
April 17th- fixed default blowdown calculation at bottom for e-init() - JCG same line as bugfix above.
May 2, 1994 - set updatestatus field to E UPDATEOK in a init() (so it is in a well defined state after each a init() call).
May 29, 1994 - added support for an ammonia correction to the ;pH as per Scot Boyette's request.
~7une 3, 1994 - added an a congruencyratio() exported function to support callers who warxt to see the Na:P04 ratio of the entered pH, P04, NH3, and temperature last entered.
*/
#include "macc4.h"
#include <math.h>
/* These macros provide for on--the-fly units conversion from engineering units to the research units used in MACC4's internal calculations. Rather than bl.r->famin we write FAMIN(blr), and these macros guarantee that the resulting value will be in research units, even though the 'value stored in blr->famin is in engineering units.
*/
# define E LITERS_PER_GALLON (3.785) # define E MW P04 (94 97) # define E MW NH3 (17.0) # define E ZERO K IN F (-459.6.') # define E GPD TO KG~ERHR(gpd,spg) ((E LITERS_PER GALLON/24.0)*(spg)*(gpd)) # define E PPM TOlMOLESPERKG(ppm,mw) (Tppm)*1 Oe-3/(mw)) # def ine E-I~IOL~,SP~RKG TO PPM (mpk, mw) ( (mpk) *1 . Oe+3* (mw) ) # define E PERCENT 'TO MO~ESPERKG(percent,mw) ((percent)*l.Oe+1/(mw)) # define E LB TO K~(lb) ((lb)*0.45359) # define E KG TOrLB(kg) ((kg)/0.45359) # de f ine E F2I~EL~IN ( f ) ( ( 5 . 0 / 9 . 0 ) * ( ( f ) - E ZERO K IN F ) ) # define K DEFAULT 'TEMP 298.1a /*room temperature in degrees K */
# define K DEFAULT NH3 0.0 /*for Na:P04 ratio setpoints, in moles/kg */
# define K DEFAULT :MHO 0.997047 ,/*room temperature density of saturated H20*/

#i. E_ENGINEERING U7VITS ;'* this compilation switch is set in macc4.h */
/*c -~ rersions internal require to obta.i.n units MACC4' s : */

/*t~~ws: gallons/day -_> kg/hr */

/*P04 boiler cons:ppm as ortho P04 moles/kg */
-_>

/*P04 fwws conc: % as ortho P04 -_> moles/kg */

/*Boiler mass: lbs -_> kgs */

# define FAMIN(blr) E GPD TO KGPERHR( (blr) ->famin, (blr) ->spga) # define FAMAX(blr) E_GPD_T0~_KGPERHR((blr)->famax,(blr)->spga) # define FADEF(blr) E GPD TO~KGPERHR((blr)->fadef,(blr)->spga) # define P04A(blr) E PERCENT TO MOLESPERKG((blr)->po4a, E MW P04) # define NAA(blr) ((bIr)->ratioa*P04A(blr)) # define FBMIN(blr) E GPD TO KGPERHR((blr)->fbmin,(blr)->spgb) # define FBMAX(blr) E GPD_TO~KGPERHR((blr)->fbmax,(blr)->spgb) # def ine FBDEF (blr) E GPD TO--~KGPERHR ( (blr I - >fbdef , (blr) ->spgb) # define P04B(blr) E PERCENT TO MOLESPERKG((blr)->po4b, E MW P04) # define NAB (blr) ( ~;blr> ->rar.:iob*P048 (blr;l ) " -# define FDT(blr) (iblr)->fdt) # define USERP04(po9:) E MOLE:~PERKG TO PPM(po4, E MW P04) # define P04(blr) E PPMrTO MOLESPE~KG~(blr)->po4, E MW P04) # define LASTP04(blr) E PPM ";'O MOLESPERKG((blr)->lastpo4, E MW_P04) # define DP04 (blr) E:_PPI~ TO~MOLESPERKG( (blr) ->dpo4, E MW P04) # define MINP04(blr) E PPM 1C MOLESPERKG((blr)->minpo4, E MW P04) # define MAXP04(blr) E PPM TO'MOLESPERKG(Cblr)->maxpo4, E MW P04) # define P04SETPOINT(bIr) E_PPM_TO MOLESPERKG((blr)->po4setpoint, E MW P04) # def ine NH3 (blr) \ -E PPM TO MOLESPER.R:G(e fixNAN((blr)->nh3,K DEFAULT NH3), E MW NH3) # define LASTNH3(blr) \J - - - -E PPM TO MOLESPERR:G(e fixNAN((blr)->lastnh3,K DEFAULT NH3), E MW NH3) # define LASTPH(blr) ((Slr)->lastph) - '-# define PH (blr) ( (blr) ->ph) # define DPH(blr) ((blr)->dph) # define PHSETPOINT(blr) ((blr)->phsetpoint) # define DPHSETPOINT(blr) ((bl.r)->dphsetpoint) # define LASTT(blr) ((blr)->lastt) # define T(blr) ((blr)->t) # define M (blr) E LH_TO KG ( (bl:r) ->m) # define BDTEMP(bIr) E F2KELVIN(blr->bdtemp) # define LASTBDTEMP(blr) E F2KELVIN(blr->lastbdtemp) # define MAX SAMPLE INTERVAL(blr) ((blr)->max sample interval) # d~~.fine MAX OUTOFBOX ADJUSTMENT(blr) ((blr)->max outofboxladjustment) # define BEPS(blr) ((blr)->beps) # define NALEAKMIN(blr) ((blr)-.>naleakmin) # define BDMIN(blr) E LB TO KG((blr)->bdmin) # define NALEAKMAX(blr) T(bIr)->naleakmax) # define USERHD (bd) E KG TO LB (bd) # define BDMAX(blr) E LB TO KG((blr)->bdmax) # define MAX RELATIVE ER~OR~bl:: ) ( ( (blr) ->max relative error) /100.0) # define BD(Slr) E LH TO KG((blr)->bd) # d~sfine L(blr) ( (blrT->I) # define DT (blr, n) ( (blr) ->dt [n] ) # define FA(blr,n) E GPD TO KG1?ERHR( (blr) ->fa[n] , (blr) ~->spga) # define FB(blr,n) E~GPD-TO_KGPERHR((blr)->fb(n], (blr)->spgb) # d.. f ine DEFAULT DP04 0 . ~
# define DEFAULT DPH 0.05 # define DEFAULT DPHSETPOINT 0.025 # define DEFAULT MAK OUTOFBOX ~'~DJUSTMENT (24.0*7.0) /* seven days */
# define DEFAULT MAX_RELATIVE~ERROR 20.0 /*20% error allowed by default*/
# define DEFAULT MAX SAMPLE IN'.CERVAL (24.0*7.0) # define DEFAULT BEPS 1.0e-12 #e7.se /* research units */
# de~"ine FAMIN(blr) ( (blr) ->farnin) # de ~e FAMAX(blr) ( (blr) ->farnax) # detlne FADEF(blr) ((blr)->fadef) # define P04A(blr) ((blr)->po4a) # define NAA(blr) ((blr)->ratioa*P04A(blr)) # define FBMIN(blr) ( (blr) ->fbrnin) # define FBMAX(blr) ((blr)->fbmax) # define FBDEF(blr) ((blr)->fbdef) # define P04B (blr) ( (blr) ->po4b) # define NAB(blr) ((blr)->ratiob*P04B(blr)) # define FDT (blr) ( (blr) ->fdt ) # define LASTP04(blr) ((blr)->lastpo4) # d.=_fine USERP04 (po4) (po4) # define P04 (blr) ( (blr) ->po4) # define DP04(blr) ((blr)->dpo4) # define MINP04(blr) ((blr)->minpo4) # define MAXP04 (blr) ( (blr) ->maxpo4) # define P04SETPOIN'T(blr) ( (b:Lr) ->po4setpoint) # define LASTNH3(blr) (e fixNADT((blr)->lastnh3, K DEFAULT NH3)) # define NH3(blr) (e fixNAN((blr)->nh3, K DEFAULT NH3)) -# define LASTPH(blr)-((blr)->laistph) -# define PH(blr) ((lolr)->ph) # define DPH(blr) ((b1r)->dphl # define PHSETPOINT(:blr) ((blr)->phsetpoint) # define DPHSETPOINT(blr) ((blr)->dphsetpoint) # define LASTT(blr) ( (blr) ->laa:~tt) # define T(blr) ((b:Lr)->t) # define M(blr) ( (blr) ->m) # define BDTEMP(blrl (273.15 + (blr)->bdtemp) # define LASTBDTEMP (:blr) (273 . 1.5+ (blr) ->lastbdtemp) # define BLRTEMP(blr) (273.15+(blr)->blrtemp) # define MAX SAMPLE INTERVAL(blr) ((blr)->max sample interval) # define MAX OUTOFBt3:X ADJUSTMEDtT(blr) ( (blr)->max_outofbox adjustment) # define BEPS (blr) ( (blr) ->bep:;) # define NALEAKMIN(b1r) ((blr:1->naleakmin) # define USERBD(bd) (bd) # define BDMIN(blr) ((blr)->bdmin) # define NALEAKMAX(blr) ((blr)-~>naleakmax) # define BDMAX(blr) ((blr)->bdmax) # define MAX RELATIVE ERROR(blr) ((blr)->max relative error) # define BD(blr) ((b1r)->bd) - -# define L(blr) ((b:Lr)->1) # define DT (blr, n) ( (blr) ->dt [n] ) # define FA(blr,n) ( (blr) ->fa [n] ) # define FB (blr, n) ( (blr) ->fb [n] ) # define DEFAULT DP04 E_PPM_TO__MOLESPERKG(0.5,E_MW_P04) # define DEFAULT DPH 0 05 # define DEFAULT DPHSETPOINT 0.025 # define DEFAULT MAX_OUTOFBOX_ADJUSTMENT (24.0*7.0) /* seven days */
# define DEFAULT MAX_RELATIVE~ERROR 0.2 /*20% error allowed by default*/
# de: f ine DEFAULT MAX SAMPLE I'~VTERVAL ( 24 . 0 * 7 . 0 ) # define DEFAULT_BEPS 1.0e-12 #endif # define ROUNDED DT ( blr, n) (e round down (DT (blr, n) , FDT (blr) ) ) /*,Misc. Functions */
dou~~' a round down(double x, double dx) { /* round x down to multiple of dx */
re~..rn ( x --( (dx: <= 0.0) :' 0.0 . fmod(x,dx) ) ) ;
double a min(double x, double y) { /* minimum of 2 numbers */
returnZ (x < y) ? x: . y) ;
double a max(double x, double y) { /* maximum of 2 numbers */
returnT(x > y) ? x: : y) ;
double a forceinrang~e(double x, double low, double high) {/*force low<=x<=high*/
if ( x <= low) return(low);
else if ( x >= high) return(high);
else return(x);
/* 'use fabs (x) for ~ x ~ , log (x) for In (x) */
/* l3asic Vector Operations */
#define VBUF 100 E_VECTOR *e vec(dou.ble po4, double na) { /* convert 2 components to vec*/
static E SECTOR v[VBUF]={0.0}; /* uses a circular buffer; callers must not */
static int ibuf = VBUF-1; ,~* depend upon this memory since is will be */
ibuf = (ibuf+1) % VBUF; /* reused every VBUFth call */
v[ibuf].po4 = po4; ;* (for example, exp ressions with more than */
v[ibuf].na = na; ~* ~Uroa~~eneededl~o work aroundranlMSsC*bu */
r~_turn(&v[ibuf]); ~* When passing structures to/from functions*/
double a dot(E VECTOR *vl, E VECTOR *v2) { /* inner (dot) product of 2 vecs*/
rs~turnZvl->po4*v2->po4 + vl->na*v2->na);
double a norm(E VECTOR *v) { /* magnitude (norm) of vector */
returnZsqrt(e-dot(v,v)));
double a norm2(E VECTOR *v) { /* magnitude (norm) of vector, squared */
returnTe dot(v;v));
/*e sXv: multiply ("X") a scalar, s, tires a vector, v */
E VECTOR *e sXv(double s, E VECTOR *v) -rE:turn (e vec (s*v->po4, s*v=>na) ) ;
E VECTOR *e vDs(E VECTOR *v, double s) { /* divide a vector by a scalar */
-return(e_sxv(1.~'/s, v) ) ;
%* a vPv: add ("P") two vectors */
E_VECTOR *e vPv(E VECTOR *vl, E~VECTOR *v2) return(e vec(vl->po4 + v2->po4,v1->na + v2->na));
/* a vSv: Subtract ("S") two vectors (v1 - v2)*/
E VECTOR *e vSv(E VECTOR *vl, 13 VECTOR *v2) {
-return(e vPv(vl, e_sXv(-1.0, v2)));

/* e,vEv: Are two vectors approximately equal, within machine precision ? */
E_L0~'~"'~AL a vEv ( E VECTOR *vl , E VECTOR *v2 ) {
re ~n ( Te norm(e_vSv(vl,v2)~ <= DBL,EPSILON*(e norm(vl)+e norm(v2))) ?
E_TRUE . E_FALSE);
~* Vector Functions */
E_VECTOR *e rot90(E VECTOR *v) {/*rotate vector counterclockwise 90 degrees */
return(e_vec(- v->na, v->po4));
/* ~~ between: given three co-l~.near points, is <B> between <A> and <C> ? */
E LOGICAL a between(E VECTOR *vA, E VECTOR. *vB, E VECTOR *vC) {
-return( (e norm(e vSv(vA,vB)) <= a norm(e vSv(vA,vC)) &&
a norm(e~ vSv(vB,vC)) <= e_norm(e vSv(vA,vC))) ? E TRUE . E FALSE);
_ .._ - -/* a decompose: decomposes a given vector into the given ~~coordinate system's.
Specifically, computes alpha and beta such that:
<A> _ <Origin> + alpha*<X~:> + beta*<YY>.
Here <A>, <Origin>, <XX>, and <,Y'Y> are vectors, alpha and beta are scalars.
Note: <XX> and <Y'Y> must be linearly independent and non-zero, or divide by 0 errors will occur (macc4 prevents such a possibility by various up front checks).
*/
void a decompose(E VECTOR *vA, E VECTOR *vOrigin, E VECTOR *vXX, E-VECTOR
*vYY, double *alpha, 3ouble *beta) { E VECTOR *vx, *vu, *vuperpx, *vYYperpx;
vx = a vDs(vXX,e norm(vXX));
vu = a vSv(vA,vOrigin);
vuperpX = a vSv(vu,, e_sXv(e dot(vu,vx), vx));
vY'Yperpx = a vSv (v'.tY, a sXv~e dot (vYY, vx) , vx) ) ;
*beta = a dot (vupesrpx, vYYpez~px) /e dot (vYYperpx, vYYperpx) ;
*a.lpha.' = a dot(vx, a vSv(vu,, e-sXv(*beta,vYY)))/e norm(vXX);.

/* =unctions for ph to no Conversion */
/* d'-who: density of water a:Long the saturation line ("psat") */
/* (i...:luded from CMS 2.0 module db2.e) */
/* Scott Boyette provided; original source Journal of Physical and Chemical Reference Data, Vol_ 10, No. 2, 1981 p 295. W. L. Marshal and E. U. Franck.
*/
double db rho(double t) { /* t in degrees K */
static double a[] -.{-0.735396, 0.015695, -1.153587E-5, -8.157489E-8, 2.121698E-10, -1.590221E-13 int i = sizeof(a)/sizeof(a[o]);
double rho = a [--i] ; /* ~.:c>mpute "sum over i of a [i] *t**i" */
if (fobs (K DEFATJLT TEMP-t;~ r= K DEFAULT TEMP*DBL EPSILON) - - /* to avoid"fit of a fit" errors */
return(K DEF.~ULT_RHO); /* for special case of room temp values*/
- ,~* use the exact value (as per S. Boyette)*/
do rho *= t;
rho += af--i.l;
while (i > 0),;
rho /_ (1.0 + 0.003582*t);
return(rho);
/* e: no: Back calculates an "equivalent no" in the boiler, using, tfie charge balance equation and measured po4 and ph in the boiler.
po4, no concentrations are in moles/kg.-Temperature in Kelvin. Activity is assumed 1.0 for simplicity (no ionic strength correction).
See CMS (Condensat:e Modell.irag System) 2.0 module db2.c for equilibrium constants and the algebraic expression used for the charge balance equation.
May 29, 1994: added nh3 parameter to account for ammonia */
double a na(double po4, double ph, double t, double nh3) {
double-Ti - 1.0/t;
double rho = db rho(t);
double H = pow(10.0, -ph)/rho;
double Kl - pow(10.0, - (-3.900391 + 0.01216*t + 725.804792/t));
double K2 - pow(10.0, - (-2.654013+0.01534*t+1579.171467/t));
double K3 - pow(10.0, - (-3.399999E-3*t + 12.44871));
double Kw - pow (:10.0, - 4.098 + Ti*(-3245.2 + Ti*(2.2362e5 - Ti*3.984e7)) +
1og10(rho)*(13.957 + Ti*(-1262,3 + Ti*8.5641e5)));
double Klnh3 - pow(10.0, - (1.2.423779 + 6.202054E-4*t +
2188.435335/t - 4,325705*1og10(t))):
return(Kw/H - H - nh3*(H/Klnh3)/(1.0 + H/Klnh3) +
+ po4* (K1/H +. 2.0* (Kl/H) * (K2/H) + 3.0* (K1/H) * (K2/H) * (K3/H) ) /
(1.0 + (K1/H) + (Kl/H)*(K2/H) + (Kl/H)*(K2/H)*(K3/H)));
/*e~congruencyratio: exported congruency Na:P04 ratio calculation */
double a congruencyr~atio (E BOILER *blr) if_ (E 1~AN =- blr->po4 0 . 0 >= P04 (b1_ ) ~ i E NAN =_ blr--:>ph ) ~,eturn(E NAN);
..aturn(e na(P04 (blr) , E' PH (blr) , e_~:LxNAN(BDTEMP(b1r), K DEFAULT TEMP) a f:LxNAN(NH3(blr), K DEFAULT NH3) > 7 1?04 (blr) double a napo4ratio(~ BOILER *blr) { /* napo4ratio setpoint for boiler */
' return ((blr->napo4ratio != E NAN) ? (blr->napo4ratio) .
T.',e na(P04SETPOINT(blr), F~HSETPOINT(blr), K DEFAULT TEMP, K DEFAULT NH3)/
~04SETPOINT(blr)) );
double a minnapo4rat.io(E BOILER. *blr) { /*min napo4ratio setpoint for boiler */
return ((blr->minnapo4ratio != E NAN) ? (blr->minnapo4ratio) .
a max(E MINMINNAP04RATI0, a naZP04SETPOINT(blr),PHSETPOINT(blr)-DPHSETPOINT(blr), K DEFAULT 'TEMP, K DEFAULT_NH3) P04SETPOINT(b:Lr)) ) double a maxnapo4rat:..io (E BOILER. *blr) { /*max napo4ratio setpoint for boiler */
return ((blr->maxnapo4ratio != E NAN) ? (blr->maxnapo4ratio) .
e__m i n ( E_MAxMAXNAP04 RAT I O , a na(P04SETI?DINT(blr),FHSETPOINT(blr)+DPHSETPOINT(blr), K DEFAULT TEMP,K DEFAULT_NH3) / P04SETPOINT (b:LrT) ) ; -/* e_fixNAN: replace E NAN's (not a number) with a given value */
double a fixNAN(doub:Le number, double forNAN) {
return (number =- 3~ NAN ? forNAN . number) ;

/* Main Functions */
/*~ '.nit: check that required constants of macc4 are properly specified*/
/* .~.~ould be used in conjunction with a undefined boiler() as defined in macc4.h, e.g..
# include "macc4.h"
E BOILER b1 = *e undefined boiler(); (sets all fields to E NAN (undefined) bl.famin = fam.in; bl.famaX = ... etc. (define the fields)-if (e init(&bl) != E_INITC)K) error ( ) ;
*/
E_INITSTATUS e_init(E_BOILER *blr) b:Lr->initstatus = E INITOK; /* clear initstatus flag */
blr->undoable = E FALSE;
blr->updatestatus = E UPDATEOK;
i:E (blr->t =.- E NAN) blr->initstatus = E MISSING T;
else if (blr->famin =- E NAN) blr->initstatus = E MISSING FAMIN;
else if (blr->famax =- ErNAN) blr->initstatus = E MISSING FAMAX;
e:Lse if (blr->fadef =- E~NAN) blr->initstatus = E MISSING FADEF;
a:Lse if (blr->po4a == E NAN) blr->initstatus = E MISSING P04A;
else if (blr->ratioa== E NAN) blr->initstatus = E MISSING RATIOA;
else if (blr->fbmin =- E~NAN) blr->initstatus = E MISSING FBMIN;
a:Lse if (blr->fbmax =- E'-NAN) blr->initstatus = E MISSING FBMAX;
else if (blr->fbdef =- E NAN) blr->initstatus = E MISSING FBDEF;
else if (blr->po4b -- E NAN) blr->initstatus = E MISSINGrP04B;
else if (blr->ratiob== E_NAN) blr->initstatus = E MISSING RATIOB;
a:Lse if (blr->po4 -- E NAN) blr->initstatus = E MISSING P04;
else if (blr->ph -- E NAN) blr->initstatus = E MISSING~PH;
else if (blr->m -- E NAN) blr->initstatus = E MISSING M;
a:Lse if (blr->bdtemp== E NAN) blr->initstatus = E-MISSING BDTEMP;
a:Lse if (blr->po4setpoint --- E NANI blr->initstatus = E MISSING P04SETPOINT;
else if (blr->minpo4== E_NAN) blr->initstatus = E MISSING~MINP04;
a:Lse if (blr->maxpo4== E NAN) blr->in:itstatus = E_MISSING MAXP04;
else if (blr->phsetpoint--- E NAN &&
blr->napo4ratio -=E_NAN) blr->initstatus = E MISSING RATIOSETPOINT;
#if E ENGINEERING UNITS
else if (blr->spga =- E NAN) blr->initstatus = E MISSING SPGA;
e:Lse if (blr->spgb =- E_NAN) blr->initstatus = E'MISSING SPGB;
#endi f else { /* if we got this far, all required fields rave been specified */
/*,-~~~fine initial feed program as feeding the default feed rates */
blr->dt [0] - blr->dt [1] _ t) ., 0;
blr->fa [0] - blr->fa [1] - blr->fa [2] - blr->fadef ;
blr->fb [0] - blr->fb [1] .- blr->fb [2] - blr->fbdef;
blr->lastt = bl.r->t; /* set origin of time, po4, and ph */
blr->lastbdtemp = blr->bdte:mp;
blr->lastpo4 = blr->po4;
blr->lastnh3 = :olr->nh3;
blr->lastph = blr->ph;
/* provide defaults for the optional parameters if they are undefined */
blr->fdt = a fix:L~AN(blr->fdt, 0.0); I*assume instantaneous pump changes*/
blr->dpo4 = a fixNAN(blr->dpo4, DEFAULT DP04);
blr->dph = a ~i:K~lAN(blr->dph, DEFAULT DPH) ;
blr->dphsetpoint = a fixNAN(blr->dphsetpoint, DEFAULT DPHSETPOINT);
blr->max sample interval = a fixNAN(blr->max_sample~interval, DEFAUL3' MAX S~.'~IPLE INTERVAL) ;
blr->max outo~bo:x adjustment =
a fixNAN(blr-:>max outofbox adjustment, DEFAULT MAX-OUTOFBOX ADJUSTMENT);
blr->beps = a fi:xNAN(blr->beps, DEFAULT HEPS);
blr->naleakmaX := a fixNAN(blr->naleakmax, 1.0e3 * (FAMAX~blr)*fabs(NAA(blr)) + FBMAX(blr)*fabs(NAB(blr)) +
FAM,AX(blr)*fabs(PO4A(blr)) + FBMAX(blr)*fabs(P04B(blr))));
blr->naleakmin = a fixNAN(blr->naleakmin, - blr->naleakmax);
blr->bdmax = a :'ix~lAN(blr~->bdmax, blr->m/1.0) ;
/*default bdmax empties boiler in 1 hr*/
blr->bdmin = a ~ixNAN(blr~->bdmin, l.Oe-6*blr->m/1.0);
/* defaultf bdmin is blowdown rate needed for a time constant of 1e6 hours */
blr->max relatives error = a fixNAN(blr->max relative error, - - DEFAUL2 MAX RELATIVE ERROR);

/* apply consistency checks to the constants of the macc4 model */
if (HDMAX(blr) < BDMIN(blr)) ,* blr->initstatus = E BDMAX LT BDMIN;
lse if (NALEAKMAX(blr) < DJALE~KMIN(blr)) blr->initstatus = E NALEAKMAX LT NALEAKMIN;
else if (FAMAX(blr) --FAMIDT(blrj <=
DBL EPSILON * (fabs (FAMAX (blr) ) +fabs (FAMIN (blr) ) ) ) blr->initstatus = E_FAMA~; LE FAMIN; j* max pump rates less than min */
else if (FBMAX(blr) - FBMIDJTblr) <_ DBL EPSILON*fabs(fabs(F'BMAX(blr))+fabs(FBMIN(blr)))) blr->initstatus.= E FBM~, LE FBMIN; /* max pump rates less than min */
else if (MINP04(blr), < E MI:NMINP04) blr->initstatus = E MINP04 LT MINMINP04;
else if (MAXP04 (blr) > E W~XMAXP04) blr->initstatus = E MAXP04 GT MAXMAXP04;
else if (P04SET:POINT(blr) <: MINP04 (blr) ) blr->initstatus = E P04SE:TPOINT LT MINP04;
else if (P04SETPOINT(blr) , MAXP04(bIr)) blr->initstatus = E P04SF;TPOINT GT MAXP04;
else if (P04A(b:Lr) < 0.0) blr->initstatus = E P04A LT ZERO;
else if (P04B (b:Lr) < ~. 0) --blr->initstatus = E P04B LT_ZERO;
else if (e minnapo4ratio(b7.r) < E MINMINNAP04RATI0) blr->initstatus = E MINRATIO LT_MINMINRATIO;
else if (e maxna:po4ratio (b7.r) > E MAXMAXNAP04RATI0) blr->initstatus = E MAXRATIO_GT MAXMAXRATIO;
else if (e napo4ratioTblr) < a minnapo4ratio(blri) blr->initstatus = E RATIO LT'_MINRATIO;
else if (e napo4ratioTblr) :> a maxnapo4ratio(blr)) blr->initstatus = E RATIO GT MAXRATIO;
else if (fabs(NAA(blr~*P04BTblr) - NAB(blr)*P04A(blr)) <_ DBL EPSILON * (fabs (NAA(blr) *P04B (blr) ) ~ fabs (NAB (blr) *P04A(blr) ) ) ) blr->initstatus = E RATIOA EQ RATIOB; /* both tanks have same na/po4 rati else {/*initial blowdown, 7.eak only used for testing--reasonable defaults*/
blr->1 = a fix~lAN(blr->:1, 0.0);
blr->bd = a f.ixNAN(blr->bd, USERBD((FADEF(blrT*:P04A(blr) + FBDEF(blr)*P04B(blr))/P04SETPOINT(blr)));
re=turn(blr->initstatus);

/* e~_getfeed: retrieves feed rates for pumps a or b for a given time */
doui ' e_getfeed(E BOILER *blr, double *f) {
double deltat :- T (blr) - L~ASTT (blr) ;
if (deltat < RO1?NDED_DT(blr,0))/*use appropriate part of feed program */
return(f [0] ) ;
else if (deltat < ROUNDED_DT(blr,0) + ROUNDED_DT(blr,l)) return(f [1] ) ,;
else return(f [2] ) ;
double a afeed(E BONER *blr) { /* retrieves pump a feed rate */
returnZe getfeed(b:Lr, blr->fa));
double a bfeed(E BOILER *blr) { /* retrieves pump b feed rate */
re:turnTe getfeed (b:Lr, blr->:Eb) ) ;
/*e-rpo4pred: predicted po4 concentration */
double eJpo4pred(E BOILER *blr) {
double po4 == i~P.STP04 (blr) ;
double deltat::ime = T (blr) - LASTT (blr) ;
int i = 0;
double deltat[3];
deltat[0] - e__min(deltatime,ROUNDED DT(blr,0));
deltat [1] - a min (deltatime - deltat (O] , ROUNDED DT (blr, 1) ) ;
deltat [2] - di~ltatime ~- deltat [0] - deltat [1] ;
for (i = 0; i < 3; i++) {
po4 = ( (FA(blr, i) *PO~A~blr) +FB (blr, i> *P04B (blr) ) /BD (blr) ) (1.0-exp(- deltat [i] *BD(blr) /M(blr) ) ) +
po4*exp ( - deltat [i] *BD (blr) /M (blr) ) ;
}eturn(po4);

/* F~ estimate bd: estimate blowdown (B) via bisection */
dour ~ e_estimate bd(E BOILER *blr) {
int bisections :- E MAXBISECTIONS;
double Blow = blr=>bdmin;
double Bhigh = blr->bdmax;
blr->bd = 0.5 * (Blow + Bhigh);
while (bisections-- && (Bh.igh - Blow) >
blr->bep:a* (fabs (blr->bdmax) + fabs (blr->bdmin) ) ) {
if (e~ o4pred(blr) > P04(blr)) Blow = bl:~->bd; !* estimated po4 too high - Blowdown too low */
else Bhigh = b:lr->bd; %* estimated po4 too low - Blowdown too high */
blr->bd = O.a * (Blow + Bhigh);
/* f:lag if the final. low, high don't actually bracket the root */
blr->bd = Blow;
if (e po4pred(bar) < P04(blr)) blr->updatestatus ~= E_INCONSISTENT-P04;
blr- >Sd = Bhigh ;
if (e_po4pred(b:Lr) > P04(blr)) blr->updatestatus ~= E'INCONSISTENT-P04;
return (blr->bd :- 0 . 5 * (~3low + Bhigh) ) ;
/*e~napred: predicted na concentration */
doux>le a napred(E BO:CLER *blr) {
double na == e: na(LASTP04(blr), LASTPH(blr), LASTBDTEMP(blr), LASTNH3(blr));
double deltatime = T(blr) - LASTT(blr);
int i = o;
double deltat[3];
deltat [0] - e__min (deltatime, ROUNDED DT (blr, 0 ) ) ;
deltat (1] - a min (deltatime - deltat (0] , ROUNDED~DT (blr, 1) ) ;
deltat [2] = dEeltatime - deltat ~O] -~ deltat (1] ;
for (i=0; i < 3; i++) (, na = ( (FA(b:Lr, i) *NAAi,b~lr) + FB (blr, i) *NAB (blr) +L (blr) ) /BD (blr) ) (1.0-exp( - deltat(i]*BD(blr)/M(blr))) +
na*exp ( - dE~ltat [i] *BD (blr) /M (blr) ) ;
return(na);

/* = estimate_1: estimate na "leak" (L) via bisection */
don ' a estimate 1 (E BOILER *blr) int bisections = E MAXBISECTI01VS;
double na = a na(P04(blr), PH(blr), BDTEMP(blr), NH3(blr));
double Llow = bIr->naleakmin;
double thigh = blr->naleakmax;
blr->1 = 0.5*(Llow+Lhigh);
while (bisections-- && (thigh - Llow) >
blr->beps* (fabs (blr->naleakmax) + fabs (blr->naleakmin) ) ) {
if (e napred(blr) > na) /* estimated na too high - na Leak too high */
thigh = blr->1;
else /* estimated na too low - na leak too low */
Llow = blr->1;
blr->1 = 0.5*(Llow+Lhigh);
/* flag if the final low, high don't actually bracket the root */
blr->1 = Llow;
if (e napred(blr) > na) blr->updatestatus ~= E-INCONSISTENT_PH;
blr->I = thigh;
if (e napred(blr) < na) bl.r->updatestatus ~= E_INCONSISTENT_PH;
return(blr->1 = 0.5*(Llcw+Lhigh));
/* e~phmodel: computes the current model predicted pH for given boiler.*/
#define LOGBASE20FRELATIVEPHERROR 64 /*works out to less than 1e-16 pH units*/
double e~hmodel(E BOILER *blr) {
double minph = 1.0;
double maxph = 14.0;
double ph = 0.5*(minph + maxph);
double po4 = e_po4pred(blr);
double na - e,napred(blr);
int i = 0;
for (i=0; i < LOGBASE20FRELATIVEPHERROR-; i++) {
if (e na(po4,ph,BDTEMP(blr), NH3(blr)) > na) maxph = ph;
else minph = ph;
~ ph = 0.5*(minph + maxph);
return(ph);
/*e_po4model: computes the current model predicted po4 for given boiler*/
double a po4model(E BOILER *blr) {
return (USERP04 (e~o4pred(b:lr) ) ) ;

/* Note on our repre:sentatior~ of convex polygon regions:
. ~nvex polygon regions with .N vertexesr p1, p2, ... pn are represented by . 1 points, R [0] , R [1] , . .. . , R [N] as follows :
R[0] - p1; F.[1] - p2; .... R[N-1] - pn; R[N] - p1;
Here pl,p2; p2,p3; ... pn,pl form the edges of the convex polygon region.
We pass the array, R and number of vertexes, N, when passing a convex polygon region to a function; note that it takes an array of N+1 points to represent a convex polygon region with N distinct vertexes. (You can think of the last point as a delimeter, just like the 0 at the end of strings). */
/* a perimetercrossings: determines points where a given line intersects the perimeter c~f .a given convex polygon region; returns number of intersection points */
int e_perimetercrossings(E_VECTOR *vX, /* 2 element array of intersection pts*/
E VECTOR *vV, /* V [i] , V [i+1] are edges; V [NV] ==V [0] */
int NV, /* number of vertexes in region */
E VEC'rc~R *vPl,/* line that (may) intersect region */
E~_VECTOR *vP2)/* P1 <> P2 is required ! */
{ int i = 0, N = 0;
double lastalpha, lastbeta, alpha, beta;
a decompose(&vV[0], vPl, a vSv(vP2,vP1), e-rot90(e,vSv(vP2,vP1)), &lastalpha, &lastbeta);
for (i=0; i < NV; i++) a decompose(&vV[i+1],{vP7., a vSv(vP2,vP1), a rot90(e vSv(vP2,vP1)), &alpha, &beta);
if (lastbeta*beta <= 0.0 && /* does edge crosses given line ? */
(fabs(lastbeta) > 0.0 ~~ fabs(beta) > 0.0 ) ) {
E VECTOR *X = a vPv(&vV[i], a sXv(fabs(lastbeta)/(fabs(lastbeta)+fabs(beta)), a vSv (&vV [i+1] , &vV [i] ) ) ) ;
if (N
vX [N++] - *X.;
else if (e between(&vXl:l;l, ~vX[OJ, X)) vX[0] _ *X;
else if (e between ( &vX I:0] , &vX [1] , X) ) vX [1] _ *X;
/* else first 2 intersection points are already the furthest apart */
la}tbeta = beta;
eturn (N) ;

/* a within~region: is the given point within (or on perimeter of) region ? */
E_l-~ICAL a within oegion(E_VECTOR *vP, E-VECTOR *vR, int N) ECTOR vX [ 2 ] ;
it ( a vEv (vP, &vR [0] ) ) return(E TRUE);
else if (e_perimet:ercrossings(vX, vR, N, vP, &vR[0]) > 1 &&
E TRUE ~_= e_between (&vX [0] , vP, &vX [1] ) ) return(E_TRUE);
else return(E_FALSE);
/*
a clip_region: clips the part of the convex polygon region, R, tFlat is to your left if you are standing at P1 and walking towards P2 (or walking backwards from P2 towards P1). Assumes P1 <> P2.
*/
int a clip_region(E VECTOR'*vC, /* clipped region (output) */
- E-VECTOR *vR, /* original region (input) */
in.t N, /* number of distinct points on perimeter */
E VECTOR *vPl, E VECTOR *vP2) { /* clipping line */
double alpha, beta, lastbet:a;
int i = 0, j - 0;
a decompose(&vR[0], vPl, a vSv(vP2, vPl), e,rot90(e vSv(vP2, vPl)), &alpha, &beta);
for (i=0; i < N; i++) if (beta <= 0) /* to{the right of, or on, the clipping line through P1,P2*/
vC [j++] - vR [i] ;
lastbeta = beta;
a decompose(&vR[i+1], vPl, a vSv(vP2,vP1), a rot90(e,vSv(vP2, vP1)), - &al:pha, &beta);
if (lastbeta*beta < 0.0) /* Segment R[i],R[i+1] crosses clipping line: */
vC[j++] - *e vPv(&vR[i], /*add point where it crosses to clipped region*/
- e_sXv (fabs (lastbeta) / (fabs (lastbeta) +fabs (beta) ) , a vSv(&vR[i+1] , &vR[i] ) ) ) ;
vc~[j] - vC[0]; l* "close the loop" to form the convex polygon region */
return (j);

e_intersect regions: determines the region, AB, which is the '~'tersection of the two given convex polygon regions, A and B. The _~rst region, A, must be oriented such that moving from vertex A[i]
to vertex A[i+1] places the outside of the region to one's left (in other words, you are moving clockwise around the perimeter).
/*
IMPORTANT: AB(] MUST contain at least NA+NB+1 elements, which is the maximum possible number of points needed to define the region of intersection, or memory will be trashed. Function returns the number of distinct vertexes in the resulting, intersecting, region.
For more information, see section 3,:14.1, "The Sutherland-Hodgman Polygon-Clipping Algorithm", of the second edition of Foley, et. al's "Computer Graphics" 1990, Addison-Wesley.
*/
int e_intersect-regions(E VECTOR *vAH, /* intersecting region (returns NAB) */
E VECTOR *vA, /* points representing the 1st region */
int NA, /* number of vertexes in 1st region */
E VECTOR *vB, i* points representing the 2nd region */
int NBA { /* number of vertexes in 2nd region */
int l = 0, j - 0, N = NB;
for (l = 0; l < NA; i++) for (j = N; j >= 0; j--) { /* copy/move region to be clipped to end of AB*/
vAB (NA+NB-N+j ] - (i==0) ? vB [j ] . vAB [j ] ; /*AB [] MUST have NA+NB+1 elems*/
N = a clip-region(vAB, vAB+NA+NB-N, N, &vA[i], &vA[i+1]);
return (N) ;
/* e-point2chord: return point on the chord (line-segment) closest to P */
E VECTOR *e~point2chord(E VECTOR *vP, E VECTOR *vLl, E VECTOR *vL2) i:E (e vEv(vLl, vL2)) return(vLl);
e:Lse {
double alpha, beta;
a decompose(vP, vLl, a vSv(vL2, vLl), a rot90(e vSv(vL2, vLl)), &alpha, &beta);
return(e vPv(vLl, a sXv(e max(0.0, a min(1.0, alpha)), a vSvl.vL2, vLl))));
- _ _ _ -/* a region2region: given 2 non-intersecting convex polygon regions, returns tfie point on region A that is closest to (the perimeter of) region B */
E_v _TOR *e region2re~ion(E VECTOR *vA, int NA, E VECTOR *vB, int NB) {
E VECTOR vminA = vA 0], vminB = vB[0], vX;
int i = 0, j - 0;
for (i=0; i < NA; i++) {
for (j = 0; j < NB; j++) {
vX = *e_point2chord(&vA[i] , &vB[j] , &vB[j+1] ) ;
i f ( a norm ( a vSv ( &vA [ i ] , &vX ) ) < a norm ( e,vSv ( &vminA, &vminB ) ) ) vminA = vATi ] ;
vminB = vX;
for (i=0; i < NB; i++) < NA; j++) {
fovX(~ *e~oint2chord(&vB Li] , &vA[j] , &vA[j+ll ) ;
if (a norm(a vSv(&vX, &vB[i])) < a norm(a vsv(&vminA, &vminB)>) {
vminA = vX;
vminB = vH [i] ;
}~~turn(e vec(vminA.po4,vminA.na));
# define E REPS 5 ;* number of replicates for sensitivity analysis */
/* ce_pertubate: perturbs field in boiler structure for sensitivity analysis */
void e_perturbat{(E BOILER *blr, int index, double multiplier) switch(index) case 0: blr->lastpo4 +_ (multiplier*b1r->dpo4);
break;
case 1: blr->lastph +_ (multiplier*blr->dph);
break;
case 2: blr->po4 +_ (multiplier*blr->dpo4);
break;
case 3: blr->ph +_ (multiplier*blr->dph);
break;
default: break;

/* a backup: stores E BOILER'S current state for later use by a undo */
vor °_ backup(E BOILER *blr) {
i.__ i = 0;
for (i=0; i < 2; i++) {lr->lastdt [i] - blr->dt [i] ;
for (i=0; i < 3; i++) blr->lastfa [i] - blr->fa [i] ;
blr->lastfb [i] - blr->fb [i] ;
blr->b4lastt - blr->lastt;
b:Lr->b4lastbdtemp =.blr->lastbdtemp;
b:Lr->b4lastpo4 - blr->lastpo4;
b:Lr->b4lastnh3 - blr->lastnh3;
blr->b4lastph - blr->lastph;
b:Lr->lastbd - blr->bd;
b:Lr->lastl - blr->l;
for (i=0; i < E NV+1; i++) blr->vlastV [i] - blr->vV [i] ;
for (i=0; i < E NW+1; i++) blr->vlastW[i] - blr->vW(i];
for ( i=0; i < 3 ; i++) blr->vlastF [i] - blr->vF [i] ;
b:Lr->lastupdatestatus = blr->updatestatus;
blr->undoable = E TRUE; /* since we are backed up, an undo is now possible */
%* s: undo: undoes the effect of the last a update(); 1 update can be undone.
*/
E LOGICAL a undo(E BOILER *blr) {
-if (E FALSE =- bIr->undoable) return(E FALSE);
e:Lse {
int i = 0;
for (i=0; i < 2; i++) blr->dt [i] - blr->lastdt [i] ;
for (i = 0; i < 3; i++) blr->fa(i] - blr->last~a[i];
blr->fb [i] - blr->lastfb [i] ;
blr->lastt - blr->b4lastt;

blr->lastbdtemp- blr->b4lastbdtemp;

blr->lastpo4 - blr->b4lastpo4;

blr->lastnh3 - blr->b4lastnh3;

blr->lastph - blr->b4lastph;

blr->bd - blr->lastbd;

blr->1 = blr->lastl;

for (i=0; i NV+1; i++) blr->vV[i] - blr->vlastV(i];
< E

for (i=0; i NW+1; i++) blr->vW[i] - blr->vlastW[i];
< E~

for (i=0; i i++) blr->vF[i] - blr->vlastF[i];
< 3;

blr->updatestatus = blr->lastupdatestatus;

blr->undoable = E_FALSE; ,!* no more than 1 undo between updates */
return(E TRUE);

/* E~ update: update the feed program associated with a boiler */
E_L" ~.TESTATUS a update(E BOILER *blr) int l = 0, i0 = 0, NV = E NV, NW = E NW, NVW = 0;
E__VECTOR vfamin, vfamax, vfbmin, vfbmax, vL;
E_VECTOR *vV=blr->vV, vminV, *vW=blr->vW, vminW, vVW[E NV+E NW+1];
E__VECTOR vP, vT, uSS;
E VECTOR *vF = blr->vF, vX[2];
double fa [E REPS] , fb [E REPS] ;
double ssq = 0.0;
a backup(blr); /* backup the current state for undo purposes */
b'Ir->updatestatus = E UPDATEOK; /* clear any previous status */
/*define the target wedge (the "box" in coordinated phosphate/ph lingo)... */
/*N.B: the region described by vW must be oriented in t:ae clockwise direction, c.f. comment at top of e_intersect regions() */
v1N[0] - *e vec(MINP04(blr), MINP04(blr)*e minnapo4ratio(bIr));
vW [1] - *e vec(MINP04(blr), MINP04(blr)*e maxnapo4ratio(blr));
vW [2] - *e vec (MAXP04 (blr) ,. MAXP04 (blr) *e maxnapo4ratio (blr) ) ;
viN[3] - *e vec(MAXP04(blr), MAXP04(blr)*e minnapo4ratio(blr));
vw [4l - ~wTo] ;
/* ... and target point */
v'T = *e vec(P04SETPOINT(blr)" P04SETPOINT(blr)*e napo4ratio(blr));
/* ;estimate the steady-state boiler concentration variability due to */
/* v~he given variability of P04 (dpo4) and pH (dph), via a propagation */
/* of errors calculation. We assume that the impacts of the errors in */
/* lastpo4, lastph, po4, and ph on steady state boiler concentrations */
/* .are independent; since this is not strictly true, the calculation */
/* below will tend to overestimate the resulting error on steady state */
/* ;toiler concentrations. But :it should be a reasonable estimate. */
i0 = ((DP04(blr)<=0.0 && DPH(blr) <= 0.0) ? (E REPS-1) ~ 0);
for (l = i0; l < E REPS; e~perturbate(blr, i++; -1.0)) /* this loop evaluates steady-state feed rates using pertubated inputs */
a perturbate(blr, l, 1.0); /* varies one of: lastpo4,lastph,po4,ph *~
bIr->bd = e_estimate_bd(blr); /* (if l = E_REPS-i then it varies nothing)*i blr->1 - a estimate 1(blr>;
vfamin = *e vet (FAMIN (bl.r) *P04A(blr) ; BD (blr) , FAMIN(bl.r) *NAA(blr) /BD(blr) ) ;
vfbmin = *e_vet(FBMIN(bl.r)*P04B(blr)/BD(blr), FHMIN (bl.r) *NAB (blr) /BD (blr) ) ;
vfamax = *e vet (FAMAX (blr) *P04A (blr) /BD (blr) , FAMAX (b~..r) *NAA (blr) /BD (blr) ) ;
vfbmax = *e vet (FBMAX (b7~.r) *P04B (blr) /BD (blr) , - FBMAX (b'_.r) *NAB (blr> /BD (blr) ) ;
vL - *e vet (0 . 0, L (xalr) /BD (blr) ) ;
a decompose (&vT, ~vL, E~ vDs(&vfamax, FAMAX(blr)), a vDs (&~r~~bmax, FBMAX (blr) ) , &fa [l] , &fb [l] ) ;

nor (ssq = 0.0, i - iG; i < E REPS-1; i++) { /* sum up the squared errors*/
ssq += e_norm2( /* on steady-state boiler */
e_vSv(&vT, /* concentrations due to */
ewPv(&vL, /* the various perturbations*/
e_vec ( ( fa [i] *P04A (blr) +fb [i] *P04B (blr) ) /BD (blr) , (fa [i] *NAA(blr) +fb [i] *NAB (blr) ) /BD (blr) );
if ( sqrt(ssq) > MAX RELATIVE ERROR(blr) * a norm(&vT) ) {
blr->updatestatus ~= E INDE~'ERMINATE; /* variability of steady-state */
blr->updatestatus - E~NOTUPDATED; /* boiler concentrations is too big*/
if ((E INCONSISTENT P04 .~ E INCONSISTENT PH) & blr->updatestatus) blr->updatestatus ~= E;~NOTUPDATED; /* feed program not updated */
/* if P04 or pH are inconsistent*/
/* with blowdown or Na leak ranges*/
if (E NOTUPDATED & blr->updatestatus) goto nochange;
/* definethe steady state feasable region in boiler conc space....
*/

vV [ 0 - *e vPv ( a vF>v ( &vfamin,&vfbmin) ) ;
] &vL, vV[1] - *e-vPv(&vL, e-vPv(&vfamax,&vfbmin));
-vV[2] vPv(&vL, vPv(&vfamax, &vfbmax));
- *e- e vV[3] _ _ &vfbmax));
- *e vPv(&vL, vPv(&vfamin, e vV [4 - vVTO ] ; _ ]

/* ...
and the current point */

vP - *e vec(P04(blr),a na(P04(blr) ,PH(blr), BDTEMP(blr), NH:3(blr)));

/* compute the steady-state feed rate, defined to be the point within the feasable region that is closest to the target wedge, and, secondarily, if the feasable region and target wedge overlap, closest to the target point.*/
if ((NVW=a intersect regions(vVW, vW, NW, vV, NV)) > 0) {
/*regions intersect */
if (e within-region(&vT, vVW, NVW)) vSS = vT;
else /* target not in feasable region*/
vX~O] - vX[1] - vT;
vSS = *e region2re ion(vVW, NVW, vX, 1);
blr->updatestatus ~= E_POINT UNMAINTAINABLE;
else /* regions are disjoint*/
blr{>updatestatus E POINT UNMAINTAINABLE;
blr->updatestatus I= E BOX_UNMAINTAINABLE;
vSS = *e region2regionwV, NV, vW, NW);
blr->dt[0] - blr->dt[1] - 0.0; /'* set default feed program to */
vF [O] - vF [1] - vF [2] - vSS; / * steady state feed rates */

/* Adjust na/po4 ratio (stage :L of feed program) */
(E FALSE==a within-region(&vP, vW, NW)) ( /* if already in the box */
lnt i = 0, j-= 0; /* no stage 1 adjustment required */
E LOGICAL region unreachable = E_TRUE;
for (i=0; i < NV; i++) {
if ( ! a vEv (&vP, &vV [i] ) &&
e_perimetercrossin.gs(vX, vW, NW, &vP, &vV[i]) > 1) {
for (j=0; j < ~% ~++) { &vV[i] ) ) if (e between(~vP, &vX[j], {
if Tregion unreachable) vminV = vV [i] ;
vminw = vX[j];
region_unreachable = E_FALSE;
}lse i.f (e norm(e vSv(&vV[i],&vX[j]))*e norm(e vSv(&vminV,&vP)) >
a norm(e~_ vSv(&vminV, &vminW) ) *e-_norm(e-_vSv(&:vV[i] , ~vP) ) ) {
vminV = vV [i] ; -vminW = vX [ j ] ;
for (i=0; i < NW; i++) {
if (!e vEv(&vP, &vW[i] ) &&
a erimetercrossings(vX, vV, NV, &vP, &vW[i]) > 1) {
for ~=0; j < 2; j++) {
if (e between(&vP, &vW[i] , ~vX[j] ) ) {
if region unreachable) {
vminV = vX[j];
vminW = vW [ i ] ;
region unreachable = E FALSE;
_ _ else if (e norm (e vSv(&vX[j] ,~vW[i] ) ) *e norm (e vSv(&vminV, &vP) ) >
e~norm(.ewSv(&vminV,&vminW)) *e norm(e vSv(&vX[j],&vP))){
vminV = vX [ j ] ;
vminW = vW [ i ] ;
}f (region unreachable==E TRUE ~~
( e_norm ( a vSv ( &vminV , ~&vminW ) ) < _ .
2 0*DBL I~IN*e norm(e vSv(&vminV, &vP)))) blr{>updatestatus ~= E_BOX UNREACHABLE;
else blr->dt [0] - - (M (blr) /BD (blr) ) loge norm(e vSv(&vm~.nV,&vminW))/e_norm(e vSv(&vminV, &vP)));
vF [ 0 ] - vminV;
vP = vminW;
if (DT(blr,0) > MAX_OUTOFBOX ADJUSTMENT(blr)) {
blr->updatestatus ~= E OUTOFBOX TOOLONG;
_ _ /* :Stage 2: move to the setpo:int po4 and NA/po4 ratio if you can */
if vEv(&vP, &vT)) i~_ ve~ erimetercrassin s(vX, vV, NV, &vP, &vT) <= 1) b{r->updatestatus ~= E_POINT UNREACHABLE;
a:Lse int i = 0;
E LOGICAL point unreac{able = E_TRUE;
for (i=0; i < 2; i++) if (e between (&vP, &vT, &vX [i] ) ) {
if Tpoint unreachable; {
vminV =-vX [i] ;
point unreachable = E_FALSE;
}lse if (e norm(e vSv(&vX[i] , &vT) ) *e norm (e vSv(&vminV, &vP) ) >
e-norm(e-vSv(&vminV,&vT))*e-norm(e-vSv(&vX[i],&vP))) vminV = vX [ i ] ;
}f (E TRUE==point unreachable a norm(e vSv(&vminV,&vT)) <= 2.0*DBL MIN*e norm(e vSv(&vminV, &vP))) bTx->updatestatus ~= E_POINT_UNREACH~BLE;
else blr{ >dt [1] - - (M (blr) jBD (blr) ) log(e_norm(e,vSv(&vmi:nV,&vT))/e~norm(e vSv(&vminV, &vP)));
vF[1] - vminV;
for (i=0; i < 3; i++) { /* determine feed rates needed to attain feed points */
a decompose(&vF[i], &vL, a vDs(&vfamax,blr->famax), -- a vDs (&vfbmax,Flr->fbmax) , &blr->fa [i] , &blr->fb [i] ) ;
b:Lr->fa [i] - a forceinrange (blr->fa [i] , b1r->famin, blr->famax) ; /*fixes up*/
b:Lr->fb[i] - e-forceinrange(blr->fb[i],blr->fbmin,blr->fbmax);/*roundoff*/
if (T(blr) - LASTT(blr) > MAX SAMPLE INTERVAL(blr)) b:Lr->updatestatus ~= E_SAMPLEINTERVAL,TOOLONG;
blr->lastt - blr->t; /* make prey current values last. values */
blr->lastpo4 - blr->po4;
blr->lastnh3 - blr->nh3;
blr->lastph - blr->ph;
blr->lastbdtemp - blr->bdtemp;
noclzange: /* if we jump to here, feed program not updated*/
return(blr->updatestatus);

con:.=t E
BOILER
*e undefined boiler(void) static const E
BOILER
E_UNDEFINED
BOILER
=

NAN,E NAN,E NAN,E NAN,E NAN, NAN,E NAN,E
NAN,E
NAN,E
NAN,E_NAN,E
NF~""''E' E
~

_ NAN,E NAN,E NAN,E NAN,E
_ NAN,E NAN, _ _NAN,E
NAN,E
N~, NAN,E
NAN,E~~NAN,E
NAN,E
E

_ NAN,E NAN,E NAN,E NAN,E
_ NAN.E NAN, NAN,E
NAN,E
NAN,E
NAN,E
NAN,E~NAN,E
NAN,E
E

_ _ _ _ NAN,E ~NAN,E NAN,E NAN,E
_ NAN,E NAN, E
N.AN,E
NAN,E
NAN,E
NAN,E
NAN,E
NAN,E
NAN,E

x.0,0.0} ,{E E NAN , - _ NAN,E NAN,~
L~1AN,E NAN,E
I~AN~, -0.0,0.0} ~ NAN ~ , (E E NAN
NAN,E E -NAN,E NAN.E
NAN
, o.o,o.o, o.o,o'.o, o.a,o.o~, o.o;o.o, o.o,o.o , ~

o.o,o.o o.o,o.o , o.o,o.o;, o.o,o.a, o.o,o.o , , ~

o.o,o.o 0.0,0.0 , 0.0,0.0~ 0.0,'0.0, 0.0,0.0 , , ~

0.0,0.0 O.O,o.O ~ , ~ 1~

~0.0,0.0 . ~ ~ . ~ , , , , ~
. ~

FALSE};
E
UPDATEOK, E
UPDATEOK, E
INITOR, E

-return(&E
UNDEFINED
BOILED);

/* a hpm2po4: converts hmp (expressed as % by weight) to ortho P04 */
/* (also in % by weight) */
double a hmp2po4(double hmp) returnZ0.93135 * hmp); /* conversion factor provided by SMB Aug 4 */

/* macc4ssp.h */
#def IDC PASSWORD 155 ~

#defineIDC 156 LOCKWINDOW

#defineIDC 157 DATETIME

#defineMACC4SSP 2 ICON

#defineMACC4SSP 1 DIALOG

#defineIDC GROUPBOX1 101 #defineIDC _ _BD 152 #defineIDC BDMIN 131 #defineIDC BDMAX 132 #defineIDC _ #defineIDC _ 127 BDTEMP

#defineIDC DPHSETPOINT126 #defineIDC _ 125 PHSETPOINT

#defineIDC DPH 124 #defineIDC PH 123 #defineIDC MAXP04 120 #de:fineIDC MINP04 119 #de:fineIDC P04SETPOINT118 #de:fineIDC DP04 117 #de:fineIDC P04 116 #de:fineIDC SPGA 146 #de:fineIDC SPGB 114 #de:fineIDC P04B 112 #de:fineIDC FBDEF 111 #de:fineIDC FBMAX 110 #de:fineIDC FBMIN 109 #de:fineIDC RATION 107 #de:EineIDC RATIOA 106 #de:fineIDC P04A 105 #de:fineIDC FADEF 104 #de:fineIDC STATUSFLAGS151 #de:fineIDC POINTNAMES 154 #de:fineIDC START 121 #de:fineIDC _ 147 SIMULATESSP

#de:EineIDC NH3ENABLED 162 #de:fineIDC NH3TEXT 163 #de:fineIDC NH3 164 #defineIDC AFEED 200 #defineIDC BFEED 201 #defineIDC FAMAX 103 #defineIDC FAMIN 102 #defineIDC GROUPBOXS 129 #defineIDC MESSAGELINE145 #defineIDC LOGTOFILE 113 #defineIDC GROUPBOX6 148 #defineIDC ENGINES 144 #defineIDC ENGINEB 143 #defineIDC 142 ENGINE?

#defineIDC ENGINE6 141 #defineIDC ENGINES 140 #defineIDC ENGINE4 139 #defineIDC ENGINE3 138 #define_ ENGINE2 137 IDC

#defineIDC ENGINE1 136 #defineIDC ENGINEO 135 #defineIDC STOP 122 #defineIDC UNDO 204 #defineIDC REINIT 203 #defineIDC UPDATE 202 #defineIDC LOCALINPUT 158 #defineIDC FORECASTTIME

#defineIDC FORECAST 150 #deLineIDC GROUPBOX4 -1 #defineIDC GROUPBOX3 2.15 #def'~.IDC GROUPBOX2 108 #def_._aIDC FWFLOW 128 #defineIDC FWNA 161 #defineIDC MAXFWNA 160 #def:ineIDC MINFWNA 159 #def:ine IDC AFEED 200 /* SSP communication id's */
#def:ine IDC BFEED 201 /*lIDC_PH, IDC_P04, and IDC,BDTEMP also used)*/
#def:ine IDC FLAG1 205 #def:ine IDCiFLAG2 206 #def:ine IDCiFLAG3 207 #def:ine IDC FLAG4 208 #def:ine IDC FLAGS 209 #de f: ine IDC_FLAG6 210 #def:ine IDC FLAG? 211 #def:ine IDC FLAGB 212 #def:ine IDC_FLAG9 213 #def:ine IDC FLAG10 214 #def:ine IDC FLAG11 215 #def:ine IDC FLAG12 216 #def:ine IDC RATIO 217 #def:ine IDC T 300 /* E_BOILER field ids that do not have */
#def:ine IDC NAP04RATI0 301 /* a control associated with them */
#def:ine IDC MINNAP04RATI0 302 #define IDClMAXNAP04RATI0 30:3 #def:ine IDC MAX SAMPLE INTERVAL 304 #def=ine IDC MAX OUTOFBOX ADJUSTMENT 305 #def:ine IDC MAX RELATIVE~ERROR 306 #define IDC BEPS 307 #define IDC FDT 308 #define IDC LASTT 309 #define IDC LASTBDTEMP 310 #define IDC LASTP04 311 #dei=ine IDC LASTPH 312 #dei=ine IDC B4LASTT 313 #dei=ine IDC B4LASTBDTEMP 314 #de:°ine IDC B4LASTP04 315 #define IDC~B4LASTPH 316 #de:Eine IDC LASTED 319 #de:Eine IDC LASTL 320 #de:Eine IDC DT1 321 #de:fine IDC DT2 322 #de:fine IDC FA1 323 #define IDC FA2 324 #define IDC~FA3 325 #define IDC FB1 326 #define IDC FB2 327 #define IDC~FB3 328 #define IDC LASTDT1 329 #define IDC LASTDT2 330 #define IDC LASTFA1 331 #define IDC LASTFA2 332 #define IDC LASTFA3 333 #define IDC LASTFB1 334 #define IDC LASTFB2 335 #define IDC LASTFB3 336 #define IDC~UPDATESTATUS 337 #define IDC LASTUPDATESTATUS 338 #define IDC_INITSTATUS 339 #define IDC UNDOABLE 340 #de:fine IDC_LASTNH3 341 #de:fine IDC B4LASTNH3 342 /* misc/MACC4SSP specific/state/control/error ids */
#de~ °_ IDC CURRENTENGINEID ~:~01 #dei~ae IDC~RUNNING 402 #define IDC FLAGSUPTODATE 403 #define IDC ACTIONSUPTODATE 404 #define IDC~SCIERR 405 #define IDC PIDSCIERR 406 #de:fine IDC~ACTION 407 #define IDC~CHECKSUM 408 #define IDC LOADFORM 409 #define IDC LASTEVENTTIME 410 #define IDC~EVENTID 411 #define IDC STARTTIME 412 #de~fine IDC LAUNCH 413 #define IDC~CLOSE 414 *~l~**************************************************************************
mace ~sp.rc produced by Borland Resource Workshop *****************************************************************************~
#de~fine IDC PASSWORD 155 #defineFWNA 161 IDC

#define_ 160 IDC MAXFWNA

#defineIDC MINFWNA 159 #defineIDC LOCKWINDOW 156 #defineIDC DATETIME 157 #defineMACC4SSP ICON 2 #include<windows.h>

#defineMACC4SSP DIALOG1 #defineGROUPBOX1 101 IDC

#define_ #defineIDC BDMIN 131 #def II7C BDMAX 13 2 ine #defineM 130 IDC~

#define_ 127 IDC BDTEMP

#de:fineDPHSETPOINT 126 IDC

#define_ 125 PHSETPOINT
IDC

#define_ #defineIDC PH 123 #defineIDC MAXP04 120 #defineIDC MINP04 119 #defineIDC~P04SETPOINT118 #defineIDC DPO4 117 #defineIDC P04 116 #defineIDC NH3TEXT 163 #defineIDC NH3 164 #defineIDC SPGA 146 #defineIDC SPGB I14 #defineIDC P04B 112 #defineIDC FBDEF 111 #defineIDC FBMAX 110 #define~IDC FBMIN 109 #defineIDC RATIOB 107 #defineIDC RATIOA 106 #defineIDC P04A 105 #defineIDC FADEF 104 #defineIDC STATUSFLAGS151 #defineIDC POINTNAMES 154 #defineIDC NH3UNDERLINE

#defineIDC START 121 #defineIDC SIMULATESSP147 #defineIDC AFEED 200 #defineIDC BFEED 201 #defineIDC FAMAX 103 #defineIDC FAMIN 102 #defineIDC GROUPBOX5 129 #defineIDC MESSAGELINE145 #defineIDC'LOGTOFILE 113 #defineIDC NH3ENABLED 162 #defineIDC FWFLOW 128 #defineIDC~GROUPBOX6 148 #defineIDC ENGINES 144 #defineIDC ENGINES 143 #defineIDC ENGINE? 142 #defineIDCJENGINE6 141 #define IDC ENGINE5 140 #define IDC ENGINE4 139 #defe IDC ENGINE3 138 #def ' IDC ENGINE2 137 #def~..e IDC ENGINE1 136 #define IDC ENGINEO 135 #define IDC_STOP 122 #define IDC UNDO 204 #define IDC REINIT 203 #define IDC_UPDATE 202 #define IDC LOCALINPUT 158 #define IDC FORECASTTIME 149 #define IDC FORECAST 150 #define IDC GROUPBOX4 -1 #define IDC GROUPBOX3 115 #define IDC GROUPBOX2 108 MACC4SSP_DIALOG DIALOG 1, l, 352, 248 STYLE WS OVERLAPPED ~ WS_CAPTION ~ WS~SYSMENU ~ WS MINIMIZEBOX
CLASS "MACC4SSP" -CAPTION "MACC4SSP Boiler0 (ERROR NONE)"
FONT 8, "MS Sans Serif"
LTEXT "Password:", -1, 4, 4, :34, 8 EDITTEXT IDC PASSWORD, 41, 2, 43, 12, :~S UPPERCASE ~ ES_PASSWORD ( WS BORDER
PUSHBUTTON "Lock", IDC LOCKWINDOW, 87, 2, 20, 12 CO:~TTROL "Last update:", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ( WS
VISIBLE
CO:~ITROL "24-Dec-1994 09:24:29", IDC DATETIME, "STATIC", SS LEFTNOWORDWRAP ~
WS
GROUPBOX "Pump/Tank A", IDC GROUPBOX1, 4, 14, 109, 91, BS GROUPBOX
LT:EXT "Min Feed (gpd) :", -1, 12, 30, 52, 12 EDITTEXT IDC FAMIN, 70, 28, 4(), 12 C01VTROL "Max Feed (gpd):", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS,CHILD ~ WS
VISI
EDITTEXT IDC FAMAX, 70, 40, 4(), 12 CO1VTROL "Init. Feed (gpd):", -1, "STATIC", SS_LEFTNOWORDWRAP ~ WS CHILD ~ WS
VI
EDITTEXT IDC FADEF, 70, 52, 40, 12 - '-LT;~XT "P04 (% by wt.) :", -1, 12, 66, 53, 12 EDITTEXT IDC P04A, 70, 64, 40, 12 LTEXT "Na/P04 ratio:", -1, i2, 78, 52, 12 EDITTEXT IDC RATIOA, 70, 76, 40, 12 LT:EXT "Specific Gravity:", -1, 12, 90, 55, 12 EDITTEXT IDC SPCA, 70, 88, 40,, 12 GROUPBOX "Pump/Tank B", IDC GROUPBOX2, 118, 15, 109, 90, BS GROUPBOX
C01VTROL "Min Feed (gpd):", -1, "STATIC", SS,LEFTNOWORDWRAP T WS~CHILD ~
WS_VISI
EDITTEXT IDC FBMIN, 183, 28, 40, 12 CO1VTROL "Max Feed (gpd):", -1,, "STATIC", SS~LEFTNOWORDWRAP ~ WS__CHILD ~ WS
VISI
EDITTEXT IDC FBMAX, 183, 40, 40, 12 CO1VTROL "Init. Feed (gpd):", -1, "STATIC", SS'LEFTNOWORDWRAP ~ WS CHILD ~
WS_VI
EDITTEXT IDC FBDEF, 183, 52, 40, 12 LT:EXT "P04 (% by wt.):", -1, 127, 64, 52, 12 EDITTEXT IDC P04B, 183, 64, 4c), 12 LT:EXT "Na/P04 ratio:", -1, 12'7, 76, 49, 12 EDITTEXT IDC RATIOB, 183, 76, 40, 12 LTEXT "Specific Gravity:", -1, 127, 88, 56, 12 EDITTEXT IDC SPGB, 183, 88, 40, i2 GROUPBOX "Boiler Phosphate", IDC GROUPBOX3, 4, 109, 109, 91, BS GROUPBOX
C01VTROL "Phosphate (ppm):", -1, "STATIC", SS_LEFTNOWORDWRAP ~ WS CHILD ~ WS
VI:
EDITTEXT IDC P04, 70, 122, 40, 12 LTEXT "Variability (ppm):", -1, 11, 136, 55, 10 EDITTEXT IDC DP04, 70, 134, 40, 1.2 LTEXT "Set Point (ppm):", -1., 10, 147, 55, 9 EDITTEXT IDC P04SETPOINT, 7C!, 146, 40, 12 LTEXT "Min (ppm):", -1, 10, 1.59, 54, 10 EDITTEXT IDC MINP04, 70, 158, 40, 12 LTEXT "Max (ppm):", -1, 10, 171, 52, 10 EDITTEXT IDC MAXP04, 70, 170, 40, 12 CONTROL "pH:", -1, "STATIC", SS LEFTNOWORDWRAP ~ WSVCHILD ~ WS VISIBLE ~ WS
GRc EDITTEXT IDC PH, 184, 123, 9:0, 12 LTEXT "Variability:", -1, 128, 137, 38, 12 EI>.ITTEXT IDC DPH, 184, 13S, 40, 12 L:.'EXT "Setpoint:", -1, 128, 150, 30, 10 EI)T~~'EXT IDC PHSETPOINT, 15"7, 149, 31, 13 LTl "+/_~~~-_1, 188. 151, 11, 9 EDITTEXT IDC DPHSETPOINT, 199, 149, 25, 13 EDITTEXT IDC_-BDTEMP, 184, 1E~4, 40, 12 CHECKBOX " ", IDC NH3ENABLED, 119, 176, 9, 11, BS AUTOCHECKBOX ~ WS TABSTOP
CONTROL "NH3 (ppm)*:", IDC NH3TEXT, "STATIC", SS LEFTNOWORDWRAP ~ WS-CHILD ~
WS
EDITTEXT IDC NH3, 184, 176, 40, 12 CONTROL "", IDC NH3UNDERLINF, "static", SS BLACKFRAME ~ WS CHILD ~ WS VISIBLE
LTEXT "* of boiler water pH sample", -1, 1$0, 190, 106, 9 GROUPBOX "Other Parameters" IDC GROUPBOX5, 231, 109, 117, 91, BS GROUPBOX
CONTROL "BW Mass (lbs):", =, "STATIC", SS LEFTNOWORDWRAP ~ WS-CHILD ~ WS VISIB
EDITTEXT IDC M, 297, 122, 4~), 12 CONTROL "FW Flow (lbs/hr):" -1, "STATIC", SS LEFTNOWORDWRAP ~ WS-CHILD ~ WS VI
EDITTEXT IDC FWFLOW, 29?, 134, 49, 12 CONTROL "Min:", -1, "STATIC", SS_LEFTNOWORDWRAP WS_CHILD WS VISIBLE WS_GR
CONTROL "Max:", -1, "STATIC", SS LEFTNOWORDWRAP WS_CHILD WS VISIBLE WS_GR
CONTROL "Est.", -1, "STATIC", SS-LEFTNOWORDWR.AP WS CHILD WS-VISIBLE WS GR
CONTROL " BD (lb/hr)", -1, "STATIC", SS LEFTNOWORDWRAP ( WS CHILD ' WS VISIBLE
EDITTEXT IDC BDMIN, 248, 159, 49, 12 EDITTEXT IDC HDMAX, 248, 171., 49, 12 EDITTEXT IDC BD, 248, 183, ~9, 12 LTEXT "FW Na (ppb)", -1, 29~'', 150, 49, 10, WS BORDER ~ WS GROUP
EDITTEXT IDC MINFWNA, 297, i.59, 49, 12 - -EDITTEXT IDC MAXFWNA, 297, 1.71. 49, 12 EDITTEXT IDC FWNA, 297, 183, 49, 12 CONTROL "Model Adaptive Congruent Controller 4 Smartscan+ 1Ø Copyright (c) Be PUSHBUTTON "Boiler0", IDC ENGINEO, 1, 217, 35, 14, WS GROUP ~ WS TABSTOP
PUSHBUTTON "Boiled", IDC ENGINEl, 36, 217, 35, 14 PUSHBUTTON "Boiler2", IDC ENG:LNE2, 71, 217, 35, 14 PUSHBUTTON "Boiler:3", IDC ENG:LNE3, 106, 217, 35, 14 PUSHBUTTON "Boiler4", IDC' ENGINE4, 141, 217, 35, 14 PUSHBUTTON "Boilers", IDC' ENGINES, 176, 217, 35, 14 PUSHBUTTON "Boiler6", IDC'~ENGINE6, 211, 21?, 35, 14 PUSHBUTTON "Boiler?", IDC ENGTNE7, 246, 217, 35, 14 PUSHBUTTON "Boiler8", IDC-ENGTNEB, 281, 217, 35, 14 PUSHBUTTON "Boiler9", IDC EN'GINE9, 316, 217, 35, 14 COLVTROL "Point names:
m0a~eed,mObfeed,mOpH,mOP04,mObdtemp,m0update,m0undo,mOrei PUSHBUTTON "Stop", IDC STOP, 231, 4, 58, 14 PUSHBUTTON "Start", IDC START, 289, 4, 59, 14 CH'ECKBOX "Simulate SSP", IDC_SIMULATESSP, 231, 20, 54, 14, BS AUTOCHECKBOX ~
WS
CHECKBOX "Local Inputs", IDC LOCALINPUT, 295, 21, 53, 12, BS AUTOCHECKBOX ~
WS_ PUSHBUTTON "Update", IDC UPDATE, 231, 35, 39, 12 PUSHBUTTON "Undo", IDC UFTDO, 270, 35, 39, 12 PUSHBUTTON "Reinit", IBC REINIT, 309, 35, 39, 12 GROUPBOX "Outputs to SmartScan+", IDC GROUPBOX6, 231, 48, 117, 44, BS GROUPBOX
LTEXT "Feed Rate A (gpd):", -l, 235, 59, 64, 12 LTEXT "", IDC AFEED, 298, 59, 45, 10, WS BORDER ~ WS-GROUP
LTEXT "Feed Rate B (gpd):", -1, 235, 68,-64, 12 LTEXT "", IDC BFEED, 298, 68, 45, 10, WS BORDER ~ WS GROUP
LTEXT "Status-Flags:", -1, 235, 79, 43, LTEXT "", IDC STATUSFLAGS, 292, 78, 51, 10, WS BORDER ( WS GROUF
EDLTTEXT IDC FORECASTTIME, 231, 94, 17, 1.2 PUSHBUTTON "F~r P04,ipH forecast", IDC FORECAST, 248, 94, 66, 12 CHECKBOX "Logfile" IDC LOGTOFILE, 316, 95, 32, 12, BS AUTOCHECKBOX ~ WS TABSTC
CO1VTROL "", -1, "static", SS BLACKFRAME WS CHILD WS VISIBLE, 128, 133, 11, C01VTROL "", -1, "static", SS-i3LACKFR.AME WS CHILD WS'VISIBLE, 233, 192, 12, CONTROL "", -1, "static", SS'-BLACKFRAME WS-CHILD WS VISIBLE ~ WS_BORDER, 11 CONTROL "", -1, "static", SS~BLACKFRAME WS CHILD WS VISIBLE WS BORDER, 12 CO1VTROL "Temperature (F)*~", -1, "STATIC'°, S~ LEFTNOWOR~WRAP ~ WS
CHILD ~ WS VI
GROUPBOX -"Boiler pH", IDC-GROUPBOX4, i18, 109, 109, 91, BS GROUFB~X

/* macc4ssp.c: Model Adaptive Congruent Controller Engine for SmartScan Plus*/
/* vgrsion 1Ø Copyright (c) Betz Labs, 1994. All rights reserved. */
/* w~,.tten by John Gunther, R&D Computer Applications Groug, July, 1994 */
/* (extension 2760, Trevose office) */
#include <windows.h>
#in.clude <string.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <dos.h>
#include "scintdll.h" /* SmartCard Interface Library */
#include "macc4.h" /* Model Adaptive Congruent Controller, version 4*/
#include "macc4ssp.h" /* Identifiers for fiel3s, other parameters */
#define TESTING 0 /* determines the test pattern used in operator sim.*/
#define WORKAROUND 0 /* used to workaround memory leak in SOIL version lAl*/
#if TESTING
#define SCILERRORFRACTION 0.05 /* used during dey. to produce SCIL errors */
#else #define SCILERRORFRACTION 0.0 /* no SCIL errors in production simulations*/
#endif #define ROOMTEMPERATUREDEGREESF 77.0 #define UNDOINTERVAL 1.0 /* only give them 1 hour to undo */
#define M4SMODULENAME "MACC4SSP"
static HWND hmainwin = NULL; /* handle to main (dialog) window */
#define DOUBLEPRECISIONFORMAT "%.lOg" /* full precision doubles */
#define FLOATPRECISIONFORMAT "%.6g" /~* full precision floats */
#define SHORTFLOATPRECISIONFORMAT "%.5g" /* helps squeeze stuff in sometimes */
#define MAXSTRING 100 #define IGNORECHECKSUM -1 /* if checksum is set to this, it is ignored*/
#define SIMHOURSPERREALSECOND 1.0 #define SIMHOURSPEREVENT 24.0 #define SECONDSPERHOUR 3600 #define M4STIMERID 1 #define DEFTIMERINTERVAL 3000 /* in milliseconds */
#define NANINT -32767 /* undefined integer */
#define NANFLT E NAN /'* undefined floating point value (from macc4.h)*/
#define NANSTRIN~ "" /* string corresponding to undefined value */
#define NANBOOL -1 /* used with BOOLPLUS type to allow "undefined"*/
typedef enum boolplus { /* extends BOOL to include NANBOOL ("not a number") */
HP TRUE=TRUE. BP_FALSE=FALSE, BP NANBOOL=NANBOOL, BOOLPLUS;
typedef struct simsspevent { BOOLPLUS update, undo, reinit; ~ SIMSSPEVENT;
SIMSSPEVENT simsspinputs(1 -{
#if TESTING
TRUE, FALSE, FALSE}, FALSE, TRUE, FALSE}, FALSE, FALSE, TRUE}, TRUE, TRUE, TRUE}
FALSE, TRUE, TRUE , TRUE, FALSE, TRUE , TRUE, TRUE, FALSE , #endif {TRUE, FALSE, FALSE}, };
stav~4~ SIMSSPEVENT nullevent - {FALSE, FALSE, FALSE};
#define MINENGINEID 0 #define DEFENGINEID 0 #define MAXENGINEID 9 typ~~def int M4SENGINEID;/* all reference to M4SENGINE objects is via this id */
typedef int M4SPARAMID; /* identifies a parameter used by the M4SENGINE */
/* (c.f. m4sparameters and/or macc4ssp.h for list of defined pids) */
typ~~def struct m4sengine { /*in memory representation of a MACC4SSP Engine */
BOOL running; /*is the engine running? */
SCIError scierr; /*Ist SmartCard Interface Library error code seen */
M4SPARAMID pidscierr; /*identifier of parameter SCIL error involves*/
BOOL actionsuptodate; /*SSP action codes MACC4SSP maintains up-to-date?*/
BOOL flagsuptodate; /*SSP flags MACC4SSP maintains up-to-date?*/
int checksum; /* used to validate engine state I/O */
BOOL badchecksum; /* signals a corrupted INI file (invalid state)*/
BOOL logtofile; /* .~.og actions to *.LOG file?*/
BOOL simulatessp; /* simulate SSF inputs? (for testing)*/
BOOL localinput; /* Gaccept inputs 1oeo1ly?*/
BOOL nh3enabled; /* use an ammonia correction for pH's?*/
double starttime; /* (actual, real world) time at which run began */
double lasteventtime; /* time of last simulated operator input*/
int eventid; /* cursor into table of simulated operator inputs*/
SIMSSPEVENT currentevent; !* simulated or locally generated events*/
double fwflow; /* feedwater flow--used to scale no leak */
E BOILER blr; /* MACC4 controller as defined in macc4.h */
}_ M4 SENG I NE ;
M4SENGINE m4e[MAXENGINEID+1];
static int currentengineid = DEFENGINEID;
#de:fine xquote(x) #x #de:fine quote(x) xquote(x) /* forces expansion of x before quoting it */
# define M SAY(s) MessageBox(hmainwin,s,"Informational message",MB OK) # d.=_fine M BUGALERT(s) MessageBox(hmainwin,s,\
"MACC4SSP Eug in file: " quote(~FILE_) " at line: " quote( LINE_),MB_OK) #de:fine M ASSERT(cond) \
if (!(con3)) MessageBox(hmainwin, "Failed assertion: " #cond,\
"As;sertion Failure in File: " quote( FILE ) " at Line: " quote( LINE ),\
MB ICONHAND~MB OK) #define M M4SINF0(s) clearmarque(),marque(s) #define M M4SERROR(s) MessageBeep(0),clearmarque(),marque(s) #define M M4SERROR2(s) MessageBeep(0),MessageBeep(0),clearmarque(),marque(s) #de f ine lei M4 S FATALERROR ( s ) \
Messa eBox(hmainwin, s, "MACC4SSP fatal error--cannot continue.",\
MB-~K~MB_ICONEXCLAMATION) /*
Initialization error message information. Index of array is integer code as defined in the initstatus enum in macc4.h.
*/
struct initerror {char *msg; /* message to display for initstatus error*/
M4SPARAMID field;} /*id of error related control to move to*/
initerr [] _ {
"E INITOK: Initialization OK", IDC STOP}, "E MISSING T: required field t (time) has not been specified."', IDC STOP}
"E MISSING FAMIN: required field famin has not been specified", IDC FAMIN , "E MISSING FAMAX: required field famax has not been specified", IDC FAMAX , "E MISSING FADEF: required field fadef has not been specified", IDC FADEF , "E MISSING P04A: required field po4a has not been specified", IDC_P04A}, "E_MISSING RATIOA: required field ratioa has not been specified", IDC RATIOA}, ~"E MISSING FBMIN: required field fbmin has not been specified", TDC FBMIN~, 'MISSING FBMAX: required field fbmax has not been specified", IDC FBMAX , _MISSING FBDEF: required field fbdef has not been specified", IDC FBDEF~, "E MISSING P04B: required field po4b has not been specified", IDC P04B}, "E MISSING RATIOB: required field ratiob has not been specified",-IDC RATIOB}, "E MISSING P04: required field po4 has not been specified", IDC P04}, "E MISSING PH: required field ph has not been specified", IDC_PH}, "E MISSING M: required field m (mass) has not been specified", IDC M), "E MISSING BDTEMP:. required field bdtemp has not been specified", -IDC BDTEMP}, {"E MISSING P04SETPOI:I~T: required field po4setpoint has not been specified", IDC_P04SETPOINT1, {"E MISSING MINP04: required field minpo4 has not been specified", IDC MINP04~, {"E MISSING MAXP04: required field maxpo4 has not been specified", IDC MAXP04~, {"E MISSING RATIOSETPOINT: neither phsetpoint nor napo4ratio were specified"
IDC PHSETPOINT}, {"E; BDMAX LT BDMIN: max blowdown rate, bdmax < min blowdown rate,, bdmin", IDC BDMIN}, {"E; NALEAKMAX LT NALEAKMIN: max Na leak, naleakmax < min Na leak, naleakmin", IDC MINFWNA}, {"E; FAMAX LE FAMIN: max pump a flow, famax <= min pump a flow, famin", IDC FAMIN}, {"E; FBMAX LE FBMIN: max pump b flow, fbmax <= min pump b flow, fbmin", IDC FBMII3} , {"E; MINP04 LT MINMINP04: lower boiler po4 control limit, minpo4 < 1 ppm", IDC MINP04}, {"E,_MAXP04 GT MAXMAXP04: upper boiler po4 control limit, maxpo4 :> 100 ppm", IDC MAXP04}, {"E. P04SETPOINT LT MINP04: setpoint boiler po4 < lower control limit", IDC P04SETP~IN~I}, {"E: P04SETPOINT GT MAXP04: setpoint boiler po4 > upper control limit", IDC P04SETPOINT~, {"E,_P04A LT ZERO: pump a phosphate concentration < 0", IDC P04A}, {"E; P04B LT ZERO: pump b phosphate concentration < 0", IDC P04B}, {"E; MINRATIO LT MINMINRATIO: lower Na/P04 ratio control limit < 2.2", IDC DPHSETPOINT}, {"E; MAXRATIO GT MAXMAXRATIO: upper Na/P04 ratio control limit > 2.8", IDC DPHSETPOINT~, {"E RATIO LT MINRATIO: setpoint Na/P04 ratio < lower control limit", IDC DPHS~TPOINT~, {"E; RATIO GT MAXRATIO: setpoin.t Na/P04 ratio > upper control limit", IDC DPHSETPOINT}, {"E; RATIOA EQ RATIOB: pump a Na/P04 ratio = pump b Na/P04 ratio", IDC RATIOA}, {"E; MISSING SPGA: required field spga !specific gravity) not specified", IDC SPGA}, {"E; MISSING SPGB: required field spgb ispecific gravity) not specified", IDC SPGB}
typedef enum contro:Lsecuritylevel NULL LEVEL=0, /* no security enforced on the control */
fIANAGERONLY LEVEL=1, /* access only for users who have entered password */
OPERATORONLY LEVEL=2,/* access only for users who have NOT entered password*/
C:ONTROLSECURITYLEVEL;
typedef enum controlaccess {
NULL ACCESS=0, /* unconditional access */
STATIC ACCESS=1, /* control. ONLY accessable when controller is stopped */

DYNAMIC ACCESS=2, /* control ONLY accessable when controller is running */
LOCAL ACCESS=4, /*if running, cntrl accessable only if local input checked - if stopped, cntrl accessable only if user is a manager */
NI, ACCESS=8 /* control ONLY accessible when nh3 is checked */
CONTROLACCESS;
typedef enum controltype {
NULL CONTROL,/*not a control, or a control not stored/restored to/from disk*/
STOREDEDIT CONTROL, /* edit control which is stored/restored to/from disk */
STOREDCHECKBOX_CONTROL /* stored/restored to/from disk checkbox control */
CONTROLTYPE;
typedef struct m4sparam {
int pid;
char *name;
/* remaining fields only apply to parameters that are associated with one of the on-screen controls */
double low; /* for range checking of edit controls*!
double high;
CONTROLSECURITYLEVEL Level; /* who can access control ? */
CON'TROLACCESS access; /* and under what conditions? */
CONTROLTYPE type; /* stored text (edit ctrl), stored mode (checkbox)*/
M4SPARAM;
#define SMALLNUMBER 1e-15 M4SPARAM m4sparameters[] _ /*misc.ids*/
{IDC CURRENTENGINEID,"current:engineid",NANFLT,NANFLT,NU'LL LEVEL, NULL ACCESS,NULL CONTROL}, {IDC RUNNING,"running",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, {IDC FLAGSUPTODATE,"flagsuptodate",NANFLT,NANFLT,NU'LL LEVEL,NULLVACCESS, NULL CONTROL}, {IDC ACTIONSUPTODATE,"actionsuptodate",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, IDC CHECKSUM,"checksum",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, ~IDC LASTEVEN'TTIME,"lasteventtime",NANFLT,N~NFLT,NULL~LEVEL,NULL_ACCESS, NULL CONTROL}, {IDC EVENTID,"eventid",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, /*
dialog box control ids (all parameters which are associated with a control must be placed between IDC~FIRSTCONTROL and IDC LASTCONTROL) #define IDC FIRSTCONTROL IDC FORECASTTIME
{IDC FORECASTTIME,"forecasttime",0,999, NULL LEVEL, DYNAMIC ACCESS, ST'OREDEDIT CONTROL}, /* limit to 999 so it~fits in narrow 3 character field*/
{IDC FORECAST "forecast",NANFLT,NANFLT,NULL LEVEL, DYNAMIC ACCESS, NULL CONTROL, {IDC PASSWORD "password",NANFLT,NANFLT,OPERATORONLY LEVEL,NULL'ACCESS, NULL CONTROL , {IDC -LOCKWINDOW,"lockwindow",NANFLT,NANFLT,MANAGERONLY LEVEL, NULL ACCESS, NLfLL CONTROL } , IDC STARTTIME,"starttime",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, ~IDC LOGTOFILE,"logtofile",NANFLT,NANFLT,MANA'GERONLY LEVEL, STATIC ACCESS, STOREDCHECKBOX CONTROL}, {IDC SIMU'LATESS~,"simulatessp",NANFLT,NANFLT,MANAGERONLY LEVEL,STATICrACCESS, STOREDCHECKBOX CONTROL}, {IDC LOCALINPUT,"localinput",NANFLT,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, S7.'OREDCHECKBOX CONTROL } , {IDC MINFWNA,"minfwna",NANFL"r,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL), {IL)C MAXFWNA,"maxfwna",NANFL'T,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STO~EDEDIT CONTROL}, {IDC FWFLOW,"fwflow",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STO~EDEDIT CONTROL), {Ii)C BDMIN,"bdmin",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL,STATIC_ACCESS, ST'~DEDIT CONTROL} , {ID~~ 3DMAX,"bdmax",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC M,"m",SMALLNUMBER,NANFL'T,MANAGERONLY~LEVEL,STATIC~ACCESS, STOREDEDIT CONTROL), {II7C DPHSETPOINT,"d~hsetpoint",SMALLNUMBER,1.O,MANAGERONLY LEVEL, STATIC
ACCESS, STOREDEDIT CONTROL , {IDC PHSETP~INT,"phsetpoint",2,13,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC DPH,"dph",SMALLNUMBER,1.O,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, (IDC MAXP04,"maxpo4",1.,1000.,MANAGERONLY~LEVEL,STATIC ACCESS, S'.COREDEDIT CONTROL} , (IDC MINP04,"minpo4",1.,1000.,MANAGERONLY~LEVEL,STATIC ACCESS, S".."OREDEDIT CONTROL} , {IDC P04SETF~OINT,"po4setpoint",1.,1000.,MANAGERONLY LEVEL, STATIC.~ACCESS, S7.'OREDEDIT CONTROL} , {IDC DP04,"3po4",SMALLNUMBER,lO.,MANAGERONLYlLEVEL,STATIC ACCESS, STOREDEDIT CONTROL}, {IDC SPGA,"spga",SMALLNUMBER,NANFLT,MANAGERONLY~LEVEL,STATIC ACCESS, STO~EDEDIT CONTROL}, {IDC SPGB,"spgb",SMALLNUMHER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STO~EDEDIT CONTROL , {II)C P04B,"po4b",O,:LOO,MANAGERONLY LEVEL, STATIC ACCESS, STO~EDEDIT CONTROL}, {II>C FBDEF,=fbdef",O,NANFLT,MANAGERONLYrLEVEL,STATIC ACCESS, STO~EDEDIT CONTROL), {IDC FBMAX,"fbmax",O,NANFLT,MANAGERONLYrLEVEL,STATIC ACCESS, S'1.'O~EDEDIT CONTROL ~ , {II>C FBMIN,"fbmin",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC RATIOB,"ratiob",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC RATIOA,"ratioa",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL.}, {IDC P04A,"po4a",O,100,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, (IDC FADEF,"fadef",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STO~tEDEDIT CONTROL), (IDC FAMAX,"famax",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL), {IDC FAMIN,"famin",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC MESSAGELINE,"messageline",NANFLT,NANFLT,NULL,LEVEL,NULL ACCESS, NULL CONTROL}, {IDC NH3ENABLED,"nh3enabled",NANFLT,NANFLT,MRNAGERONLY LEVEL,STATIC_ACCESS, STO1~EDCHECKBOX CONTROL}, {IDC NH3TEXT,"nfi3text",NANFL'T,NANFLT,NULL LEVEL,NH3 ACCESS,NULL CONTROL}, {IDC START,"START",NANFLT,NANFLT,MANAGERONLY LEVEL,STATIC_ACCES~S, NULL CONTROL } , (ID(: STOP,"STOP",NANFLT,NANFLT,MANAGERGNLY~LEVEL,DYNAMIC,ACCESS, NULL CONTROL}, /* SSP point names (some also correspond to controls) */
/* (we use uppercase for SmartScan+ point names cause' SmartScan+ */
/* converts point names to uppercase)*/
(IDC BDTEMP,"HDTEMP",32,212,NL1LL_LEVEL,LOCAL~ACCESS, STUR~DEDIT CONTROL , (IDC P04,"P04",SMALLNUMBER,1000.O,NULL_LEVEL,LOCAL ACCESS, STOR~DEDIT CONTROL}, (IDC NH3,"1~H3",0.0 1000.0,NULL,LEVEL,LOCAL ACCESS~NH3_ACCESS, STOR~DEDIT CONTROL , {IDC PH,"PH",1,14, NULL LEVEL, LOCAL ACCESS, STO~EDEDIT CONTROL}
{IDC ~WNA,"FWNA",NANFLT,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOR- :DIT CONTROL}, -{ID'S ~D,"BU",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC UPDATE,"UPDATE",NANFLT,NANFLT,NULL~LEVEL,LOCAL ACCESS~DYNAMIC ACCESS, NULL CONTROL}, {IDC ~tEINIT,"REINIT",NANFLT,NANFLT,NULL~LEVEL,LOCAL ACCESS~DYNAMIC ACCESS, NULL CONTROL}, {IDC UNDO,"UNDO",NANFLT,NANFLT, NULL LEVEL,LOCAL~ACCESS~DYNAMIC_ACCESS, NULL,CONTROL}, #define IDC LASTCONTROL IDC UNDO
IDC AFEED,"AFEED",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC BFEED,"BFEED",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDG RATIO,"RATIO",SMALLNUMBER,NANFLT, NULL LEVEL, NULL ACCESS, NULL CONTROL
IDC FLAG1,"FLAG1",NANFLT,NANFLT,NULL LEVE~,NULL ACCESS,NULL CONTROL , IDC FLAG2,"FLAG2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAGS,"FLAGS",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC: FLAG4,'°FLAG4",NANFLT,NANFLT,NULL LEVEL,NULL~ACCESS,NULL
CONTROL , IDC FLAGS,"FLAGS",NANFLT,NANFLT,NULL LEVEL,NULL_ACCESS,NULL CONTROL , ID(~ FLAG6,"FLAG6",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG?,"FLAG?",NANFLT,NANFLT,NULL LEVEL,NULLJACCESS,NULL CONTROL , IDC FLAG$,"FLAGB",NANFLT,NANFLT,NULL~LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG9,"FLAG9",NANFLT,NANFLT,NULL~LEVEL,NULL ACCESS,NULL CONTROL , IDC: FLAG10,"FLAG10",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC. FLAG11,"FLAG11'",NANFLT,NANFLT,NULL~LEVEL,NULL ACCESS,NULL CONTROh , IDC_FLAG12,"FLAG12",NANFLT,NANFLT,NULL_LEVEL,NULL ACCESS,NULL CONTROL , /* Must E BOILER fields */
IDC T,"t",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, ~IDC: NAP04RATI0,"napo4ratio",~ANFLT,NANF~T,NULL~LEVEL,NULL ACCESS, NULL CONTROL } , {IDC MINNAP04RATI0,"minnapo4ratio",NANFLT,NANFLT,NULLlLEVEL,NULL ACCESS, NULL CONTROL } , {IDC I~AXNAP04RATI0,"maxnapo4ratio",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL } , {IDC: MAX SAMPLE INTERVAL,"max sample interval",NANFLT,NANFLT, NULL LE~3EL,NUL~ ACCESS,NULL CONTROLS, {IDC: MAX OUTOFBOX ADJUSTMENT,"max outofbox,adjustment",NANFLT,NANFLT, NULL LEVEL,NULL ACCESS,NULL CONTROL}, {IDC MAX RELATIVE ERROR,"max~relative error",NANFLT,NANFLT, NULL LEVEL, NULL ACCESS,NULL CONTROL}, IDC BEPS,"beps",NANFLT,NANFL'r,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC' FDT,"fdt",NANFLT,NANFLT,NULL LEVEL,NULL A~CESS,NULL CONTROL}, IDC:~LASTT,"lastt",NANFLT,NANFLT,I~ULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC LASTBDTEMP,"lastbdtemp",NANFLT,N~NFLT,NULL_LEVEL,NULL_ACCESS, NULL CONTROL } , IDC LASTP04,"lastpo4",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC LASTNH3,"lastnh3",NANFLT,NANFLT,NULLILEVEL,NULL ACCESS,NULL CONTROL}, IDC: LASTPH,"lastph",NANFLT,NANFLT,NULL L~VEL,NULL A~CESS,NULL CONTROL}, IDC: B4LASTT,"b4lastt",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC: B4LASTBDTEMP,"b4lastbdtemp",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL } , IDC B4LASTP04,"b4lastpo4",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL
IDC:_B4LASTNH3,"b4lastnh3",NANFLT,NANFLT,NULL'LEVEL,NULL ACCESS, NULL CONTROL
IDC B4LASTPH,"b4lastph",NANFLT,NANFLT,NULL L~'VEL,NULL AUCESS,NULL CONTROL}, IDC LASTBD,"lastbd",NANFLT,NANFLT,NULL LEVEL,NULL ACC~SS,NULL CONTROL}, IDC:VLASTL,"lastl",NANFLT,NAN:FLT,NULL L'EVEL,NULL A(CESS,NULL CONTROL}, IDC: DT1,"dtl",NANFLT,NANFLT,NULL LEV~L,NULL ACC~SS,NULL CONTROL , IDC' DT2 , "dt2" , NANFLT, NANFLT, NULL~LEVEL, N'CTL,L ACCESS, NULL CONTROL , IDC: FAl,"fal",NANFLT,NANFLT,NULL LEVEL,NUI,L~ACCESS,NULL CONTROL , IDC FA2,"fat",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC:_FA3,"fa3",NANFLT,NANFLT,NULLrLEVEL,NULL~ACCESS,NULL-CONTROL , IDS'.: FB1,"fbl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL ~~ONTROL , ID~~ FB2, "fb2" , NANFLT, NANFLT, a~LTLL LEVEL, NULL ACCESS, NULL CONTROL , IDC.~83,"fb3",NANFLT,NANFLT,L~UI,L LEVEL,NULL ACCESS,NULL CONTROL , IDC ~STDT1,"lastdtl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTDT2,"lastdt2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULIJ CONTROL , IDC_LASTFA1,"lastfal",NANFLT,NANFLT,NULL~LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFA2,"lastfa2",NANFLT,NANFLT,NL1LL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFA3,"lastfa3",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDWLASTFB1,"lastfbl",NANFLT,NANFLT,NULL~_LEVEL,NULL ACCESS,NULIJ CONTROL , IDC LASTFB2,"lastfb2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFB3,"lastfb3",NANFLT,NANFLT,NULL_~LEVEL,NULL ACCESS,NULL CONTROL , IDWUPDATESTATUS,"updatestatus",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, {IDC LASTUPDATESTATUS,"lastupdatestatus",NANFLT,NANFLT,NULL_LEVEL, NULL ACCESS,NULL CONTROL}, {IDC INITSTATUS,"initstatus",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, IDC UNDOABLE,"undoable'°,NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL
CONTROL}, IDC LAUNCH,"LAUNCH",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC CLOSE,"CLOSE",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, NAIVINT,"NANINT",NANFLT,NANFLT,NULLILEVEL,NULL~ACCESS,NULL_CONTROL}, /* pid2param: returns the parameter struct given the parameter id */
M4S1?ARAM *pid2param(M4SPARAMID pid) {
:int i = 0 ;
.'or (i = o; i r sizeof(m4sparameters)/sizeof(m4sparameters[0]); i++) if (m4sparameters[i].pid =- pid) return(&m4sparameters[i]);
M BUGALERT("Bad parameter id.");
return(&m4sparameters [i-1] ) ;~
/* hid2name: returns name given the pid */' char *pid2name(M4SPARAMID pid) return(pid2param(pid)->name);
%* pid2low: return low field*/
double pid2low(M4SPARAMID pid) rE~turn (pid2param (pid) - >low) ;
%* pid2low: return high field*,~
double pid2high(M4SPARAMID pid) return (pid2param (pid) ->high) ;
/*~compressexponent: replaces first e0, a+0, or e-0 with e, a+, or e- */
/* !Vote: the extra 0 in numbers like 1e-9 introduced by sprintf's 1-09 encoding can be a problem (it doesn't fit into the field anymare) .3o we replace e0*, e-0*, and a+0* with a*, e-*, and a+* */
char *compressexponent(char *numstring) {
char *e = strpbrk(numstring, "eE");
if (NULL =- e) /* no a or E in the string--leave as is */
else if ('0' -- a[1] && a[~] !_ '\0') /* for example, e09 */
lstrcpy(e+1, a+2);
else if ( (' -' -- a [1] ~ ~ ' +~' -= a [1] ) &&
(' 0' -- a [2] && a [3] ! _ ' \0' ? ) /* for example, e-09*/
lstrcpy(e+2, a+3);
return (numstring);
/*encodenumber: convert a number to a character string */

/* (5/28/94) Phil Gables reported that he noted that Windows became anusable (VERY SLOW) when MACC4SSP was run on a system that had S~~ rtscan Plus but no SmartCard. He also reported that ~ JR SMARTCARD TIMEOUT was always showing. This routine is used to shortcircuit SCIL calls after the first ERROR SMARTCARD TIMEOUT; since each such timeout takes 0.5 seconds and the timer messages come in every 3 seconds, this gives Windows a "timeout breather" and is intended to correct the problem of multiple SOIL 0.5 sec timeouts adding up to VERY SLOW Windows response.
This routine relies on the fact that each time a WM TIMER message comes in the sciinit() call is made and, if succesful, clears any old timeout errors stored in the scierr field.
BOOL timedoutbefore(M4SENGINEID eid, SCIError *err) if * (m4e [eid] . scierr c~DRTQM~O~TCARD TIMEOUT) err - ERROR SMART , return(TRUE);
else return(FALSE);
/* scigetdouble: retrieves value associated with an engine,parameter from SSP*/

double scigetdouble(M4SENGINEID eid, M4SPARAMID pid) {

float f;

SCIError err = ERROR NONE;

if (m4e [eid] .local input) f = ini2dbl(eid, pid);

else if (!timedoutbefore(eid, &err)) {

if (m4e [eid] . simulatessp) simSCReadAnalogPvint(m4spointname(eid,pid), &f, &err);

else SCReadAnalogPoint(m4spointname(eid,pid), &f, &err);

}aybestoreerror(eid, pid, err);

i:E (err == ERROR NONE) return(decodedouble(encodefloat(f)));

/* the curious encode/decode above is a hack to simplify numericformatting*/

else return (NANFLT);

/*scisetdouble: sends value to the SSP point associated with engine,parameter*/

SCIError scisetdouble(M4SENGINEID eid, M4SPARAMID pid, double{
x) float f = x;

SCIError err = ERROR NONE;

if (!timedoutbefore(eid, &err)) {

if (m4e[eid].simulatessp) simSCWriteAnalogPoint(m4spointname(eid,pid), (const float) f, &err);

else SCWriteAnalogPoint(m4spointname(eid,pid), (const float) f, err);

}aybestoreerror(eid, pid, err);

return(err);

/* ;scigetbit: retrieves flag associated with an engine,parameterfrom SSP */

BOOLPLUS scigetbit(M4SENGINEID eid, M4SPARAMID pid) {

BOOL bit;

SCIError err = ERROR NONE;

if (m4e [eid] .local input) 'bit = ini2int(eid, pid);
e1 " if ( ! timedoutbefore (ei.d, &err) ) (m4e [eid] . simulatessp) simSCReadDigitalPoint(m4spointname(eid,pid), &bit, &err);
else SCReadDigitalPoint(m4spointname(eid,pid), &bit, &err);
}aybestoreerror(eid, pid, err);
return ( (err =- ERROR NONE) ? bit . NANBOOL );
/* scisetbit: sends flag to the SSP point associated with an engine,parameter*/
SCIError scisetbit(M4SENGINEID eid, M4SPARAMID pid, BOOL bit) {
SCIError err=ERROR NONE;
if (!timedoutbefore(eid, &err)) {
if (m4e [eid] . simulatessp) simSCwriteDigitalPoint(m4spointname(eid,pid), bit, &err);
else SCWriteDigitalPoint(m4spointname(eid,pid), bit, &err);
}aybestoreerror(eid, pid, err);
return(err); ' /*
scierrstr: returns the engine's error string; basically just the SCIL error keywords except, sometimes, point specific info is added.
*/
char *scierrstr(M4SENGINEID eid) {
static char scierr[MAXSTRING+1];
switch (m4e [eid] . scierr) {
case ERROR NONE:
return("ERROR NONE");
case ERROR INVALID PROJECT NAME:
return("ERROR INVALID~PROJECT_NAME");
case ERROR PROJECT NOT FOU13D:
return("ERROR PROJECT NOT FOUND");
case ERROR SMARTSC~N PLUS NOT RUNNING:
return("ERROR SMARTS~'AN NOT_RUNNING");
case ERROR SMARTCA~D TIMEOUT:
return("ERROR SMARTC:ARD TIMEOUT"'.;
case ERROR POINT NAME NOT FOUND: /* SCIL errors that involve points*/
sprintf(scierr, "ERROR %s NOTFOUND", m4spaintname(eid,m4e[eid].pidscierr));
return(scierr);
case ERROR DATA TYPE MISMATCH:
sprintf(scierr,~"ERROR %s's DATA TYPE", m4spointname(eid,m4e[eidT.pidscierr));
return(scierr);
/* these cases should never t:appen (or else it's a bug): */
case ERROR LINK ALREADY ESTABLISHED:
return("BUG ALREADY LINKED");
case ERROR LINK NOT CURRENTLY ESTABLISHED:
return("~UG_N~T_L~NKED");
default return("BUG UNRECOGNIZED ERROR");

/* ci"tle: returns the title bar. for the dialog box (acts as status line)*/
char ~itle(M4SENGINEID eid) {
static char titlebuf[MAXSTRING+1];
svprintf(titlebuf, "MACC4SSP Boiler%i (%s)", eid, ( ! m4e [eid] . running) ? "STOPPED"
m4e[eid].badchecksum ? "BADCHECKSUM!!!" .
scierrstr(eid) );
return(titlebuf);
/* getssprequest: loads data fields and returns requested action shown below:
*/
typedef enum ssprequest {
NOOPSSPREQUEST, UPDATESSPREQUEST, REINITSSPREQUEST, UNDOSSPREQUEST, OUTOFRANGESSPREQUEST, NODATASSPREQUEST, AMBIGUOUSSSPREQUEST
SSPREQUEST;
SSPREQUEST getssprequest(M4SENGINEID eid) {
BOOLPLUS update, reinit, undo;
double ph, po4, nh3, bdtemp;
if (!m4e[eid].actionsuptodate)/* avoids reading the same action twice */
return(NOOPSSPREQUEST); /* if last action was not cleared*/
else if (NANBOOL =- (update = scigetbit(eid, IDC UPDATE)) NANBOOL =- (reinit = scigetbit(eid, IDC REINIT)) NANBOOL =_ (undo = scigetbit(eid, IDC UNDO))) return(NOOPSSPREQUEST);i'*can't read all action flags;wait til next timer*/
else if (update + reinit ~~ undo =- a) /* either no, ...*/
return(NOOPSSPREQUEST);
else if (update + reinit ~- undo > 1) { /* or several, actions requested */
M M4SERROR("Ambiguous request was ignored.");
return(AMBIGUOUSSSPREQUEST);
}lse'if (undo) /* undo has no arguments (no need to read pH, P04, etc.)*/
return(UNDOSSPREQUEST);
else if ( NANFLT =- (ph - scigetdouble(eid, IDC PH)) NANFLT =- (po4 - sci etdouble(eid, IDC P04)) (m4e [eid] . nh3enabled &~
NANFLT =_ (nh3 = scigetdouble(eid, IDC NH3))) NANFLT =_ (bdtemp = scigetdouble(eid, IDC BDTEMP)) ) return(NODATASSPREQUEST); /* can't read required info; wait til next timer*
else M ASSERT(update+reinit==1);
/* check/display range errors on each numerical input */
if (!inrange(ph, pid2low(IDC PH), pid2high(IDC~PH))) {
M M4SERROR2(rangeerror(IDC PH));
return(OUTOFRANGESSPR.EQUEST);
else if (!inrange(po4, pid2low(IDC P04), pid2high(IDC_P04))) {
M M4SERROR2(rangeerror(IDC P04)) return(OUTOFRANGESSPREQUEST);
else if (!inrange(bdtemp, pid2low(IDC BDTEMP), pid2high(IDC BDTEMP))) {
M M4SERROR2(rangeerror(IDC BDTEMP));
return(OUTOFRANGESSPREQUESfi);

else if (m4e [eid] .
nh3enabled &&

!inrangelnh3, pidclow(IDC
NH3),pid2high(IDC
NH3))) _ M M4SERROR2(rangeerror(IDC
NH3));

. return(OUTOFRANGESSPREQUES~');

else { /* so... */
all inputs in allowed range m4e[eid].blr.ph request - ph; /* */
store data needed to service m4e [eid]
. blr. po4 - po4 ;

if (m4e [eid]
.nh3enabled) m4e [eid]
.blr.nh3 - nh3;

m4e[eid].blr.bdtemp = bdtemp;

return(update ? UPDATESSPREQUEST
. REINITSSPREQUEST);

/*encodeflags:
encodes status code flags as a string */

char *encodeflags(M4SENGINEID
eid) static char flagtext[MAXSTR.ING+1];

flagtext - (m4e [eid] .blr.updatestatus&E INDETERMINATE) :' ' ;
(O] ? ' 1.' flagtext[1] - (m4e [eid].blr.updatestatus&E BOX UNREACHABLE) 2'~' ';
? ' f lagtext ' 3' : ' [2 ] - (m4e ' ;
[eid] .
blr . updatestatus&E
OUTOFBOX
TOOLONG) ?

flagtext _ (3] - (m4e ' 4' :' [eid] .blr.updatesta-tus&E ' ;
POINT UNREACHABLE) ?

flagtext[4]= 'S':' ';
(m4e(eid].blr.updatestatus&E
BOX UNMAINTAINABLE) ?

flagtext[5]= ?'6':'_';
(m4e[eidl.blr.updatestatus&E-POI~3T

UNMAINTAINABLE) flagtext (6] _ (m4e [eid]
.blr.t-m4e [eid] .blr.lastt~m4e [eid] .blr.max_sample-interval) ., , .~ .
, , flagtext[7] ' ';
- (m4e[eid].blr.updatestatus & E NOTUPDATED) ? '8':
~

NCONSISTENT ' 9' : ' P04 ) :' ' ;
f lagtext (8 ] _ (m4e (eid] .
blr . updatestatus & E 3 PH) ? 'A':',';
flagtext[9]
- (m4e[eid].blr.updatestatus & E INCONSISTEN~

_ ';
flagtext(10]
- (m4e(eid].blr.initstatus!=E
I~ITOK) ? 'B' ' _ ' ;
f:lagtext (11] - m4e (eid] .bad.checksum ? 'C'-' ,-flagtext [12] _' \0' ;

return ( f lagtext ) ;

/* :logfile: returns the name of the MACC4SSP log file for a given engine */
chair *logfile(M4SENGINEID eid) {
r~aturn(lstrcat(lstrcat(m4spath(),nameprefix(eid)),".LOG"));
/* fileexists: checks if a file exists. */
BOO:L fileexists (char *fname) {
unsigned attr=0;
return(( doe_getfileattr(fname,&attr) -- 0)?TRUE: FALSE);
/* logaction: used to log updates, reinits, undos, STARTS, and STOPS */
void logaction(M4SENGINEID eid, M4SPARAMID pid) /* indicates action: update, reinit, etc.*/
int hFile = -1;
long lastbyte = -1;
if (!m4e[eid].logtofile) /* skip if logging is disabled */
else if (fileexists(logfile(eid) ) &&
-1 =- (hFile = lopen(logfile(eid), OF READWRITE))) M M4SERROR("Cannot open log file");

else if (!fileexists(logfile(eid)) &&
-1==(hFile = _lcreat(logfile(eid), 0))) M M4SERROR("Cannot create log file");
e1 ~~ if (-1 =_ (lastbyte = _llseek(hFile, 0, 2))) _-i M4SERROR("Cannot move to end of log file");
else ~ /* print out header (only if file empty) and next log file line */
static char tobeprinted[5*MAXSTRING+1];
if (lastbyte a 1) { /* file not empty, assume it already has header*/
lstrcpy(tobeprinted, °");
else { /* empty file, add header to it */
sprintf(tobeprinted, "°s-4s,%-6s,%-20s,%-14s,%-14s,"
"%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s,%-9s", "Sim?","Action","DateTime","HoursSince1970","flags", "pH", "P04", "bdtemp","NH3", "fal", "fat", "fa3", "fbl", "fb2", "fb3", "dtl", "dt2");
/* append any to be printed info to the (optional) header created above */
sprintf(tobeprinted+lstrlen(tobeprinted), "\n%-4s,%-6s,%-20s,%-14f,%-14s,", m4e[eid].simulatessp ? "Yes" . "No", pid2name(pid), m4sdatetime(timeinhours()), m4e[eid].blr.t, encodeflags(eid));
sprintf(tobeprinted+lstrlen(tobeprinted), "%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f,%-9f", m4e [eid] .blr.ph., m4e [eid] .blr.po4, m4e [eid] .blr.bdtemp, fixnandbl (m4e [eid] .blr.nh3, 0.0) , m4e [eid] .blr. fa [0] , m4e [eid] .blr. fa [1] , m4e [eid] .blr. fa [2] , m4e [eid] . blr. fb [O] , m4e [eid] . blr. fb [1] , m4e [eid] .blr. fb [2] , m4e [eid] .blr.dt [0] , m4e [eid] .bl.r.dt [1] ) ;
if ( lwrite(hFile, tobeprinted, lstrlen(tobeprinted)) <= 0) M_I~4SERROR("'Cannot write to log file");
~f (hFile !_ -1 &~ -1 =- lclose(hFile))~
M M4SERROR("'Cannot close log file");
/*maybeclearactions: clear action flags if not already cleared */
BOO:L maybeclearactions(M4SENGINEID eid) if (!m4e[eid].actionsuptodate) {
if (m4e [eid] .local input) {
int2ini(eid, IDC UPDATE, FALSE);
int2ini(eid, IDC UNDO, FALSE);
int2ini(eid, IDC~REINIT, FALSE);
m4e[eid].actionsuptodate = TRUE;
}lse {
m4e[eid].actionsuptodate =(scisetbit(eid,IDC UPDATE,FALSE)==ERROR NONE &&
scisetbit(eid,IDC UNDO, FALSE)==ERROR NONE &&
scisetbit(eid,IDC REINIT,FALSE)==ERROR NONE);
return(m4e[eid].actionsuptodate);
/* maybesetflags: sets flags in SSP that correspond to the updatestatus */
/* note: order in which flags are set matters due to a kludge used in simSCWriteDigitalPoint() */
BOOL maybesetflags(M4SENGINEID eid) {
BOOL fl,f2,f3,f4,f5,f6,f7,f8,f9,f10,fl1,f12;
#de:fine M BIT(eid,flag) ((m4e[eid].blr.updatestatus & (flag))!=0) iii (!m4e[eid].flagsuptodate) { /* update SSP flags if required */
if (m4e [eid] .local input) {
f7=(ERROR NONE==scisetbit.(eid,iDC FLAG7, m4e[eidT.blr.t-m4e[eid].blr.lastt>m4e[eid].blr.max sample interval));
.12=(scisetbit(eid,IDC_FLAG12,m4e[eid].badchecksum)==ERROR NONE);
m4e[eid].flagsuptodate = (f7 &~ f12);
else fl=(scisetbit(eid,IDC FLAG1, M BIT(eid,E INDETERMINATE))==ERROR NONE);
f2=(scisetbit(eid,IDC FLAG2, M BTT(eid,E~BOX UNREACHABLE))==ERROR NONE);
f3=(scisetbit(eid,IDC FLAG3, MuBIT(eid,E OUT~FBOX TOOLONG))==ERROR NONE);
f4=(scisetbit(eid,IDC_FLAG4, M BIT(eid,E~POINT UNREACHABLE)) -=ERROR NONE);
f5=(scisetbit(eid,IDC FLAGS, M_BIT(eid,E_BOX_UNMAINTAINABLE)) -=ERROR NONE);
f6=(scisetbit(eid,IDC-PLAG6, M BTT(eid,E~POINT-CTNMAINTAINAHLE)) -=ERROR NONE);
f7=(ERROR NONE==scisetbit:(eid,IDC FLAG7, m4e[eidT.blr.t-m4e[eid].blr.lastt>m4e[eid].blr.max sample interval));
f8=(scisetbit(eid,IDC FLAGB, M BIT(eid,E NOTUPDATED)T==ERROR NONE);
f9=(scisetbit(eid,IDC FLAG9, M BIT(eid,E~INCONSISTENT P04))==ERROR NONE);
f10=(scisetbit(eid,IDC FLP.G10,M BIT(eid,>~ INCONSISTENT PH))==ERROR_NONE);
fll=(scisetbit(eid,IDC_FLAGll,m4'eCeid].blr.initstatus!=E_INITOK) -=ERROR NONE);
f 12= ( scisetbit (eid, IDC FL,AG12 , m4e Ceid] . badchecksum) ==ERROR NONE) ;
m4e [eid] . f lagsuptodate~= ( f l && f2 ~s~ f 3 && f4 ~& f 5 && f 6!&& f 7 &&
f 8 &&
f9 && f10 && fll && f12);
rE~turn (m4e (eid] . flagsuptodate) ;
%*setbuttontext: sets the appropriate text string for an engine's button */
/* (assumes that IDC ENGINEO, IDC ENGINE1, ... ids are sequential) */
void setbuttontext(HWND hDlg, M4SENGINEID eid) {
static char buttontext [MAXSTRING+1] ;
sprintf(buttontext, "%sBoiler%i%s", m4e[eid].running ? "*" . "", eid, m4e[eid].running ? "*" . "");
s:2ctrl(GetDlgItem(hDlg, IDC ENGINEO+eid), buttontext);
/*updateform: updates those fields which change while engine is running */
void updateform(HWND hDlg, M4SENGINEID eid) {
if ( !m4e [eid] . running) s2ctrl(GetDlgItem(hDlg,IDC DATETIME), "");
s2ctrl(GetDlgItem(hDlg,IDC~_AFEED), "");
s2ctrl(GetDlgItem(hDlg,IDC BFEED), "");
s2ctrl(GetDlgItem(hDlg,IDC STATUSFLAGS), "");
else { /* update the form from the current in-memory info */
if ( !m4e [eid] .local input) {
s2ctrl(GetDlgItem(hDlg, IDC_PH), encodeshortfloat(m4e[eid].blr.ph));
s2ctrl(GetDlgItem(hDlg, IDC. P04), encodeshortfloat(m4e[eid].blr.po4));
if (m4e [eid] . nh3enabled) s2ctrl(GetDlgItem(hDlg, IDC NHS), encodeshortfloat(m4e[eid].blr.nh3));
s2ctrl(GetDlgItem(hDlg, IDC_B~TEMP), encodeshortfloat(m4e~:eid] blr.bdtemp));
s2ctrl(GetDlgItem(hDlg, IDC_BD), encodefloat(m4e[eid].blr.bd));
s2ctrl(GetDlgItem(hDlg, IDC_BDMIN), encodefloat(m4e[eid].blr.bdmin));
s2ctrl(GetDlgItem(hDlg, IDC BDMAX), encodefloat(m4e[eid].blr.bdmax));
s2ctrl(GetDlgItem(hDlg, IDC FWNA), encodefloat (naleak2fwna (m4e [eid] . blr. 1, m4e [eid] . fwflow) ) ) ;

s2ctrl(GetDlgItem(hDlg, IDC MINFwNA), encodefloat(naleak2fwna(m4e[eidl.blr.naleakmin,m4e[eid].fwflow)));
s2ctrl(GetDlgItem(hDlg, IDC MAXFWNA), ~.~ncodefloat (naleak2fwna (m4e [eid] .blr.naleakmax,m4e [eid] . fwflow) ) ) ;
trl(GetDlgItem(hDlg, IDC DP04), encodefloat(m4e[eid].blr.dpo4));
s~ctrl (GetDlgItem(hDlg, II)ClDPH) , er_codefloat (m4e [eid] .blr.dph) ;~ ;
/* note: macc4.c~s a init() may fill in min/max bd and naleak, dpo4 and dph, hence above Iines to update screen to reflect such fill-ins */
s2ctrl(GetDlgItem(hDlg,IDC DATETIME), m4sdatetime(m4e[eid],blr.lastt));
s2ctrl(GetDlgItem(hDlg,IDC' AFEED), encodef loat ( ( float ) e-_afeed ( &m4e [eid] , blr) ) ) ;
s2ctrl(GetDlgItem(hDlg,IDC BFEED), encodefloat ( (float) a bfeed(&m4e [eid] .blr) ) ) ;
s2ctrl(GetDlgItem(hDlg,ID(~_STATUSFLAGS), encodeflags(eid));
/* predictphpo4: computes and displays MACC4 predicted pH/P04. pH is computed at room temperature and without ammonia (NH3=0).*/
void predictphpo4(M4SENGINEID eid, /* id of boiler controller/model */
double deltas /* number of hours into the future to look */
static char predictstr[2*MAXSTRING+1];
E BOILER blr = m4e[eid].blr; /*make a copy so we can change with impunity*/
blr.t = m4stimeinhours(eid) + deltat;
blr.bdtemp = ROOMTEMPERATUREDEGREESF; /* Uses room temperature and */
blr.nh3 = 0.0; ;'* no NH3 to be compatible/comparable with */
~'* pH setpoint (which is so defined) */
blr.po4 = e_po4model(&blr); /* set current P04, pH to model projections*/
blr.ph - erphmodel(&blr);
sprintf(predictstr, "The %s MACC4 forecast: P04=%.5g, pH (no NH3; %g F)=%.5g Na:P04=%.5g ", m4sdatetime(blr.t), blr.po4, ROOMTEMPERATUREDEGREESF, blr.ph, a congruencyratio(&blr));
clearmarqueZ);
marque(predictstr); /* display the forecast */
/*oktostopmessage:confirmation text message for stopping a boiler controller*/
char *oktostopmessage(M4SENGINEID eid) {
static char oktostoptext[MAXSTRING+1];
sprintf(oktostoptext, "OK to stop MACC4SSP Boiler%i?", eid);
return(oktostoptext);
/*
closewarningmessage: warning. message if they try to shut down MACC4 while boilers are still running/being controlled */
char *closewarningmessage(int numberrunning) {
char boilerlist[MAXSTRING+1]={0~;
static char closewarningtext[4*MAXSTRING+1];
#define M BOILERTEXT(eid) (m4e[eid].running ? (", Boiler" #eid) : "") sprintf-(boilerlist, "%s%s%s%s%s%s%s°ss%s%s", M BOILERTEXT(0), M
BOILERTEXT(1), M BOILERTEXT(2), M BOILERTEXT(3), M B~SILERTEXT(4), M B~ILERTEXT(5).
M BOILERTEXT(6), M_BOILERTEXT(7), M HOILERTEXT(8), M BOILERTEXT(9));
sprintf(cIosewarningtext, "%s %s still running. If you close MACC4SSP now, the chemical feed rates for "

"%s will not be updated until you re-launch MACC4SSP. tTnless MACC4SSP is "
"re-~launched promptly, this could result in poor chemical control.", boilerlist+2, (numberrunn:ing > 1) ? "are" . "is", °°°imberrunning > 1) ? "tha_se boilers" . "this boiler");
return(closewarningtext);
/* setfocusnextboiler: sets focus to the next boiler's selection button*/
/* (assumes boiler selection button id's are sequential) */
void setfocusnextboilerbutton(HWND hDlg, M4SENGINEID eid) {
3etFocus(GetDlgItem(hDlg,IDC.'~ENGINEO+(eid+1)%(MAXENGINEID+1)));
/* WndProc: handles all messages Windows sends to our main window/dialog box */
long FAR PASCAL WndProc(HWND hDlg, WORD message, WORD wParam, LONG lParam) static int numberrunning = 0;
static BOOL windowlocked = TRUE;
HWND hCtrl - LOWORD(lParam); /* parse messages to controls */
lilt ctrlmsg = HIWORD (lParam) ;
lilt eid = 0 ;
switch(message) {
case WM CREATE:
/* _ l~lote that SSP need not be running at this point; if SSP communications are down for an extended period we "ride the clutch" (c.f WM TIMER
case) until SSP communications are restored and sciinitT)==ERROR NONE.
*/
for (numberrunning = 0, eid = MINENGINEID; eid <= MAXENGINEID; eid++){
m4e[eidJ.running=fixnanint(ini2int(eid,IDC-RUNNING), FALSE);
if (m4e[eidJ.running) {
numberrunning++;
if (ini2engine(eid)) { /* bad checksum -- alert them */
M_M4SERROR( "BAD CHECKSUM! Cannot restore state of a running engine!");
logaction(eid, IDC LAUNCH);
m4e[eidJ.currentevent = nullevent;
updatessppoints(ei.d);/*brings SSP points to well defined state*/
/*.current engine id is always stored in the default engine's INI file */
currentengineid=fixnanint(ini2int(DEFENGINEID,IDC CURRENTENGINEID), DEFENGINEID);
M ASSERT(currentengineid >= MINENGINEID);
M ASSERT(currentengineid <= MAXENGINEID);
i~ (currentengineid < MINENGINEID ~~ currentengineid > MAXENGINEID) currentengineid = DEFENGINEID; /* defensive programming */
windowlocked = TRUE; %* user must enter password to change/close */
break;
case WM SETFOCUS:
if (windowlocked) SetFocus(GetDlgItem(hDlg, IDC~PASSWORD));
break;
~r case WM COMMAND:
awitch(wParam) {
case IDC LOGTOFILE: /* record change in log status in ini file*/
m4e[currentengineid].logtofile = getcheckboxstate(hCtrl);
int2ini(currentengineid, IDC_LOGTOFILE, m4e(currentengineid] logtofile);
break;
case IDC SIMULATESSP:
m4e[currentengineid].simulatessp = getcheckboxstate(hCtrl);
int2ini(currentengineid, IDC SIMULATESSP, m4e[currentengineid]rsimulatesap);
break;
case IDC LOCALINPUT:
m4e[currentengineid].localinput = getcheckboxstate(hCtrl);
int2ini(currentengineid, IDC LOCALINPUT, m4e[currentengineid]~localinput);
constraininputs(hDlg, currentengineid, windowlocked);
s2ctrl(GetDlgItem(hDlg, IDC_POINTNAMES),pointnames(currentengineid));
break;
case IDC NH3ENABLED:
m4e[currentengineid].nh3enabled = getcheckboxstate(hCtrl);
int2ini(currentengineid, IDC NH3ENABLED, m4e (currentengineid] ~nh3-enabled) ;
if (!m4e[currentengineid].nh3enabled) { /*if NH3 not used*/
s2ini(currentengineid, IDC NH3, NANSTRING); /*clear in INI file*/
s2ctrl(GetDlgItem(hDlg, IDC_NH3), NANSTRING);/*as well as on form*/
}onstraininputs(hDlg, currentengineid, windowlocked);
s2ctrl(GetDlgItem(hDlg, IDCiPOINTNAMES),pointnames(currentengineid));
break;
case IDC UPDATE:
case IDC UNDO:
case IDC REINIT:
M ASSE~tT(m4e[currentengineid].localinput);
int2ini(currentengineid, wParam, TRUE);
SendMessage(hDlg, WM_TIMER, M4STIMERID, NULL);
break;
case IDC LOCKWINDOW:
windowIocked = TRUE;
s2ctrl(GetDlgItem(hDlg, IDC PASSWORD),""); /* clear password */
constraininputs(hDlg, currentengineid, windowlocked);
if (windowlocked) SetFocus(GetDlgItemChDlg, IDC_PASSWORD));
break;
case IDC PASSWORD:
if (windowlocked && lstrcmp(ctrl2s(hCtrl), "ERIC4") _= 0) windowlocked = FALSE;
s2ctrl(GetDlgItem(hDlg, IDC PASSWORD),""); /* clear password */

constraininputs(hDlg, currentengineid, windowlocked);
SetFocus(hDlg);
break;
case IDC FORECAST:
M ASSERT(m4e[currentengineid].running);
predictphpo4(currentenc~ineid, max(O.O,fixnandbl(im2dbl(currentengineid, IDC FORECASTTIME),0.0)));
break;
/* LVumeric only entry restrictions and on-the-fly initialization file backup of edit control entries:
Invariants:
1) All edit control entries agree with those stored in the initialization file, except possibly changes made to the edit control that has the focus.
2) Edit controls below either contain a valid number, or have the focus (caret or text cursor) and contain a valid numeric prefix (., . are all examples of valid numeric prefixes that aren't valid numbers) These invariants are assumed r_o hold initially (ini2dlg() call in IDC
LOADFORM
comrnand message, sent to main window in WinMain, guarantees this).
The first invariant is maintained by writing the final numeric value in the edit control to the initialization file when the control loses the focus, unless this edit control text has not changed.
The second invariant is maintained as changes are made to the edit controls (the:se changes generate EN CHANGE messages') by the following rules:
1) Function forcenumeric() is used to either truncate or totally eliminate non-numeric entries for any changes that lead to non-numbers in those controls that do not have the focus. forcenumeric() is also applied t:o the control when it just loses the focus (via the EN~KILLFOCUS message).
2) When the control has the focus, the most recently validated text is z:ememb~red and the control text is restored to this value if any changes result in an entry that is not a valid numeric prefix. This is mostly intended to reject user entry errors (e. g. pressing the 'A' key).
*/
case IDC MINFWNA: case IDC MAXFWNA: case IDC_FWNA: case IDC BDMAX:
case IDC BDMIN: case IDC M~ case IDC BDTEMP: case IDC DPHSETPOINT:
case IDC PHSETPOINT: case IDC DPH; case IDC_PH: case ~DC_MAXP04:
case IDC MINP04: case IDC P04SETPOINT: case IDC DP04:
case IDC P04: case IDC SPCA: case IDC SPGB: case IDC P04B:
case IDC FBDEF: case IBC FBMAX: case ADC FBMIN: case IDC RATIOB:
case IDC RATIOA: case IDC P04A: case IDC FADEF:
case IDC FAMAX: case IDC FAMIN: case IDC BD:
case IDC FORECASTTIME: case IDC~FWFLOW: case IDC NH3:
switch(ctrlmsg) {
static HWND currentctrl = NULL; /* handle to control in focus*/
static BOOL etrlchan ed = FALSE; /* did control in focus change?*/
static char lasttext~MAXSTRING+1]; /* contains last valid entry */
/* in current control */
case EN SETFOCUS: /* entering the edit control */
ctrlcfianged = FALSE;

currentctrl = hCtrl;
lstrcpy(lasttext,ctrl2s(hCtrl)); /* assumes what i.s in there*/
break; !* is a valid number */
case EN_C~NGE: /* contents of edit control changed*/
if (hCtrl != currentctrl) { /* if not in focus, force numeric*/
if (!isvalidnumber(ctrl2s(hCtrl))) {
s2ctrl(hCtrl, NANSTRING);
s2ini(curreaztengineid, wParam, NANSTRING);
M M4SERROR( "Attempt to place,a non-number :in a numeric .field--entry cleared.");
else if (!inrange(decodedouble(ctrl2s(hCtrl)), pid2low(wParam), pid2high(wParam))) s2ctrl(hCtrl, NANSTRING);
s2ini(currentengineid, wParam, NANSTRING);
M M4SERROR.(rangeerror(wParam));
else if (!isnumericprefix(ctrl2s(hCtrl))) {/*in focus&invalid*/
long lastpt = SendMessage(hCtrl, EM GETSEL, 0, C);
/*next line assumes single character insertion cause3 last text change*/
/*(otherwise the location of point will be unintuitive--no great harm)*/
lastpt = MAKELONG(max(LOWORD(lastpt)-1,0), max(HIWORD(lastpt)-1,0));
s2ctrl(hCtrl,lasttext); /* restore previously validated text*/
SendMessage(hCtrl, EM SETSEL, 0, lastpt); /* and position */
M_M4SERROR("Numeric input required.");
else { /* vali.d new input: remember that control changed*/
ctrlchanged = TRUE;
lstrcpy(lasttext,ctrl2s(hCtrl)); /* store validated input*/
break;
case EN KILLFOCUS: /* exiting the edit control */
if (ctrlchangedl {
if (!isvalidnumber(ctrl2s(hCtrl))) s2ctrl(hCtrl, "");
M_M4SERROR(q'Incomplete numeric entry cleared.'");
}1se if (!inrange(decodedouble(ctrl2s(hCtrl)), pid2low(wParam), pid2high(wParam))) s2ctrl(hCtrl, NANSTRING>;
M_M4SERROR(rangeerror(wParam));
ctrl2ini(currentengineid, wParam, hCtrl);/* save the change*/
/* in the INI file*/
currentctrl = NULL;
break;
default:
break;
break;
case IDC LOADFORM: /* Use in lieu of WM INITDIALOG--there's no dlgproc*, for (eid=MINENGINEID; eid <= MAXENGIN~'ID; eid++) setbuttontext(hDlg, eid):
wParam = IDC_ENGINEO + currentengineid;
/* no break - emulates clicking current engine's button */
case IDC ENGINEO:
case IDC ENGINE1: case IDC ENGINE2: case IDC ENGINE3:
case IDC ENGINE4: case IDC~ENGINE5: case IDC ENGINE6:
case IDC ENGINE7: case IDC ENGINE8: case IDC ENGINES:
/* next Tine assumes that above ids are sequential */
currentengineid = wParam - IDC ENGINEO;
M ASSERT(currentengineid >= MINENGINEID):
M ASSERT(currentengineid <= MAXENGINEID);
int2ini(DEFENGINEID~, IDC CURRENTENGINEID, currentengineid);
ini2dlg(hDlg, currentengineid); /* load form from INI file */
updateform(hDlg, cu.rrentengineid);
SetWindowText(hDlg, r_itle(currentengineid));
constraininputs(hDlg, currentengineid, windowlocked);
/* allow for quickly reviewing each configuration in turn via the space bar:*/
setfocusnextboilerbur_ton(hDlg, currentengineid);
break;
case IDC START:
M ASSERT(!m4e[currentengineid].running);
m4e [currentengineid] . blr = *e undef izied,boiler ( ) ;
ini2engine(currentengineid);
m4e[currentengineid].starttime = timeinhours();
m4e[curre.ntengineid].lasteventtime =
m4e[currentengineid].blr.t = m4stimeinhours(currentengineid);
m4e[currentengineid].eventid = 0;
if ( NANFLT == m4e[currentengineid].fwflow ) {
M M4SERROR("Feedwater flow rate must be specified.");
SetFocus(GetDlgltem(hDlg, IDC FWFLOW));
else if ( m4e[currentengineid].hh3enabled &&
NANFLT == m4e[currentengineid].blr.nh3 ) {
M M4SERROR("Ammania must either be specified~or disabled.");
SetFocus(GetDlgItem(hDlg, IDC NH3));
else if (E INITOK != a init( &m4e[currentengineid].blr ) ) M M4SERR~R(initerr[m4e[currentengineid].blr.initstatus].msg);
SetFocusiGetDlgItem(hDlg, /* position to error related field */
initerr [m4e [currentengineid] .blr. initstatus] . field) ) ;
else ( m4e [current engine id] . running .= TRUE;
constraininputs(hDlg, currentengineid, windowlocked);
SetFocus(GetDlgItem(hDlg,IDC STOP));
updatessppointsl:currentengineid);
m4e[currentengineid].scierr = ERROR_NONE;
m4e[currentengineid].currentevent = nullevent;
engine2ini(currentengineid);
logaction(currentengineid, IDC START);
SetWindowText(hDlg, title(currentengineid));
setbuttontext(hDlg, c~.lrrentengineid);
clearmarque();
M M4SINF0("");
numberrunning++;
break;
case IDC STOP:
M ASSERT(m4e[currentengineid].running);
i~ (IDOK== MessageBox(hDlg, oktoatopmessage(currentengineid), ~'MACC4SSP Confirmation", MB OKCANCEL~MB_ICONQUESTION)) {
m4e[currentengineid].running = FALSE;
constraininputs(hDlg, currentengineid, windowlocke<i);
SetFocus(GetDlgItem(hDlg,IDC START));
int2ini(currentengineid,IDC RUNNING, m4e[currentengineid].running);
logaction(currentengineid, IDC STOP);
updateform(hDlg, currentengineid);
SetWindowText(hDlg, title(currentengineid));
setbuttontext(hDlg, currentengineid);
numberrunning--;
break;
break;
case WM TIMER:
if (numberrunning > 0) {
for (eid=MINENGINEID; eid<=MAXENGINEID; eid++) if (m4e [eid] . running) {
SCIError sciiniterr = sciinit(eid);
if (ERROR NONE==sciiniterr && !m4e [eid] .badchecksurrO
/*link to SmartCard inititialized OK and engine is running and we don't have a bad checksum (__> undefined controller state) */
m4e[eid].blr.t = m4stimeinhours(eid); /* update current time */
if (m4e [eid] . simulatessp && ! m4e Ceid] . localinput'~
simulateoperator(eid); /* simulate operator updates, etc. */
switch (getssprec~uest (eid) ) {
HCURSOR holdcursor;
case NOOPSSPREQUEST: /* no action requeste,i */
break; ' case UNDOSSPREQUEST:
holdcursor = SetCursor(LoadCursor(NULL,Iii_W,~~IT));
if (E INITOK != m4e[eid] .blr.initstatus m4stimeinhours (eid) > m4e [eid] .blr.lastt~-UNDO INTERVAL
! a undo ( ~m4e [eid] . blr) ) M.M4SERROR("Could not UNDO");
updatessppoints(eid);
engine2ini (e:id) ;
logaction(eid, IDC UNDO);
SetCursor(holdcursor);
break;
case UPDATESSPREQUEST:
holdcursor = SetCursor(LoadCursor(NULL,IDC WAIT));
if (E INITOK == m4e[eid].blr initstatus) if Te update (&m4e [eid] .blr) & E NOTUPDATED) M M4SERROR("Update failed.");
else if (m4e[eid].blr.updatestatus != E UFDATEOK) M M4SERROR("Update warning--check flags.");
updatessppaints(eid);
engine2ini(eid);
logaction(eid, IDC UPDATE);
SetCursor(holdcursor);
break;
case REINITSSPREQUEST:

holdcursor = SetCursor(LoadCursor(NULL,IDC WAIT));
m4e[eid].blr.bd = E NAN; /* set bd, na leak to their */
m4e[eid].blr.l = E NAN; /* MACC4 determined defaults*/
if (e init(&m4e[eidT.blr)!=E INITOK) M M4SERROR("REINIT Failed.");
updatessppoints(eid);
engine2ini(eid);
logaction(e.id, IDC REINIT);
SetCursor(ho7.dcursor);
break;
case NODATASSPREQUEST:
M_M4SERROR( "Could not read required data to service request ");
/* no break */
case OUTOFRANGESSPREQUEST: /* numeric parameters out of range*/
case AMBIGUOUSSSPREQUEST: /* multiple requests--clear them */
m4e[eid].actionsuptodate = FALSE;
break;
f (E INITOK =- m4e [eid] . blr . initstatus ) scisetdouble(eid,IDC AFEED,e afeed(&m4e~eid].blr));/vset feed*/
scisetdouble(eid,IDC BFEED,e-bfeed(&m4e[eid].blr));/*pumps */
if ( !m4e [eid] .local input) { -scisetdouble(eid,IDC BD, m4e[eid].blr.bd);
scisetdouble(eid,IDC FWNA, naleak2fwna (m4e [ei3] .blr.l,m4e [eid] . fwflow) ) ;
scisetdouble(eid,IDC RATIO, e-congruencyratio(&m4e[eid].blr));
} maybeclearactions(eid); /* clear action flags */
if (ERROR NONE =- sciiniterr) {
maybesetflags(eid); /* service flag setting requests */
scifini(eid); /* close link to SSP */
} %* end of "if running" */
} } /* end of looping over each engine id */
if (m4e[currentengineid].running) { /* make form reflect any changes*/
updateform(hDlg, currentengineid); /* due to operator inputs/actions */
SetWindowText(hDlg, title(currentengineid));
}eturn(0) ;
case WM CLOSE:
if (windowlocked) { /* cannot close locked window */
M M4SERROR("You must enter the password before closing MACt'4SSP");
return(0);
}
else if (numberrunning > 0 &&
IDOK != MessageBox(hDlg, closewarningmessage(numberrunning), "MACC4SSP Warning", MB OKCANCEL~MB_ICON~:AND)) return(0); /* short circuit close *%
else { /* log the user's CLOSE action for each running boil.en */
for (eid=MINENGINEID; eid <= MAXENGINEID; eid++) if (m4e [eid] . running) logaction(eid, IDC CLOSE);
} /* fall through to ordinary Windows WM_CLOSE */
break;

case WM DESTROY:
KillTimer(hDlg,, M4STIMERID);
PostQuitMessage(0);
" return ( 0 ) ;
default:
break;
return DefWindowProc (hDlg, message, wParam, lParam);
/* WinMain: uses a dialog box as the main window as per the HEXCALC example*/
/* program from Petzold's book "Programming Windows" (Microsoft Preas) */
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) {
MSG msg;
WNDCLASS wndclass;
HC:URSOR holdcursor;
hmainwin = NULL;
lpszCmdLine = lpszCmdLine; ;'* to supress compiler warning */
if: (hPrevInstance) { M M4SFATALERROR("MACC4SSP already running; switch to MACC4SSP instead.");
return (0);
else { /* register the class used by our dialog box/main window */
wndclass.style = CS HREDRAW ~ CS VREDRAW;
wndclass.lpfnWndProc = (WNDPROC)~WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = DLGWINDOWEXTRA;
wndclass.hlnstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(MACC4SSP_I~ON));
wndclass.hCursor = LoadCursor(NULL,IDC ARROW);
wndclass.hbrBackground = COLOR WINDOW+1;
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = M4SMODULENAME;
RegisterClass(&wndclass);
holdcursor = SetCursor(LoadCursor(NULL,IDC WAIT));
hrnainwin=CreateDialog(hInstance, MAKEINTRESOURCE(MACC4SSP DIALGG), 0, NULL);
SetCursor (holdcursor) ;
if (hmainwin =~ NULL) M M4SFATALERROR("MACC4SSP fatal error: cannot create main window.");
return(0);
}:Lse if (!SetTimer(hmainwin, M4STIMERID, DEFTIMERINTERVAL, NULL)) M M4SFATALERROR("MACC4SSP fatal error: cannot create Windows tim{r");
return(0);
}:Lse ShowWindow(hmainwin, nCmdShow);
SendMessage(hmainwin, WM COMMAND, IDC LOADFORM, 0); /* loads d:g box */
/* ~~annot do this in WM~CREATE because Windows creates dialog box controls*/

/* AFTER it sends WM CREATE; plays the role WM INITDIALOG usually ~.rould */
/* if we were using an ordinary DlgProc (we are using a WndProc in:atead)*/
while (GetMessage(&msg, NULL, 0, 0)) {f (!IsDialogMessage(hmainwin, &msg)) TranslateMessage(&msg);
DispatchMessage(&msg);
return (msg.wParam);

char *encodenumber(double x, const char *fmt) {
static char encodeddouble[MAXSTRING+1];
~~ (x =- NANFLT) st{cpy(encodeddouble, NANSTRING);
else sprintf(encodeddouble, fmt, x);
compressexponent(encodeddouble);
return(encodeddouble);
/*en.codedouble: convert a double-with full precision--to a character string */
char *encodedouble(double x) return(encodenumber(x,DOUBLEPRECISIONFORMAT));
/* d.ecodedouble: convert a character string to a double */
double decodedouble(char *s) return ( (lstrcmp(s,NANSTRING) -- 0) ? NANFLT . atof(s) );
/*en.codefloat: convert a double to a full float precision character string */
char *encodefloat(double x) {
return(encodenumber(x,FLOATPRECISIONFORMAT));
/*encodeshortfloat: convert double to less than full float precision string*/
char *encodeshortfloat(double x) {
return(encodenumber(x,SHORTFLOATPRECISIONFORMAT));
/*encodeint: convert an integer to a character string */
char *encodeint(int i) {
static char encodedint[MAXSTRING+1];
if (NANINT =- i) lstrcpy(encodedint,NANSTRLNG);
else sprintf(encodedint, "%i", i);
return(encodedint);
/*decodeint: convert a character string to an integer */
int decodeint(char *s) {
return( (lstrcmp(s, NANSTRING) -- 0) ? NANINT . atoi(s) );
char' *rangeerror(M4SPARAMID pid) static char rangeerrortext[MAXSTRING+1];
M4SPARAM *param = pid2param(pid);
sprintf(rangeerrortext, "%s must obey: %s%s", param->name, encodefloat(param->low), (param->low -- NANFLT) ? "" . " .:,_ ");
sprintf(rangeerrortext + lstrlen(rangeerrortext), "%s%s%s -- entry cleared.", param->name, (param->high =- NANFLT) ? "" . " <= ", encodefloat(param->high));
ret.urn(rangeerrortext);

;* ~_imeinhours: returns time in hours since Jan l, 1970 */
dots ~ timeinhours (void) ( return ( ((double) time~NULL))/SECONDSPERHOUR );
/* m4stimeinhours: returns the time seen (simulated or real) b~; engine */
double m4stimeinhours(M4SENGINEID eid) {
return (!m4e[eid].simulatessp ?
timeinhoursC) . (m4e[eid].starttime +
(SIMHOURSPERREALSECOND*SECONDSPERHOUR*(timeinhours()-m4e[eic::~].starttime))));
/* m4sdatetime: given hours since Jan 1, 1970, returns datetime:: string */
chair *m4sdatetime(double m4stime) static char datetime[MAXSTRING+1];
tame t t = (time t) (m4stime * SECONDSPERHOUR); /* convert tc~ C time format*/
strftime(datetime,MAXSTRING,"%d-%b-%Y %X", localtime(&t));
return(datetime);
/* begin: code to perform conversions from macc4.c module's in!.ernal moles/hr sodium leak units to the feedwater (lbs/hr) plus f~:edwater Na (ppb) ~ralues that macc4ssp presents to the user *,i #de:Fine MW NA 22.99 /* molecular weight of Na */
double lb2kg(double 1b) ~ return((NANFLT == 1b) ? NANFLT : (1b~0.45359));
double kg2lb(double kg) return((NANFLT = kg) ? NANFLT (kg'0.45359));
double ppb2molesperkg(double ppb, double mw) {
re turn ((ppb =- NANFLT) ? NANFLT . (ppb*l.Oe-6/mw));
double molesperkg2ppb(double mpk, double mw) {
return ((mpk =- NANFLT) ? NANFLT . (mpk*l.Oe+6*mw));
/* nanmult: NaN aware multiplir_ation ~/
double nanmult(double x1, double x2) return ((xl==NANFLT (~ x2==NANFLT) ? NANFLT . (xl*x2));
%* nandiv: NaN aware division */
double nandiv(double x1, double x2) return ((xl==NANFLT ~I x2==NANFLT f~ x2==0.0) ? NANFLT . (xl~x2));
%* naleak2fwna: converts the Na leak units (moles/hr) used by nacc4.c to feedwater Na (ppb) used by macc4ssp.c's user interface.
feed water flow in lbs/hr.*,/
dou:~le naleak2fwna(double naleak, double fwflow) {
return(molesperkg2ppb(nandiv(naleak,lb2kg(fwflow)), MW-NA)I;
/* fwna2naleak: inverse of naleak2fwna Csee above) */
double fwna2naleak(double fwna, double fwflow) return(nanmult(ppb2molesperkg(fwna, MW NA), lb2kg(fwflow)));
/*end: code to perfarm unit conversions */
/*fixnanint: defaulting for undefined ints */

int fixnanint(int i, int fornan) {
rE:turn ( i==NANINT '.' fornan .. i ) ;
douk.-_.p~ fixnandbl (double x, double fornan) {
reaurn (x==NANFLT '.' fornan . x) ;
/* m4spath: MACC4SSP path is r_he directory in which the EXE is located */
/* the trailing \ is included in this path ( e.g. C:\MACC4SSP\ ) */
char *m4spath(void) static char path[2*MAXSTRING+1] ;
int len =
GetModuleFileNarne(GetModuleHandle(M4SMODULENAME), path, 2*MAXS''RING);
for (len--; len >= 0 && path[len]!='\\'; len--) /* zap name.ext part*/
path [len] _' \0' ;
reaurn (path) ;
/* carl2s: returns t:he string associated with a control (or wir..dow) */
char *ctrl2s(HWND hCa=rl) {
static char s [MAXS'CRING+1] ;
GeaWindowText (hCtr:l, s, MAXSTRING) ;
reaurn ( s ) ;
/* ~;2ctrl: copies teat to the given control */
void s2ctrl(HWND hCt:rl, char *s) {
SetWindowText (hCtr:L, s) ;
static char marquebu:E[2*MAXSTRING+1]; /* stores scrolling marque */
/* c:learmarque: clears. the marque */
void clearmarque (void) {
l:>trcpy(marquebuf,,"");
/* marque: uses message line as a scrolling marque */
void marque (char *s;l static BOOL firsttime = TRUE;
i.f: (firsttime) clearmarque();
firsttime = FALSE;
lstrcat(marquebuf, s); /* add new string to end of old one */
ii. (lstrlen(marquebuf) > MAXSTRING) /*destroy older part of rnarque*/
lstrcpy(marquebuf, marquebuf+lstrlen(marquebuf)-MAXSTRING);
s2ctrl(GetDlgItem(hmainwin, IDC MESSAGELINE),marquebuf);
UpdateWindow(GetDlgItem(hmainwin, IDC MESSAGELINE));
/* nameprefix: returns the macc:4 prefix associated with a given engineid */
chair *nameprefix(M4SENGINEID eid) {
sciatic char prefix [MAXSTRING+:L] ;
sprintf(prefix, "M%i", eid);
r~=_turn (prefix) ;

/* ~nifile: returns the name of the engine's initialization file */
cha .vinifile(M4SENGINEID eid) return(lstrcat(lstrcat(m4spat:h(),nameprefix(eid)),".INI"));
/*m4spointname: SSP point name for engineid/paramid pair */
char *m4spointname(:M4SENGINEID eid, M4SPARAMID pid) static char pointname[MAXSTRING+1];
lstrcpy(pointname, nameprefix(eid));
return(lstrcat(pointname,pid2name(pid)));
/* updatechecksum: simple has: to enforce I/O consistency*/
void updatechecksum(M4SENGINEID eid, M4SPAR.AMID pid, char *s) ;, if (pid != IDC CHECKSUM) for (; *s; s++) if (isdigit (*s) ) m4e [eid] .checksum ~_ (*s -- ' 0' + 1) ;
/* .ini2s: reads a string from an initialization file. */
char *ini2s(M4SENGINEID eid, M4SPARAMID pid) static char s [MAXSTRING+1] ;
GestPrivateProfile;String( M4SMODULENAME, pid2name(pid), NANSTRING, s, MAXSTRING, inifile(eid));
updatechecksum(eid, pid, s);
rE~turn ( s ) ;
/* s2ini: writes a string to an initialization file */
void s2ini(M4SENGINEID eid, M4SPARAMID pid, char *s) ij°
(!WritePrivate:ProfileString(M4SMODULENAME,pid2name(pid),s;inifile(eid))) M M4SERROR("Error writing to initialization file.");
updatechecksum(eid, pid, s);
/* ctrl2ini: writes contents of a control to associated INI file keyword */
void ctrl2ini(M4SENGINEID eid, M4SPARAMID pid, HWND hCtrl) {
s'.?ini (eid, pid, c~trl2s (hCtrl) ) ;
/* ini2ctrl: reads .associated ini. file keyword into a control v/
void ini2ctrl(HWND hCtrl, int eid, int pid) {
s2ctrl(hCtrl, ini2s(eid,pid));
void dbl2ini(M4SENGINEID eid, M4SPARAMID pid, double x) s2ini(eid,pid,encodedouble(x));
/* :ini2dbl: reads associated ini file keyword into a memory */
double ini2dbl(M4SENGINEID eid,, M4SPARAMID pid) return(decodedouble(ini2s(eid,pid)));
/* int2ini: writes contents of field to associated ini file kevyword */

void int2ini(M4SENGINEID eid, M4SPARAMID pid, int i) {
:~2ini (eid, pid, encodeint (:i) ) ;
/* ini2int: reads associated in.i file keyword into a memory */
int ini2int(M4SENGIN13ID eid, M4SPARAMID pid) return(decodeint(ini2s(eid,pid)));
/*pointnames: return:a name of points string */
char *pointnames(M4SH:.NGINEID eid) ( const char *pointsprefix = "Points: ";
static char pointnamebuf [2*MAXSTRING+1] ;
if: (m4e [eid] .local input) sprintf(pointnamebuf, "%sm%i { (afeed,bfeed,flag7,flagl2)", pointsprefix, eid);
else sprintf (pointnawebuf , "%sm%i +
(afeed,bfeed,ph,~o4,%sbdtemp,%supdate,undo,reinit,flag~l,...,flag12)", pointsprefix, eid, rn4e[eid].nh3enabled ? "nh3," : "", rn4e [eid] . local input ? "" . "bd, fwna, ratio, ") ;
return (pointnamebui: ) ;
*geacheckboxstate: '."RUE if checkbox checked, else FLASE */
BOOL~ ge~tcheckboxstat;e: (HWND hcheckboxctrl) ( re:turn(SendMessage:(hcheckboxctrl, BM_GETCHECK, 0, 0) ? TRUE . FALSE);
%* setcheckboxstate: checks or unchecks a checkbox */
void. setcheckboxstate:(HWND hcheckboxctrl, BOOL checked) Se:ndMessage(hcheckboxctrl, BM SETCHECK, checked, 0);
/* ini2dlg: loads al:. fields from the ini file into dialog box (main window) */
/*
checkboxes, which indicated controller modes, are loaded into memory a.s well as into the form--this makes modes easily available (without disk access) later on.
*/
void ini2dlg(HWND hDlg, int eid) M4SPARAM *currentct:rl = pid2param(IDC FIRSTCONTROL);
M4SPARAM *lastctrl - pid2param(IDC_LASTCONTROL);
#define M INI2CHECKBOX(wheretostore,pid,default) \
wheretostore = fixnanint(ini2int(eid,pid),default), \
se~tcheckboxstate (GEstDlgItem (h.Dlg, pid) , wheretostore) M INI2CHECKBOX(m4e[eid].logtofile, IDC LOGTOFILE, TRUE);
M~_INI2CHECKBOX(m4e[eid].simulatessp, IDC_SIMULATESSP, FALSE);
M_INI2CHECKBOX(m4e[eid].localinput, IDC LOCALINPUT, FALSE);
M INI2CHECKBOX(m4e[eid].nh3enabled, IDC NH3ENABLED, FALSE);
/*make point names on form match mode of-engine just loaded into form:*/
s2ctrl(GetDlgItem(hDlg, IDC~POINTNAMES), pointnames(eid));
for ( ; currentctr:l <= lastctrl; currentctrl++) /*load form's edit controls*/
if (STOREDEDIT CONTROL == currentctrl->type) /* with this engine's info */
ini2ctrl(GetDlgItem(hDl.g, currentctrl->pid), eid, currentctrl->pid);

/*
c:onstraininputs: ~~onstrains inputs to the control based on both rv=°°~ kinds of user:a who are allowed to access it as well as the editions under which access to that control is allowed. This routine either enables or disables each control accordingly.
*/
void constraininputs(HWND hDlg, M4SENGINEID eid, BOOL windowlocked) M~6SpAR.AM *currentctrl = pid2param ( IDC FIRSTCONTROL) ;
M~6SPARAM *lastctr:L - pid2param(IDC LASTCONTROL);
BOOL localinput - m4e[eid].localinput;
BOOL running m4e zeid] . running;
BOOL nh3enabled - m4e~~eid].nh3enabled;
BOOL userisamanage:r - !wind.owlocked;
for ( ; currentctr_:L <= lastctrl; currentctrl++) BOOL manageronlyctrl - MANAGERONLY LEVEL& currentctrl-,~leve:L;

BOOL operatoron:Lyctrl= ~FERATORONLY LEVEL& currentctrl->level;

BOOL staticctr:L STATIC ACCESS & currentctrl->access;

BOOL dynamicctr_L , & currentctrl-~~access;
- DYNAMIC ACCESS

BOOL localctrl - LOCAL ACCESS & currentctrl-~~access;

BOOL nh3ctrl - NFi;~ ACCESS ~ currentctrl-.access;

BOOL enablectr:l = TRUE; /* enabled until proven otherwise */
/* Decide if control is enabled by a process of elimination: consider, in turn, each co,:ldition which could require that the control be disabled*/
if (manageronly~~trl && !userisamanager) enablectrl = FALSE;
else if (operat~~ronlyctrl && userisamanager) /* password prompt */
enablectrl = FALSE; /*(managers have already entered the password)*/
else if (nh3ctrl && !nh3enabled) enablectrl = FALSE;
else if (static~trl && running) enablectrl = FALSE;
else if (dynam:icctrl && !running) enablectrl = FALSE;
else if (localctrl && running s~& !localinput) enablectrl = FALSE;
else if (localctrl && !runni.ng ~& !userisamanager) enablectrl = FALSE;
/* else, since theta are no eb:jections from the authorities, control enabled*/
EnableWindow(GetDlgItem(hDlg, currentctrl->pid), enablectrl);
/* :ini2engine: loads all parameters from i.ni file into memory */
/* .returns badchecksum: TRUE implies a bad check sum/corrupted file*/
BOOL ini2engine(M4SENGINEID eid) O
int oldchecksum = ini2int(eid, IDC CHECKSUM);
m~4e [eid] .blr.po4 = ini2dbl r: eid, IDC Po4 ) ;
m~4e [eid] . blr . ph = ini2dbl ( e:id, IDC_PH ) ; .
m~4e [eid] .blr.bdtem.p = ini2db.L ( eid, IDC BDTEMP ) ;
m4e[eid].blr.nh3 = ini2dbl~; r=_id, IDC_NH~ );
m4e[eid].lasteventtime = ini2int ( eid, IDC LASTEVENTTIME);
m4e [eid] . eventid - ini2int ( eid, IDC--EVENT~D) ;
m4e[eid].checksum. = 0; /* we don't include first few params in checksum */
/* cause they can legitimately change */

m~4e [eid] . starttime = ini2dbl 1' eid, IDC STARTTIME) ;
m~fe [eid] . running - fixnanint (ini2int ( eid, IDC RUD1NING) , FALSE) ;
m "y'[eid].logtofile = fixnanint(ini2int ( eid, IDC LOGTOFILE), TRUE) m ~eid] . blr. t = :i:ni2dbl ( eid, IDC_T ) ;
m4e[eid].blr.famin = ini2dbl( eid, IDC FAMIN );
m4e[eid].blr.famax = ini2dbl( eid, IDC-FAMAX );
m4e(eid].blr.fadef = ini2dbl( eid, IDC~FADEF );
m4e[eid].blr.po4a = ini2dbl( eid, IDC P04A );
m4e[eid].blr.ratioa = ini2dbl( eid, IDC RATIOA );
m4e[eid].blr.spga = ini2dbl( eid, IDC SPGA );
m4e[eid].blr.fbmin = ini2dbl( eid, IDC_FBMIN );
m4e [eid] .blr. fbma;!c = ini2dbl ( eid, IDC FBMAX ) ;
m4e [eid] .blr. fbde:E = ini2dbl ( eid, IDC-FBDEF ) ;
m4e[eid].blr.po4b = ini2dbl( eid, IDC P04B );
m4e [eid] .blr.ratio:o = ini2d:bi. f eid, IDC RATIOe ) ;
m4e[eid].blr.spgb = ini2dbl( aid, IDC SPGB );
m4e [eid] .blr.m = :i:ni2dbl ( eid, IDC M T~;
m4e[eid].blr.dpo4 - ini2dbl( e.id, IDC DP04 );
m4e [eid] .blr.po4setpoint = ir~i.~dbl ( eid, IDC P04SETPOINT ) ;
m4e[eid].blr.minpo4 = ini2dbl.( eid, IDC MINP04 );
m4e [eid] .blr.maxpo4 = ini2dbl. ( eid, IDC' MAXP04 ) ;
m4e [eid] . blr. dph := ini2dbl ( eid, IDC DPH ) ;
m4e [eid] . blr . phsetpoint = i:ni.~dbl ( eid, IDC PHSETPOINT ) ;
m4e[eid].blr.dphsetpoint = ini2dbl( eid, IDC DPHSETPOINT );
m4e [eid] .blr.napo4ratio = i:ni.a?dbl ( eid, IDC NAP04RATI0 ) ;
m4e[eid].blr.minna.po4ratio = ini2dbl( eid, IDC MINNAP04RATI0 );
m~6e [eid] .blr.maxn<~yo4ratio = ini2dbl ( eid, IDC' MAXNAP04RATI0 ) ;
m~6e [eid] . blr. max :a.~mple interval = ini2dbl ( ei3, IDC MAX SAMF?LE
INTERVAL ) ;
m4e [eid] .blr.maX owtofbox adjustment =
ini2dbl ( eid, I17~S MAX OL1TOFBOX ADJUSTMENT ) ;
m~6e (eid] .blr.max :r.=_Iative error = ini2dbl ( eid, IDC MAX RELATIVE ERROR
) ;
m~6e (eid] .blr.beps = ini2db1 ( eid, IDC BEPS ) ;
m~6e [eid] . blr . bdmin = ini2dbl ( eid, IDC BDMIN ) ;
m4e [eid] . blr. bdmax = ini2dbl ( eid, IDC' B'DMAX ) ;
m~6e [eid] . fwflow = ini2dbl ( ei.d, IDC FWFLOW) ;
m~6e [eid] . blr. naleakmin = fwna2nalea>-cc ( ini2dbl ( eid, IDC MiNFWNA ) , m4e (eid] . fwflow) ;~
m4e[eid].blr.naleakmax = fwna~naleak(ini2dbl( eid, IDC_MAXFWNA ), m4e [eid] . fwflow) ;
m~6e [eid] .blr.l = fwna2naleak (ini2dbl ( eid, IDC FWNA) , m4e [eid] .~wflow) ;
mile [eid] .blr. fdt :- ini2dbl ( eid, IDC FDT ) ;
m4e [eid] .blr.last't = ini2dbl ( eid, IDC LASTT ) ;
m4e[eid].blr.lastbdtemp = ini2dbl( eid, IDC LASTBDTEMP );
m4e [eid] .blr. lastpo4 = ini2dbl ( eid, IDC ~:~ASTP04 ) ;
m4e [eid] .blr.lastnh3 = ini2dbl ( eid, IDC' LASTNH3 ) ;
m4e [eid] .blr.lastph = ini2dbl ( eid, IDC LASTPH ) ;
m4e[eid].blr.b4lastt = ini2dbl( eid, IDC'. B4LASTT );
m4e[eid].blr.b4lastbdtemp = ini2dbl( eid, IDC B4LASTBDTEMP );
m4e [eid] .blr.b4lastpo4 = ir..i:?dbl ( eid, I:DC B4LASTP04 ) ;
m4e [eid] .blr.b4lastnh3 = ini:?dbl ( eid, IDC B4LASTNH3 ) ;
m4e[eid].blr.b4lastph = ini2dbl( eid, IDC B4LASTPH );
m4e [eid] . blr . bd = ini2dbl ( ei<i, IDC BD ) ;
m4e[eid].blr.lastl = ini2dbl( eid, IDC LASTL );
m4e [eid] .blr.last:bd = ini2dbl ( eid, IDC LASTBD ) ;
m4e [eid] .blr.dt [0] - ini2dbl ( eid, IDC I~T1 ) ;
m4e [eid] .blr.dt [1] - ini2dbl ( eid, IDC' UT2 ) ;
m4e [eid] .blr. fb [0] - ini~dbl ( eid, IDC: FB1 ) ;
m4e [eid] . blr. fb (1] - ini2dbl ( eid, IDC' FB2 ) ;
m4e [eid] .blr. fb [2] - ini2dbl ( eid, IDCiFB3 ) ;
m4e [eid] .blr. fa [0] - ini2d.fa1 ( eid, IDC' FAl ) ;
m4e [eid] . blr. fa [1] - ini2dbl ( eid, IDC_FA2 ) ;
m.4e [eid] .blr. fa [2] - ini2dbl ( eid, IDC_FA3 ) ;
m4e[eid].blr.lastdt[0] - ini:2db1( eid, IDC LASTDT1 );
m4e [eid] .blr.lastdt (1] - ir~i:zdbl ( eid, ~:DC LASTDT2 ) ;

m4e [eid] .blr.lastfa [0] - ini2dbl ( eid, IDC LASTFA1 mv~e [eid] .blr. lastfa [1] - ini2dbl ( eid, IDC-LASTFA2 ) ;
muse (eid] .blr.lastfa [2] - ini2dbl ( eid, IDC-LASTFA3 ) ;
m~'.~~ [eid] .blr.lastfb[0] - ini2dbl ( eid, IDC-LASTFB1 ) ;
m eid].blr.lastf:b[1] - ini2dbl( eid, IDC LASTFB2 );
m4e[eid].blr.last.E:b[2] - ini2dbl( eid, IDC LASTFB3 );
m4e [eid] . blr . updatestatus =
fixnanint(ini2.int( eid,IDC: UPDATESTATUS),E UPDATEOK);
m4e [eid] .blr.lastupdatestatus~-~=
fixnanint(ini2:i:nt( eid, IDC LASTUPDATESTATUS ), E_UPDATEOF~;);
m4e[eid].blr.initstatus =fixnanint(ini2int( eid, IDC INITSTATUS),!E INITOK);
m4e[eid].blr.undoa:ble = fixnanint(ini2int( eid, IDC UNDOABLE), FALSE);
m4e[eid].simulateasp = fixnani.nt(ini2int ( eid, IDC SIMULATE:ySP),:»ALSE);
m4e[eid].localinput = fixnani.nt(ini2int ( eid, IDC LOCALINPUT),FALSE);
m4e [eid] .nh3enabl~=_~~ = fixnanint (ini2int ( eid, IDC~NH3ENABLEL~) , FALSE) ;
return (m4e [eid] . badchecksum =
(m4e[eid].running &.~ /* ignore if engine nc>t running or */
oldcheckaum != IGNOF:ECHECKSUM && /* if ignore checksum is flagged */
oldchecka~am ! = m4e [e~id] . checksum) ) ;
/* engine2ini: writers in memory engine representation to INI file */
void engine2ini(M4SE1VGINEID eid) dY~l2ini ( eid, IDC :P04, m4e [ei.d] .blr.po4 ) ;
dbl2ini ( eid, IDC-_:PH, m4e (eid] .blr.ph ) ;
dbl2ini ( eid, IDC _13DTEMP, m4e [eid] . blr . bdtemp ) ;
dbl2ini ( eid, IDC 1VH3, m4e (ei.d] .blr.nh3 ) ;
int2ini ( eid, IDC_LASTEVENTTIME, m4e (eid] .lasteventtime ) ;
int2ini ( eid, IDC EVENTID, as4e [eid] . event id ) ;
m9:e[eid].checksum = 0; /* don't include above params in checksum*/
/* cause they can legitimately change */
dbl2ini ( eid, IDC_STARTTIME, m4e [eid] . starttime) ;
int2ini ( eid, IDr_RUNNING, m4e [eid] . running ) ;
int2ini ( eid, IDC~LOGTOFILE, m4e[eid].logtofile );
dbl2ini ( eid, IDC _'r, m4e [eid] . blr. t ) ;
dbl2ini ( eid, IDC_:~AMIN, m4e (.eid] .blr. famin ) ;
dbl2ini ( eid, IDC_:»AMAX, m4e [,ei.d] .blr. famax ) ;
dbl2ini ( eid, IDC_:~ADEF, m4e [;eid] .blr. fadef ) ; .
dY~l2ini ( eid, IDC _P04A, m4e [e~id] . blr . po4a ) ;
dbl2ini ( eid, IDC_'.~ATIOA, m4e [eid] .blr.ratioa ) ;
dbl2ini ( eid, IDC_,3PGA, m4e [eid] .blr. spga ) ;
dbl2ini ( eid, IDC_:FBMIN, m4e f.eid] .blr. fbmin ) ;
dbl2ini ( eid, IDC_:FBMAX, m4e [eid] .blr. fbmax ) ;
dbl2ini ( eid, IDC_FBDEF, m4e (:ei.d] .blr. fbdef ) ;
dbl2ini ( eid, IDC_P04B, m4e [eid] .blr.po4b ) ;
dbl2ini ( eid, IDC_:f2ATIOB, m4e I.eid] .blr. ratiob ) ;
dbl2ini ( eid, IDC ,SPGB, m4e Ceid] . blr. spgb ) ;
dbl2ini ( eid, IDC-_1H, m4e [eid] .blr.m ) ;
dbl2ini ( eid, IDC_DP04, m4e [e~id] .blr.dpo4 ) ;
dbl2ini ( eid, IDC_P04SETPOIN'f, m4e [eid] .blr.po4setpoint ) ;
dbl2ini( eid, IDC MINP04, m4e~[eid].blr.minpo4 );
dbl2ini ( eid, IDC~_'~lAXP04, m4e: [eid] .blr.maxpo4 ) ;
dbl2ini ( eid, IDC_DPH, m4e [ei.d] .blr.dph ) ;
dY~l2ini ( eid, IDC_PHSETPOINT, m4e [eid] .blr.phsetpoint ) ;
dbl2ini( eid, IDC_DPHSETPOINT, m4e[eidJ.blr.dphsetpoint );
dbl2ini( eid, IDC_:~1AP04RATI0, m4e(eid].blr.napo4ratio );
dbl2ini ( eid, IDC_~IINNAP04RA7.'I0, m4e Ceid] .blr.minnapo4ratio dbl2ini( eid, IDC_~IAXNAP04RATI0, m4e[eid].blr.maxnapo4ratio l;
dbl2ini ( eid, IDC_;~IAX SAMPLE INTERVAL, m4e [ei.d] .blr.max_sampie_interval ) ;
dbl2ini ( eid, IDC :~IAX OUTOFB2)X ADJUSTMENT, m4e~E~id] .blr.max outofbox adjustment ) ;
dbl2ini( eid, IDC MAX RELATIVE ERROR, m4e[eid].blr.max relative error );
dbl2 ini ( eid, IDC_ F3EPS . m4e [ei3] . blr . beps ) ;
dr'"'~ini ( eid, IDC F3DMI1V, m4e [eid] .blr.bdmin ) ;
dY .ini ( eid, IDC'_F3DMAX, m4e [eid] . blr . bdmax ) ;
dbl2ini ( eid, IDC L~WFLOW, m4e [eid] . fwflor~a ) ;
dbl2ini ( eid, IDC t~IINFWNA, decodedouble(enGOdefloat( naleak2fwna (m4e [eid] .b:lr.naleakmin, m4e [eid] . fwflow) ) ) ) ;
dbl2ini ( eid, IDC PrIAXFWNA, decodedouble(encodefloat( naleak2fwna(m4e[eid] .blr.naleakmax,m4e[eid] .fwflow) ) ) ) ;
dbl2ini( eid, IDC FWNA, decodedouble (en~:odefloat (naleak2fwna (m4e [eid] . blr. 1, m4e [eid] .
fwflow) ) ) ) ;
dbl2ini ( eid, IDC 1?DT, m4e [eid] . blr. fdt ) ;
dbl2ini ( eid, IDC'_~..ASTT, m4e [eid] .blr.lastt ) ;
dbl2ini ( eid, IDC I:~ASTBDTEMP, m4e [eid] .blr.lastbdtemp ) ;
dbl2ini ( eid, IDC I:~ASTP04, m4e [eid] .blr. lastpo4 ) ;
dbl2ini ( eid, IDC _I:.ASTNH3, m4e [eid] .blr. lastnh3 ) ;
dbl2ini ( eid, IDC _','~ASTPH, m4e [eid] .blr.lastph ) ;
dbl2ini ( eid, IDC_134LASTT, °n4e [eid] .blr.b4lastt ) ;
dbl2ini ( eid, IDC 134LASTBDTEMP, m4e [eid] .blr.b4lastbdtemp ) ;
dbl2ini( eid, IDC'B4LASTP04, m4e[eid].blr.b4lastpo4 );
dbl2ini ( eid, IDC_134LASTNH3, m4e [eid] .blr.b4lastnh3 ) ;
dbl2ini ( eid, IDC _I34LASTPH, m4e [eid] . blr. b4lastph ) ;
dbl2ini ( eid, IDC_I3D, decoded.ouble (encodefloat (m4e [eid] .blr.bd) ) ) ;
dbl2ini ( eid, IDC :~ASTL, m4e [eid] .blr.lastl ) ;
dbl2ini ( eid, IDC ::~ASTBD, m4e [eid] .blr. lastbd ) ;
dbl2ini ( eid, IDC _DTl, m4e [eid] .blr.dt [0] ) ;
dbl2ini ( eid, IDC_ DT2, m4e [eid] .blr.dt [1] ) ;
dbl2ini ( eid, IDC'_:~B1, m4e [ei.d] .blr. fb [0] ) ;
dbl2ini ( eid, IDC :~B2, m4e [eid] .blr. fb [1] ) ;
dbl2ini ( eid, IDC~:~B3, m4e [eid] .blr.fb[2] ) ;
dbl2ini ( eid, IDC :~Al, m4e [ei,d] .blr, fa [0] ) ;
dbl2ini ( eid, IDC':rA2, m4e Cei,dl .blr. fa [1] ) ;
dbl2ini ( eid, IDC'_:FA3, m4e [ei.d.l .blr. fa [2] ) ;
dbl2ini ( eid, IDC LASTDT1, m9;e [eid] .blr. lastdt [0] ) ;
dbl2ini( eid, IDC':LASTDT2, m4e[eid].blr.lastdt[1] );
dbl2ini ( eid, IDC'_:LASTFAl, m4e [eid] . blr. lastfa [0] ) ;
dbl2ini ( eid, IDC_LASTFA2, m9;e [eid] .blr.lastfa [1] ) ;
dbl2ini ( eid, IDC LASTFA3, m9:e [eid] .blr. lastfa [2] ) ;
dbl2ini ( eid, IDC'LASTFB1, m9:e [eid] .blr. lastfb [0] ) ;
dbl2ini( eid, IDC LASTFB2, m4e[eid].blr.lastfb[1] );
dbl2ini ( eid, IDC_LASTFB3, m4e [eid] .blr.lastfb [2] ) ;
int2ini ( eid, IDC_UPDATESTA')?LTS , m4e [eid] .blr.updatestatus int2ini ( eid, IDC_LASTUPDATESTATUS, m4e [eid] .blr.lastupdatestatus ) ;
int2ini ( eid, IDC_INITSTATUS, m4e [eid] .blr. initstatus ) ;
int2ini( eid, IDC UNDOABLE, m4e[eid].blr.undoable );
int2ini ( eid, IDC_SIMULATESSP, m4e[eid].simulatessp );
int2ini ( eid, ID~C_LOCALINPUTr m4e [eid] . local input ) ;
int2ini ( eid, IDC NH3ENABLEI), m4e [eid] .nh3enabled ) ;
int2ini( eid, IDC CHECKSUM, m4e[eid].checksum);
/* :randscierror: generates a random SCIL error code */
SCLError randscierror(double scilerrorfraction) static SCIError scierr[]={
ERROR DATA TYPE MISMATCH, ERROR INVALID PROJECT NAME, ERROR LINK ALREADY ESTABLISHED, ERROR LINK NOT CURRENTLY ESTABLISHED, ERROR POINT' NAN-fE NOT FOU1~D, ERROR PROJEC T N'O~' FOUND, ERROR SMARTSCAN PLUS NOT RUNNING, ERROR_SMARTCARD_TIMEOUT
i "( rand() <= RAND MAX*(1.()-scilerrorfraction)) turn (ERROR NONET;
else {
int nerrors = sizeof(scierr)/sizeof(scierr[0]);
return(scierr[rand() % nerrors]>;
/*simulateoperator: simulates input from the operator at regular intervals*/
void simulateoperator(M4SENGINEID eid) {
double t = m4stimeinhours (eicil ;
if (t - m4e [eid] .lasteventt.irne >= SIMHOURSPEREVENT) {
if (m4e [eid] .eventid <
sizeof (simsspinputs) /sizeof (simsspinputs [0] ) -1) {
m4e [eid] .event id++;
int2ini ( eid, IDC EVENT:CD, m4e [eid] . event id ) ;
/* just repeat last event. when we get to the end of the array*/
m4e [eid] . lasteventtime = t ,;
int2ini ( eid, IDC LASTEVEZdTTIME, m4eCeid].lasteventtime );
m4e [eid] . current event = sirnsspinputs [m4e [eid] . event id] ;
%* else robo-operator is idle */
/* 'The following sim* function:a emulate the corresponding SCIL routines:*/
void simSCInitCommLink(SCIError *err) {
*err = randscierror(SCILERRORFRACTION);
void simSCCutCommLink(SCIError *err) {
*.=_rr = randscierror(SCILERRORFRACTION);
void simSCReadDigitalPoint(const char *s,BOOL *bit,SCIError *err) {
M~4SENGINEID eid = s[1] - '0'; /* hack to pull off engine id */
/* from point name prefix */
s += lstrlen(nameprefix(eid)); /* skip over the engine specific prefix*/
if (lstrcmp (s, pid.2name (IDC ~;JPDATE) ) ==0) *bit = m4e[eid].currentevent.update;
else if (lstrcmp(s~, pid2name(IDC UNDO))==0) *bit = m4e [eid] . currenteve:nt . undo ;
else if (lstrcmp(e;, pid2name(IDC REINIT))==0) *bit = m4e[eid].currenteve:nt.reinit;
else M BUGALERT("Invaalid digital point during simulated SSP Read.");
*err = randscierror(SCILERRORFRACTION);
void simSCReadAnaloc_~Point(const char *s, float *value,SCIError *err) M4SENGINEID eid = s[1] - '0'; /* hack to pull off engine id */
/* from point name prefix */
s += lstrlen(nameprefix(eid)); /* skip over the engine specific prefix*/
i f ( lstrcmp ( s , pi.d2name ( IDC_PH) ) ==0 ) *value = e_phmodel(&m4e[eid].blr);
e~:.se if (lstrcmp(s, pid2name(IDC P04))==0) *value = e-po4model (&m4e [eid] .blr) ; -e- ~ if (lstrcmp(s, pid2name(IDC NH3))==0) alue = m4e[eidJ.blr.nh3;
e:_se if (lstrcmp(s, pid2name(IDC BDTEMP))==0) *value = m4e[eid].blr.bdtemp; -a:se M BUGALERT("Inva.Lid analog point during simulated SSP Read.");
*err = randscierro:r (SCILERROF~.FRACTION) ;
void simSCWriteDigitalPoint(corast char *s,BOOL bit,SCIError *err) M~ESENGINEID eid = a [1] - ' 0' ; ; * hack to pull off engine id */
/* from point name prefix */
i1. (eid =- current~~ngineid) {
char buf [MAXSTR:IIVG+1] ;
s += lstrlen(nam~~prefix(eid)); /* skip over the engine specific prefix*/
if (lstrcmp(s, pid2name(IDC UPDATE))==0) {
m4e (eidJ . curre:ntevent . update = bit ;
sprintf(buf, "'update=%i ", bit);
else if (lstrcmp(s, pid2name(IDC UNDO))==0) m4e (eid] . curre:ntevent . undo = bit sprintf (buf, "'vsndo=%i ", bit) ;
else if (lstrcmp(s, pid2name(IDC REINIT))==0) m4e (eid] . curre:ntevent . rei.nit = bit ;
sprintf(buf, "reinit=%i '", bit);
}lse if (lstrcmp(s, pid2name(IDC FLAG1))==0) sprintf(buf, "flags=(%i,"', bits;
else if (lstrcmp(s, pid2name(IDC-FLAG2))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC-FLAG3))==0) sprintf(buf, "%i,", bit>;
else if (lstrcmp(s, pid2name(IDC_FLAG4))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC_FLAGS))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC_FLAG6))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC FLAG'7))==0) sprintf (buf, m4e [eid] .loc:alinput ? "flags=(%i, " . "%i, ", bit) ;
else if (lstrcmp(s, pid2name(IDC_FLAGS))==0) sprintf(buf, "%i,", bit);
else if (lstrcm;p(s, pid2name(IDC~FLAG9))==0) sprintf(buf, "%i,", bit),;
else if ( lstrcm;p ( s, pid2r:~ame ( IDC,FLAG1.0 ) ) ==0 ) sprintf(buf, "%i,", bit.),;
else if (lstrcmp(s, pid2narne(IDC_FLAG11))==0) sprintf(buf, "%i,", bit.);
else if (lstrcmp(s, pid2narne(IDC-FLAG12))==0) sprintf (buf, "%i) ", bi.t) ;
else sprintf(buf,"%s=%i ", s, bit);
marque(buf);
*err = randscierrcr(SCILERRORFRACTION);

void. simSCWriteAnalogPoint(canst char *s,f:loat f,SCIError *err M"°~~NGINEID eid = s[1] - '0'; /* hack to pull off engine id */{
/* from paint name prefix */
if (currentengineid =- eid) char buf [MAXSTRING+1] ;
s += lstrlen(nameprefix(eid)); /* skip over the engine specific prefix*/
if (lstrcmp(s, pid2name(IDC AFEED))==0) sprintf(buf, "a=%.5~ ", 1:~;
else if (lstrcmp(s, pid2name(IDC BFEED))==0) spr{ntf(buf, "b=%.5g ", i:);
else #if TESTING
sprintf(buf,"%s=%.5g ", s, f);
#else sprintf(buf,"");
#endif marque(buf);
*err = randscierror(SCILERRORFRACTION);
/*isvalidnumber: if s is a valid number or numeric prefix, TRUE, else FALSE*/
BOO:L isvalidnumber(char *s) {
char *endofnumber = NULL;
double value = strtod(s, &endofnumber);
if (value <_ -FLT_MAX ~~ value >= FLT MAX) return(FALSE); /* we require that number be in range of f:oats~ */
else /* check if entire number was read by strtod */
return(*endofnumber =- '\0' ? TRUE . FALSE);
/* isnumericprefix: FALSE if s is not valid prefix to a number, else TRUE */
BOO:L isnumericprefix:(char *s) {
s += strspn(s, " "); /* skip leading whitespace 'cause strtad() does*/
if (lstrcmp(s,".")==0 lstrcmp (s, "-") ==0 lst:rcmp (s, " . ") ==0 ( ~
lstrcmp (s, "+" ) ==0 lst:rcmp (s, "+. ") ==0) return(TRUE); /* special cases of valid prefixes that are not numbers*/
else return(isvalidnu.mber(s) ) ;
/* forcevalidnumber: truncates the string to make it a valid number. */
char *forcevalidnumt~er(char *s) {
if (!isvalidnumber(s)) lstrcpy(s, NANSTRING);
return(s);
%*inrange: is x between low and high (ignores NANFLT) */
BOOL inrange(double x, double low, double high) {
if (x =- NANFLT) return(TRUE);
else if ((low != rTANFLT && x < low) ~~ (high != NANFLT && x > high)) return(FALSE);
else return(TRUE);
/* forceinrange: farces x between low and high; ignores NANFLT */

double forceinrangeldouble x, double low, double high) {
if (x =- NANFLT) rseturn (x) ;
e~~ if (low != NFSIFLT && x <= low) _._turn(low);
else if (high != N~~.NFLT && x >= high) return(high);
else return(x);
/* updatessppoints: Nets flags that force MACC4SSP to update SSP
action flags (update, undo, and reinit) and status flags */
/*(a~ctual SSP settings are made in response to the WM TIMER message)*/
/* (need to do this because initial settings of SSP points are often hard to predict in advance, at least in my experience) */
void updatessppoints(M4SENGINEID eid) {
m4e[eid].1_lagsuptodate = FALSE;/*clear old flags...*/
m4e[eid].actionsupt.odate=FALSE;/*...and old actions*/
/*
SmartCard Interface hibrary (SCIL) based routines for accessing SmartScan Plus */
SCIE;rror sciinit (M4SENGINEID eid) {
SC'IError err = ERROR_NONE;
#if WORKAROUND
static BOOL initia:Lized = FALSE;
if (initialized) {
m4e [eid] . scierr :- err;
return(err);
#endi f if' ( !m4e [eid] . simu:Latessp) SCInitCommLink (&err) ;
if (ERROR SMARTSCAif PLUS_NOT RUNNING =- err) updatessppoints(eid); /* when SSP does start up, points must be*/
/* in a well defined initial state */
m4:e[eid].scierr = f~rr; /* note that this sets the scierr field */
/* r_o ERROR_NONE if Init succeeded *;"
#if WORKAROUND
if: (err =- ERROR_NONE) initialized = TRUE;
#endif return (err) ;
SCIE:rror scifini (M4S:ENGINEID ei.d) {
SC:IError err=ERROR_NONE;
#if WORKAROUND
return (err) ;
#endif if. ( !m4e [eid] . simulatessp) SC:CutCommLink (&err) ;
return(err);
/* rnaybestoreerror: remember error if the 1st seen since errors were cleared */
SCIError maybestoreerror(M4SENGINEID eid, M4SPARAMID pig, SCIE:rror err) i1° (m4e [eid] . scie:rr =- ERROR_ NONE && err ! = ERROR NONE) m4e [eid] . scierr = err;
m4e [eid] . pidscierr = pig;
rf~turn (err) ;
%* v~imedoutbefore: returns trues if the system timed since errors Were cleared*/

05/07i2405 12:01 FAg 615 787 3558 _~ BLG CANADA ~ 002 11 P P E ~1 D I I 8 "..~,..,._r. _,.,."_.
...._....."".~,"~.".~.~.....w....,.~,.,..~,.y".,~.,.,..,...» , . ,~,., ~.w.,....".~ .... "",".,o,~rM»",,","," a.,.,............,-... ......"r", ,..~.....~,..~.__,., "_~

05/07/2003 12:01 FAg 813 787 3558 .~_,BLG CANADA ~ 003 ,._. - ...
a ~ ~ H 0 w ~~~.51~'~ ~~~ ~ ~ ~~ ~ a ~ a ~~ ' !5 .~~~ ~ LJLJ Q
.... _...""___...~.~a.".,. . . ...
.., ~..,.~.","".,.". - . ~ . . _.,... _....... ..._.. .

05/07/2003 12:01 FAX 813- 787 3558 BLG CA1~ADA ~J004 LJ o _. w W

_ o a s a 0 g v o ~ ' ~.
~

~ ~ .~,~
a r.a U
'-'~' al x a x u1 ~ a ~ ~~ ~a ~ Oa 05/0,7/2003.12:02 FA8 613 787 3558 BLG CANADA . ~ 006 a w n fn ,.
v an o d a v rn v H
d a . a d c a E
E

U
n U7 a ~

a .~.
a a-io a a~
~

_ m w ~

U O
v m L

ou U
D
.D
N
N
f..~:
Z
d-CL
_._.. __~"."~",~,~...
CA 02415685 2003-02-04 '""°""'~"""""'°"'"""*""'",. ~'~,°-'°~-~ .~ , . ,.... . _......_._. .

05/07/Z009 12:02 FAX 61~ 787 3558 BLG C~~~A

tl~
g n Q_ oa 0 '"
o a~
R op ._. __ r, t ~~ .. ~Q~ ~ to m V
V

a a ~ ~ Ln p p ~ .v ~

a ~ ~ ~ a 3~
~

g ~ ~
a D
"

Q ;t~ ~ ~ ~ _.s-~ N
c U

00 ~ ~=~ as ~ '~~ a ~ ~ a cn Q
m a, w .. ~ as o 0 s U

e..,~ na Qa Z~~~ a U
O
U
Q?
I
d-CA 02415685 2003-02-04 ., ... . ."," ~ w""",~ .... . . .

Claims (68)

CLAIMS:
1. A method for controlling at least two interdependent chemicals in the fluids of at least two continuously stirred tank reactors (CSTRs) having respective blowdown flows and steam rate flows defining respective cycles for each of said CSTRs, said at least two interdependent chemicals being fed to said fluids through a common feedwater to each of said at least two CSTRs, said method comprising the steps of:
(a) establishing a respective mathematical model of each of said CSTRs;
(b) monitoring the concentration of one of said at least two interdependent chemicals in each of the fluids, the temperature at which the pH is measured in each of the fluids, the respective blowdown flow and the respective steam rate flow;
(c) updating the models based on the concentration of said one of said at least two interdependent chemicals in each of the fluids, the pH of each of the fluids, the temperature at which the pH is measured in each of the fluids, the respective blowdown flow and the respective steam rate flow;
(d) defining a respective target region of said at least two interdependent chemicals in said respective continuously stirred tank reactors and wherein said respective target regions are scaled according to the respective cycles, said respective target regions establishing a state of congruency for each of the fluids;
(e) providing a feedstream of a high-pH fluid treatment material comprising a mixture of said at least two interdependent chemicals and a feedstream of a low-pH fluid treatment material comprising a mixture of said at least two interdependent chemicals for feeding, at respective feed rates, to the fluids; and (f) developing an optimum feed rate program for controlling said feedstreams to automatically minimize the time that said at least two interdependent chemicals in the fluids spend outside of a common normalized target region formed by the intersection of said respective target regions.
2. The method of claim 1 wherein each of said fluids has associated therewith a respective pumpable region in a CSTR
state space that defines all reachable concentrations of said at least two interdependent chemicals in the respective fluid and including therein said respective target region for defining congruency of said at least two interdependent chemicals in said respective pumpable region, said respective pumpable region also being scaled according to said respective cycles, said step of developing an optimum feed rate program comprising overlaying said respective pumpable regions and said respective target regions to define a single pumpable region and to establish said common normalized target region defined by the intersection of said overlayed respective target regions.
3. The method of claim 2 wherein said fluids comprise respective current concentrations of said at least two interdependent chemicals and wherein said step of developing an optimum feed rate program comprises the steps of:
(a) establishing a respective region formed by the endpoint of all feed trajectories that move said respective current concentrations of said fluids into said common normalized target regions;
(b) selecting a new region formed by the the intersection of said respective regions, said new region formed by the intersection of said respective regions defining a common normalized pumpable region.
4. The method of claim 3 wherein said common normalized target region comprises vertices, including extreme vertices, and edged and wherein said step of establishing a respective region comprises forming a respective region defined by feed trajectories that originate from said current concentrations and intersect said extreme vertices for each of said respective current concentrations said fluids.
5. The method of claim 4 wherein said step of developing an optimum feed rate program further comprises step of evaluating, for each of said continuously stirred tank reactors, sets of feed rate trajectories between the current concentrations of said at least two interdependent chemicals and said common normalized target region to determine the time required to move said current concentrations into said common normalized target region.
6. The method of claim 5 wherein said common normalized pumpable region comprises vertises vertices and edges, together which define a common normalized pumpable region perimeter, and wherein said step of evaluating comprises the steps of:

(a) determining the time associated with driving the current concentrations of said at least two interdependent chemicals in each of said fluids along a first set of feed rate trajectories formed between the current concentrations and said vertices of said common normalized pumpable region;
(b) determining the time associated with driving the current concentration of said at least two interdependent chemicals in each of said fluids along a second set of feed rate trajectories formed between said current trajectories and said common normalized target region vertices, said second set of feed rates being projected until they intersect. said common normalized pumpable region perimeter, if at all, to define a third set of feed rate trajectories;
and (c) selecting one feed rate trajectory, from all of said first and third sets of feed rate trajectories from each of said fluids, that requires the least amount of time for at least one fluid to reach an edge of said common normalized target region.
7. The method of claim 6 wherein said feedstream of a high-pH fluid treatment material comprising a mixture of said at least two interdependent chemicals defines a first feedstream and wherein said feedstream of a low-ph fluid treatment material comprising a mixture of said at least two interdependent chemicals defines a second feedstream and wherein said selected one feed rate trajectory comprises an endpoint and wherein said method further comprises the step of determining how long to feed said first and second feedstreams at said selected one feed rate trajectory, said step of determining how long to feed said first and second feedstream comprising the steps of:

(a) for each of said at least two CSTR fluids establishing a respective feed rate trajectory defined by a line from said respective current concentrations of said at least two interdependent chemicals to the endpoint of said selected one feed rate trajectory;
(b) for each of said at least two CSTR fluids, determining the point at which said respective feed rate trajectory interesects an edge of said common normalized target region and determining the time associated with moving said current concentrations to said point, said intersected edge being a common normalized target region edge different from said edge reached by said at least one fluid in the least amount of time; and (c) selecting the minimum of those times associated with moving raid current concentrations to said point.
8. The method of claim 7 further comprising the step of feeding said first and second feedstreams at said selected one feed rate trajectory for an amount of time corresponding to said selected minimum of those times associated with moving said current concentrations to said point.
9. The method of claim 8 wherein each of said CSTR state spaces are updated based on said feeding said first and second feedstreams at said selected one teed rate trajectory for said selected minimum of those times associated with moving said current concentrations to said point.
10. The method of claim 9 further comprising the steps of:

(a) establishing a new common normalized target region that is nested within said common normalized target region;
and (b) repeating the steps of selecting one feed rate trajectory that requires the least amount of time for at 1east one fluid to reach an edge of said new common normalized target region; and (c) repeating the steps for determining how long to feed said first and second feeds-reams.
11. The method of claim 7 further comprising the step of feeding said first and second feedstream at said selected one feed rate trajectory for an amount of time corresponding to the time it takes for new data to be available, said data being defined as said concentration of said one of said at least two interdependent chemicals in the fluids, said pH of said fluids, said temperature at which the pH is measured, said blowdown flow and said steam rate flow.
12. The method of claim 11 wherein each of said fluid state spaces are updated based on said feeding said first and second feedstream at said selected one feed rate trajectory for said amount of time corresponding to the time it takes for new data to be available.
13. The method of claim 11 wherein a new common normalized target region is recomputed based on said new data.
14. The method of claim 2 wherein said feedstream of a high-pH fluid treatment material comprising a mixture of said at least two interdependent chemicals defines a first feedstream and wherein said feedstream of a low-pH fluid treatment material comprising a mixture of said at least two interdependent chemicals defines a second feedstream and wherein said fluids comprise respective current concentrations of said at least two interdependent chemicals and wherein said step of developing an optimum feed rate program comprises the steps of:
(a) evaluating, for each of said CSTRs, gets of feed rate trajectories between the current concentrations of said at least two interdependent chemicals and said common normalized target region to determine the time required to move said current concentrations into said common normalized target region;
(b) selecting that feed rate trajectory that moves at least one fluid current concentrations into said common normalized target region in the least amount of time; and (c) feeding said first and second feedstreams at said selected feed rate trajectory.
15. The method of claim 14 further comprising the steps of:

(a) establishing a new common normalized target region that is nested within said common normalized target region;
and (b) evaluating, for each of said CSTRs, sets of feed rate trajectories between the current concentrations of said at least two interdependent chemicals and said new common normalized target region to determine the time required to move said current concentrations into said new common normalized target region;
(c) selecting a feed rate trajectory that moves at least one fluid current concentration into said new common normalized target region in the least amount of time; and (d) feeding said first and second feedstreams at said selected feed rate trajectory.
16. The method of claim 1 wherein each of said CSTRs is an industrial boiler having a boiler fluid.
17. The method of claim 16 wherein said one of said at least two interdependent chemicals is phosphate.
18. The method claim 17 wherein said one of said at least two interdependent chemicals is sodium.
19. The method of claim 18 wherein said method further includes the step of estimating the blowdown flow.
20. The method of claim 19 wherein said method further includes the steps of calculating the phosphate concentration and the sodium concentration in the boiler fluid.
21. The method of claim 20 wherein said method further includes the step of estimating a feedwater contaminant ingress.
22. The method of claim 21 wherein said steps of estimating a blowdown flow and a feedwater contaminant ingress are based on a series of phosphate and pH measurements of the boiler fluid wherein said method uses small sample intervals.
23. The method of claim 1 wherein said method further includes a step that accounts for dead time in each of the continuously stirred tank reactors.
24. The method of claim 1 wherein said method further comprises the step of controlling the blowdown flow of each of said CSTRs.
25. An automatic control system for controlling at least two interdependent chemicals in the fluids of at least two continuously stirred tank reactors (CSTRs) linked in parallel by a common feedwater line and wherein each CSTR
includes a respective blowdown flow and stream rate flow that define respective cycles for each of said CSTRs and wherein each CSTR has associated therewith a respective target region of said at least two interdependent chemicals, said respective target regions be ing scaled according to the respective cycles of said CSTRs, said control system comprising:
input means for receipt. of fluid parameters and control means responsive to sai input means;
said control means using non-proportional control for automatically minimizing the time that said at least two interdependent chemicals in the fluids spend outside of a common normalized target region formed by the intersection of said respective target regions of said at least two CSTRs;
wherein one of said fluid parameters comprises the pH of the fluid and wherein said input means comprises means for determining the pH value of each of the fluids; and wherein said control means comprises a first feedstream and a second feedstream for feeding first and second fluid treatment materials, respectively, to the common feedwater line at respectively determined feed rates, said first material comprising a mixture of sodium and phosphate having a first predetermined sodium-to-phosphate ratio and said second material comprising a mixture of sodium and phosphate having a second predetermined sodium-to-phosphate ratio.
26. The control system of claim 25 wherein said control means further comprises an adaptive controller, said adaptive controller modeling of each of said at least two CSTRs.
27. The control system of claim 26 wherein said control means further comprises monitoring means for monitoring the concentration of said at least two interdependent chemicals in said fluids, the temperature at which the pH is measured, the blowdown flow and the steam rate flow, said monitoring means being coupled to said adaptive controller in order to update said model inch of each of said at least two CSTRs, the concentration of said at least two interdependent chemicals in said fluids, the temperature at which the pH is measured, the blowdown flow and the steam rate flow being defined as data.
28. The control system of claim 27 wherein said adaptive controller generates said respective target regions in a CSTR state space for each of said at least two CSTRs.
29. The control system of claim 28 wherein said adaptive controller overlays said respective target regions to generate said common normalized target region.
30. The control system of claim 29 wherein each fluid comprises a current concentration for each of said at least two interdependent chemicals and wherein said adaptive controller analyzes all feed rate trajectories of said first and second feedstreams that will drive said current concentrations in each of said fluids from said current concentrations to concentrations within said common normalized target region in said CSTR state space, said analyzation determining a respective region for each of said fluids that is formed by the endpoints of all said feed rate trajectories.
31. The control system of claim 30 wherein said adaptive controller selects that region in said CSRT state space that is formed by the intersection of said respective regions, said selected region forming a common normalized pumpable region that defines all reachable concentrations of said at least two interdependent chemicals among said at least two CSTR fluids, said common normalized pumpable region comprising a first set of edges and a first set set of vertices.
32. The control system of claim 31 wherein said common normalized target region comprises a second set of vertices and second set of edges, said adaptive controller determining for each of said CSTRs:

(a) the time associated with driving said current concentrations of said at least two interdependent chemicals along a first set of feed rate trajectories formed between said current concentrations and said second set of vertices;
and (b) the time associated with driving said current concentrations of said at least two interdependent chemicals along a second set of feed rate trajectories formed between said current concentrations and said second set of vertices, said second set of feed rate trajectories being projected until they intersect said first set of edges of said common normalized pumpable region to define a third set of feed gate trajectories, said adaptive controller selecting one feed rate trajectories that all of said first and third sets of feed rate trajectories that requires the least amount of time for at least one fluid to reach one of said second set of edges.
33. The control system of claim 32 wherein said adaptive controller calculates how long to feed said first and second feedstreams at said selected feedrate trajectory, said adaptive controller:

(a) establishing, for the other CSTRs of said at least two CSTRs whose current concentrations have not reached one of said second set of edges, a respective feedrate trajectory between said current concentration and said selected feedrate trajectory;
(b) said adaptive controller determining, for the other CSTRs whose current concentrations have not reached one of said second set of edges, the point at which each of said respective feedrate trajectories intersects one of said second set of edges and determining the time associated with moving said current concentrations to said point, said intersected edge being a common normalized target region edge different from said edge of said common normalized target region reached try said at least one CSTR fluid in the least amount of time, said adaptive controller selecting the minimum of those times associated with moving said current concentrations to said point and feeding said first and second feedstrearms at said selected feedrates for said selected minimum time.
34. The control system of claim 33 wherein said monitoring means updates said fluid state spaces for each of said CSTRs based or said adaptive controller feeding said first and second feedstreams at said selected feedrate trajectories for said selected minimum time.
35. The control system of claim 34 wherein said adaptive controller establishes a new common normalized target region that is nested within said common normalized target region and wherein said adaptive controller determines one feedrate trajectory that requires the least amount of time for at least one of said CSTRs to reach an edge of said new common normalized target region and determines how long to feed said first and second feedstreams at said one feedrate trajectory.
36. The control system of claim 32 wherein said adaptive controller feeds said first and second feedstreams at said selected feedrates for an amount of time corresponding to the time it takes for new data to be available.
37. The control system of claim 35 wherein said monitoring means updates said CSTR state spaces for each of said CSTRs based on said adaptive controller feeding said first and second feedstreams at said selected feedrate trajectories for said time it takes for new data to be available.
38. The control system of claim 37 wherein, said adaptive controller recomputes a new common normalized target region based on said new data.
39. A method for controlling the sodium-to-phosphate ratio of the fluids of at least two continuously stirred tank reactors (CSTRs) having respective blowdown flows and steam rate flows defining respective cycles for each of said at least two continuously stirred tank reactor, said fluids of said at least two CSTRs being fed through a common feedwater line, said method comprising the steps of:

(a) providing a supply of a first sodium phosphate fluid treatment material to said common feedwater, said first sodium phosphate fluid treatment material having a first predetermined sodium-to-phosphate ratio and a first known phosphate concentration;
(b) providing a supply of a second sodium phosphate fluid treatment material to said common feedwater, said second sodium phosphate fluid treatment material having a second predetermined sodium-to-phosphate ratio and a second known phosphate concentration;
(c) measuring a fluid parameter of each of said fluids substantially continuously;
(d) determining the cycle of each of said at least two CSTRs substantially continously;
(e) estimating the phosphate concentration in each of said fluids;
(f) determining the effective sodium in each of said fluids;
(g) determining the sodium-to-phosphate ratio in each of said fluids and identifying a maximum sodium-to-phosphate ratio and a minimum sodium-to-phosphate ratio among said fluids, said minimum and maximum sodium-to-phosphate ratios defining a first range having a first midpoint; and (h) feeding said first sodium phosphate fluid treatment material if said first midpoint is less than or equal to a second midpoint of a predetermined ratio range of sodium-to-phosphate, of feeding said second sodium phosphate fluid treatment material if said first midpoint is greater than said second midpoint.
40. The method of claim 39 wherein said step of determining the cycle of each of said at least two CSTRs substantially continuously comprises the steps of:

(a) measuring each of said blowdown flows substantially continuously; and (b) measuring the total feedwater flow substantially continuously.
41. The method of claim 40 wherein said step of feeding said first sodium phosphate fluid treatment material or said second sodium phosphate fluid treatment material comprises feeding said first or second sodium phosphate fluid treatment material at a rate which maintains the respective phosphate concentration of each boiler fluid between a respective predetermined upper phosphate control limit and a respective predetermined lower phosphate control limit.
42. The method of claim 41 wherein said respective predetermined upper phosphate control limits are identical and which form a first phosphate control limit, and wherein said respective predetermined lower phosphate control limits are identical and which form a second phosphate control limit.
43. The method of claim 42 wherein said step of feeding said first sodium phosphate fluid treatment material or said second sodium phosphate fluid treatment material is fed at a rate given by:

where TotalFeedwater is said total feedwater flow;
FeedPO4 is said first known phosphate concentration or said second known phosphate concentration, depending on which fluid treatment material is being fed;
PO4Bound is said first phosphate control limit;

PO4Bound is said second phosphate control limit;
Cycles is said CSTR having a maximum cycle value wherein said cycle is defined as:

and Cycles is said CSTR having a minimum cycle value, Steam (i) is the Steam flow rate for the "ith" boiler and Blowdown (i) is the blowdown flow for the "ith" CSTR and i=CSTR index for identifying a particular CSTR of said at least two CSTRs.
44. The method of claim 43 wherein said step of feeding said first sodium phosphate fluid treatment material or said second sodium phosphate fluid treatment material at said rate occurs as long as the following condition is met:

Cycles /Cycles PO4Bound /PO4Bound
45. The method of claim 41 wherein said step of feeding said first sodium phosphate fluid treatment material or said second sodium phosphate fluid treatment material is fed at a rate given by:
FeedRate = .alpha. = maxi FeedRate min(i) + (1 - .alpha.)* min(FeedRate min(i), where 0 <= .alpha. <= 1;
i=
CSTR index for identifying a particular CSTR of said at least two CSTRs;
FeedRates min(i) =PO4Bound min(i)/ Cycle(i)*(TotalFeedWater)/FeedPO4;
FeedRate ~(i) = PO4Bound min(i) / Cycle(i)* (TotalFeedWater) /FeedPO4 TotalFeedwater is said total feedwaterflow;
FeedPO4 is said first known phosphate concentration or said second known phosphate concentration, depending on which fluid treatment material is being fed;
PO4Bound max(i) is said respective predetermined upper phosphate control limit;
PO4Bound min(i) is said respective predetermined lower phosphate control limit; and Cycle(i) is defined as:

46. The method of claim 45 wherein said step of feeding said first sodium phosphate fluid treatment material or said second sodium phosphate fluid treatment at said rate occurs as long as the following condition is met:

max(PO4Bound min(i)/Cycle(i))~min(PO4Bound max(i)/
Cycle(i)).
47. The method of claim 46 wherein said step of determining the effective sodium in each of said fluids comprises back calculating sodium using a model projected phosphate concentration given by;

PO4Est(i,r+dr)= PO4(i)+PO4Est(i,t) where PO4Est (i,t) is the estimated concentration in the fluid at time t;
i=CSTR index for identifying a particular CSTR of said at least two CSTRs;
PO4 (i) is the steady-state phosphate concentration;
T is the characteristic time of the fluid; and at is the time between interval samples.
48. The method of claim 41 wherein said fluid parameter is the pH of the fluid.
49. The method of claim 41 wherein said first known phosphate concentration is identical to said second known phosphate concentration.
50. The method or claim 39 wherein said method further comprises the step of controlling the blowdown flow of each of said CSTRs.
51. A system for simultaneously controlling respective sodium-to-phosphate ratios of at least two boiler fluids of respective industrial boilers that are fed through a common feedwater, the industrial boilers having respective blowdown flows and steam rate flows that define respective cycles for each boiler fluid, said system comprising:
input means for receipt of a boiler fluid parameter for each of the at least two boiler fluids and a parameter indicative of the cycles of each of said industrial boilers;
and control means responsive to said input means for automatically driving the respective sodium-to-phosphate ratios of said at least two boiler fluids to a desired sodium-to-phosphate ratio region, said control means comprising model phosphate projecting means for estimating the sodium-to-phosphate ratios in each of said at least two boiler fluids.
52. The system of claim 51 wherein said boiler fluid parameter comprises the pH of the boiler fluid, the respective pH of each of the boiler fluids being defined by the respective sodium-to-phosphate ratio and the respective cycles and wherein said input means comprises a respective pH meter for determining the respective pH value of each of the boiler fluids and providing the respective pH value to said control means.
53. The system of claim 51 wherein said control means comprises a first feedstream and a second feedstream for feeding first and second fluid treatment materials, respectively, to the common feedwater, said first material comprising a mixture of sodium and phosphate having a first material comprising a mixture of sodium and phosphate having a second predetermined sodium-to-phosphate ratio and a second predetermined concentration of phosphate.
54. The system of claim 53 wherein said first predetermined concentration of phosphate and said second predetermined concentration of phosphate are identical.
55. The system of claim 53 wherein said control means comprises feeding means for feeding said first feedstream or said second feedstream at a rate which maintains the respective phosphate concentration of each boiler fluid between a respective predetermined upper phosphate control limit and a respective predetermined lower phosphate control limit.
56. The system of claim 55 wherein said respective predetermined upper phosphate control limits are identical, referred to as a first phosphate control limit, and wherein said respective predetermined lower phosphate control limits are identical, referred to as a second phosphate control limit.
57. The system of claim 56 wherein said control means further comprises means for feeding said first feedstream or said second feedstream at a rate given by:

where TotalFeedwater is the flow of said common feedwater;
FeedPO4 is said first predetermined phosphate concentration or said second predetermined phosphate concentration;
PO4Bound is a predetermined maximum phosphate concentration;
PO4Boudmin is a predetermined minimum phosphate concentration;
Cycles is said boiler having a maximum cycle value wherein said cycle is defined as:

and Cycles is said boiler having a minimum cycle value, Steam (i) is the steam flow rate for the "ith" boiler and Blowdown (i) is the blowdown flow for the "ith" boiler and i=boiler index for identifying a particular boiler of said at least two boilers.
58. The system of claim 57 wherein said feeding means includes monitoring means that permits said feeding means to feed at said rate whenever the following condition is met:

Cycles /Cycles PO4Bound/PO4Bound
59. The system of claim 58 wherein said monitoring means alerts an operator if said condition is not met.
60. The system of claim 55 wherein said feeding means feeds said first feedstream or said second feedstream at a rate given by:

FeedRate min(i) = PO4Bound min/Cycle(i)*
(TotalFeedWater)/FeedPO4;
FeedRate max(i) = PO4Bound max(i)/Cycle(i)*
(TotalFeedWater)/FeedPO4 TotalFeedwater is the flow of said common feedwater;
FeedPO4 is said first predetermined phosphate concen-tration, or said second predetermined phosphate concentration, depending on which fluid treatment material is being fed;
PO4Bound max(i) is said respective predetermined upper phosphate control limit;
PO4Bound min(i) is said respective predetermined lower phosphate control limit; and Cycle(i) is defined as:

61. The system of claim 60 wherein said feeding means includes monitoring means that permits said feeding means to feed at said rate whenever the following condition is met:
max(PO4Bound min(i)/Cycle(i)) min(PO4Bound min/Cycle(i)).
62. The system of claim 61 wherein said control means further comprises means for estimating the phosphate concentration in each of the boiler fluids.
63. The system of claim 62 wherein said control means further comprises means for back-calculating the sodium concentration in each of said boiler fluids.
64. The system of claim 63 wherein said means for back-calculating the sodium concentration in each of said boiler fluids uses the following model projected phosphate concentration:

where PO4Est (i,t) is the estimated concentration in the fluid at time t;
i=boiler index for identifying a particular boiler of said at least two boilers;
PO4 (i) is the steady-state phosphate concentration;
T is the characteristic time of the fluid; and dt is the time between interval samples.
65. The system of claim 62 wherein said control means further comprises means for determining a sodium-to-phosphate ratio for each of said boiler fluids and for identifying a maximum sodium-to-phosphate ratio and a minium sodium-to phosphate ratio from all of said boiler fluids to define a first range having a first midpoint.
66. The system of claim 62 wherein said feeding means feeds said first feedstream if said first midpoint is less than or equal to a second midpoint of a predetermined ratio range of sodium-to-phosphate, or feeding said second feedstream if said first midpoint is greater than said second midpoint.
67. The system of claim 61 wherein said monitoring means alerts an operator if said condition is not met.
68. The system of <claim 1 wherein said said system does not control the blowdown flow of each of said boilers.
CA002415685A 1994-10-11 1995-08-24 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler Expired - Lifetime CA2415685C (en)

Applications Claiming Priority (3)

Application Number Priority Date Filing Date Title
US08/321,338 1994-10-11
US08/321,338 US5696696A (en) 1994-10-11 1994-10-11 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler
CA002156880A CA2156880C (en) 1994-10-11 1995-08-24 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler

Related Parent Applications (1)

Application Number Title Priority Date Filing Date
CA002156880A Division CA2156880C (en) 1994-10-11 1995-08-24 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler

Publications (2)

Publication Number Publication Date
CA2415685A1 CA2415685A1 (en) 1996-04-12
CA2415685C true CA2415685C (en) 2003-12-23

Family

ID=25678122

Family Applications (1)

Application Number Title Priority Date Filing Date
CA002415685A Expired - Lifetime CA2415685C (en) 1994-10-11 1995-08-24 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler

Country Status (1)

Country Link
CA (1) CA2415685C (en)

Families Citing this family (2)

* Cited by examiner, † Cited by third party
Publication number Priority date Publication date Assignee Title
WO2007038533A2 (en) * 2005-09-28 2007-04-05 Saudi Arabian Oil Company System to predict corrosion and scaling, program product, and related methods
US8489240B2 (en) * 2007-08-03 2013-07-16 General Electric Company Control system for industrial water system and method for its use

Also Published As

Publication number Publication date
CA2415685A1 (en) 1996-04-12

Similar Documents

Publication Publication Date Title
CA2156880C (en) Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler
Rami et al. LMI optimization for nonstandard Riccati equations arising in stochastic control
Gustafsson et al. Nonlinear and adaptive control of pH
US20230386314A1 (en) Methods for indoor gas leakage disposal of smart gas and internet of things systems thereof
US20070208549A1 (en) Updating and Utilizing Dynamic Process Simulation in an Operating Process Environment
CA2415685C (en) Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler
Torre et al. Mixed culture model of anaerobic digestion: application to the evaluation of startup procedures
US6694195B1 (en) Diagnostic system for irrigation controllers
Brdys et al. Intelligent model predictive control of chlorine residuals in water distribution systems
Reutter III Maintenance is a management problem and a programmer's opportunity
TW392052B (en) Method for controlling at least two interdependent chemicals in the fluids of at least two continuously stirred tank reactors and automatic control system thereof, and method for controlling the sodium-to-phosphate ratio of the fluids of at least
JP3352171B2 (en) Solution neutralization control method
De Souza et al. An evaluation of the suitability of the limestone based sidestream stabilization process for stabilization of waters of the Lesotho highlands scheme
Meurice et al. The oscillatory behavior of the high-temperature expansion of Dyson's hierarchical model: A renormalization group analysis
JPH0862852A (en) Device for diluting concentrated developer
Gavaggio et al. Using models to allow for object oriented programming of the vacuum control systems
Gessing Two-level stochastic control for the linear quadratic problem related to a static system
Grivé et al. Simple functions spreadsheet tool: phosphates update and temperature assessment
JP2582437B2 (en) Plant operation guidance device
Huang et al. Self-tuning pH control in Dyeing
Puta et al. Model based optimisation of a waste water treatment plant
Lombra Modeling changes in monetary policy regimes
Joo et al. Control of the dissolved oxygen concentration in the activated sludge process
KR100205005B1 (en) Semiconductor manufacture apparatus
Pinder et al. Modeling multivariate co integrated systems: Insights from non‐linear dynamics

Legal Events

Date Code Title Description
EEER Examination request
MKEX Expiry

Effective date: 20150824